/ DEVOPS, AWS

Provision Infrastructure with Packer

본 글은 HashiCorp Learn - Provision Infrastructure with Packer에서 다루는 내용을 기반으로 작성한 글 입니다.


Packer를 사용해 AWS AMI를 만들고 Terraform과 연계하여 활용하는 방법에 약간의 설명과 팁을 담아 한국어로 재작성해 보았습니다. (설치와 관련된 준비사항은 생략되어 있으므로 원문을 확인해주세요.)


프로젝트 구조

먼저, 해당 튜토리얼을 진행하기 위한 프로젝트 구조는 아래와 같습니다. 아래와 폴더와 파일을 준비해주세요.

.
└── provision-infra-with-packer
    ├── images
    │   └── image.pkr.hcl
    ├── instances
    │   ├── main.tf
    │   └── variables.tf
    └── scripts
        └── setup.sh


Local SSH key 생성하기

provision-infra-with-packer 폴더 안에서 AWS AMI로 만들 인스턴스에 접속하기 위한 SSH 키를 생성합니다. 필자는 사용할 공개키를 Mac OS에서 ssh-keygen으로 생성하였습니다. 각자 환경에 맞는 방법으로 SSH 공개키를 생성하세요. 암호화 타입(-t)을 RSA로 주석(-C)을 이메일로 생성되는 공개키의 위치(-f)를 현재 directory로 설정하고 공개키의 이름을 tf-packer로 설정했습니다. 명령어를 입력하고 비밀번호를 입력하면 되지만 편의상 공백으로 두겠습니다.

$ ssh-keygen -t rsa -C "your_email@example.com" -f ./tf-packer

이후 tf-packertf-packer.pub 2가지 파일이 생성되었다면 다음 단계 🚀


Packer 코드 작성하기

image.pkr.hcl 파일에서 진행합니다.

1️⃣ AMI 구축에 필요한 config 작성

packer가 빌드한 이미지가 저장될 리전의 정보와 AMI에 timestamp 정보를 넣기 위한 config를 차례로 작성합니다.

variable "region" {
  type    = string
  default = "us-east-1"
}

locals { timestamp = regex_replace(timestamp(), "[- TZ:]", "") }

2️⃣️ Base AMI에 대한 Source AMI config 작성

packer가 이미지를 만들기 위해 기본으로 사용되는 Base AMI에 대한 정보를 source 블록에 정의합니다. 1단계에서 작성한 config 값을 활용해 리전과 timestamp를 넣어주는 코드와 Packer Builder로 사용할 인스턴스 타입을 지정합니다. 이후, AWS에 존재하는 수많은 AMI 중에서 source로 활용할 이미지를 filter 코드로 작성합니다. (name 부분에 직접 ami 번호를 명시적으로 기재 할 수도 있습니다.)

source "amazon-ebs" "example" {
  ami_name      = "learn-terraform-packer-${local.timestamp}"
  instance_type = "t2.micro"
  region        = var.region
  source_ami_filter {
    filters = {
      name                = "ubuntu/images/*ubuntu-xenial-16.04-amd64-server-*"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["099720109477"]
  }
  ssh_username = "ubuntu"
}

3️⃣ Packer build config 작성

build 부분에서는 환경변수 세팅이나 명령어를 inline 형태로 기입 할 수 있지만, 아래와 같은 간단한 기능만 수행하도록 코드를 작성합니다. hcl 문법에 따라 source를 지정하고, 프로비저닝을 위한 공개키의 위치를 명세합니다. source는 로컬 머신, destination은 원격 머신 입니다. 마지막으로 Packer로 빌드한 이미지에서 Application Setup이 담긴 script를 지정합니다.

build {
  sources = ["source.amazon-ebs.example"]

  provisioner "file" {
    source      = "../tf-packer.pub"
    destination = "/tmp/tf-packer.pub"
  }
  provisioner "shell" {
    script = "../scripts/setup.sh"
  }
}


4️⃣ Shell script 작성

해당 작업은 scripts 폴더의 setup.sh에서 진행합니다.

이 부분은 Terraform으로 프로비저닝 한 인프라를 웹페이지에서 확인하기 위해 간단한 샘플을 띄우는 코드가 담겨있습니다. 아래 Script에 필요한 종속성 설치, terraform을 user에 추가, 생성한 SSH키 설치, 샘플 Go App 설치가 단계가 작성되어 있습니다.

1 ~ 4단계를 마쳤다면 images 폴더 위치에 packer build image.pkr.hcl 명령어로 이미지를 빌드합니다.


이미지 빌드 후, 콘솔 Images 탭의 AMIs을 확인하면 빌드한 이미지가 존재합니다. 또한, 인스턴스 탭을 확인하면 아래와 같이 Base AMI를 만들기 위한 Packer Builder의 흔적을 볼 수 있습니다. Packer_Builder 해당 화면의 Instance ID를 눌러 설정들을 확인해보면 source 블록에 정의한 값을 바탕으로 만들어진 모습을 확인할 수 있습니다.


Terraform으로 Packer 이미지 배포

해당 작업은 instances 폴더에서 진행합니다.

Sample App Infra Code 작성하기

아래 🛠 이모티콘을 클릭하여 main.tfvaraiables.tf를 작성합니다.

🔨 main.tf 🔨


terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.26.0"
    }
  }
  required_version = "~> 1.0.2"
}

provider "aws" {
  region = var.region
}

resource "aws_vpc" "vpc" {
  cidr_block           = var.cidr_vpc
  enable_dns_support   = true
  enable_dns_hostnames = true
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id
}

resource "aws_subnet" "subnet_public" {
  vpc_id     = aws_vpc.vpc.id
  cidr_block = var.cidr_subnet
}

resource "aws_route_table" "rtb_public" {
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
}

resource "aws_route_table_association" "rta_subnet_public" {
  subnet_id      = aws_subnet.subnet_public.id
  route_table_id = aws_route_table.rtb_public.id
}

resource "aws_security_group" "sg_22_80" {
  name   = "sg_22"
  vpc_id = aws_vpc.vpc.id

  # SSH access from the VPC
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web" {
  ami                         = "ami-YOUR-AMI-ID"
  instance_type               = "t2.micro"
  subnet_id                   = aws_subnet.subnet_public.id
  vpc_security_group_ids      = [aws_security_group.sg_22_80.id]
  associate_public_ip_address = true

  tags = {
    Name = "Learn-Packer"
  }
}

output "public_ip" {
  value = aws_instance.web.public_ip
}
🔧 varaiables.tf 🔧


variable "cidr_vpc" {
  description = "CIDR block for the VPC"
  default     = "10.1.0.0/16"
}
variable "cidr_subnet" {
  description = "CIDR block for the subnet"
  default     = "10.1.0.0/24"
}

variable "environment_tag" {
  description = "Environment tag"
  default     = "Learn"
}

variable "region"{
  description = "The region Terraform deploys your instance"
  default     = "us-east-1"
}


인프라 코드에 대한 설명은 생략하겠습니다.
인프라에 대한 테라폼 코드가 궁금하시다면 다른 게시글이나 제 포스팅을 읽어보세요!


AMI Query 하기

본문에는 소개되지 않았지만, Packer와 Terraform의 통합을 위해 필자가 작성한 부분입니다. Terraform의 Data Sources를 활용해 빌드된 AMI를 Query하여 샘플 앱을 프로비저닝 해봅시다. 선행 작업에서 진행한 main.tf 하단에 아래 코드를 추가합니다.

data "aws_ami_ids" "myami" {
  owners = ["YOUR Account ID"]
  # sort_ascending = true
  
  filter {
    name   = "name"
    values = ["learn-terraform-packer-*"]
  }
}

Packer로 이미지를 빌드했으므로, ami owner가 되었습니다! owners 부분에 자신의 계정 ID를 작성하고 filter에 packer build를 할 떄 사용한 ami_name을 value 값으로 넣습니다.

sort_ascending default 값이 false

sort_ascending = false : list의 0번째 요소가 latest
sort_ascending = true : 만들어진 순서대로 리스트 생성 (가장 먼저 생성된 ami가 0번)

이어서 main.tf에 인스턴스 리소스를 정의한 부분의 ami = "ami-YOUR-AMI-ID" 코드를 아래와 같이 대체합니다. data 객체에 필터링한 결과 값들이 빌드된 AMI들이 리스트 형식으로 들어가는데 latest 버전을 사용하기 위해 0번째 이미지를 명시합니다. 편의상 사용된 ami 번호를 터미널에서 확인 할 수 있도록 output에 대한 코드도 함께 작성합니다.

resource "aws_instance" "web" {
  ami = data.aws_ami_ids.myami.ids[0]   # "ami-YOUR-AMI-ID"
  # Skip Other Config
}

output "my_ami" {
  value = aws_instance.web.ami
}

코드를 작성하고 테라폼 코드가 위치한 폴더에서(instances 폴더 하위) 다음 명령어를 실행합니다. terraform init && terraform apply 인스턴스를 생성하기 위해 yes를 기입합니다.

명령어로 인프라 상태를 점검해 아래와 같다면 다음 단계로 🚀

$ terraform state list
data.aws_ami_ids.myami
aws_instance.web
aws_internet_gateway.igw
aws_route_table.rtb_public
aws_route_table_association.rta_subnet_public
aws_security_group.sg_22_80
aws_subnet.subnet_public
aws_vpc.vpc

data object에 담긴 정보가 확인하고 싶다면?
$ terraform state show data.aws_ami_ids.myami.ids


인스턴스 확인하기

SSH를 통해 인스턴스에 연결합니다.
ssh terraform@$(terraform output -raw public_ip) -i ../tf-packer

Go 디렉토리로 이동하세요.
cd go/src/github.com/hashicorp/learn-go-webapp-demo

데모 앱을 실행합니다.
go run webapp.go

배포한 앱 확인을 위해 terraform output public_ip로 얻은 IP에 8080 포트로 접속하면 간단한 테트리스 게임 앱을 확인할 수 있습니다.

인스턴스 리소스 회수

terraform destroy 명령어로 상기 프로젝트에서 사용한 인프라를 리소스를 회수합니다. Packer로 작성한 이미지는 파괴되지 않습니다.


위와 같은 단계들을 통해 패커로 이미지를 만들고 테라폼과 통합하는 방법을 학습해보았습니다. 이번 포스팅에서 다뤘던 내용은 Immutable Servers를 유지하기 위한 방법 중 하나입니다. 오늘 포스팅에 추가로 Ansible을 통합한다면, Immutable Infrastructure를 구축할 수도 있습니다.


Terraform Courses

-->