tools

packer - AMI 생성

이 글은 다음 4가지 요구사항을 모두 만족하는 하나의 Packer 템플릿을 예제로 만들어 봅시다.

  1. 플러그인 명시: packer { required_plugins { } }
  2. 여러 변수 정의locals에서 가공
  3. AWS ECR/Docker Hub 이미지 사전 Pull → 콜드 스타트 단축
  4. 고급 태깅/권한 설정 → AMI 태그, 실행 태그, 계정/조직 공유

대상 OS는 Amazon Linux 2 기반 AMI이며, 빌드 중 Docker를 설치하고 ECR + Docker Hub 이미지를 미리 받아 이미지 캐시를 AMI에 넣어봅시다.

예제 템플릿

packer
packer {
  required_plugins {
    amazon = {
      source  = "github.com/hashicorp/amazon"
      version = ">= 0.0.2"
    }
  }
}
 
variable "region"                 { default = "ap-northeast-2" }
variable "ami_name_prefix"        { default = "app-node" }
variable "instance_type"          { default = "m6i.large" }
variable "iam_instance_profile"   { default = "ec2_role_with_ecr_ssm" }
variable "share_with_accounts"    { type = list(string), default = ["111111111111"] }
variable "org_arns"               { type = list(string), default = [] } # ex) ["arn:aws:organizations::123456789012:organization/o-abcdefghi"]
 
variable "ecr_account_id"         { default = "123456789012" }
variable "ecr_repo"               { default = "myapp/backend" }
variable "ecr_tag"                { default = "latest" }
variable "dockerhub_images"       { type = list(string), default = ["nginx:1.25-alpine", "busybox:1.36"] }
 
variable "owner"                  { default = "platform" }
variable "environment"            { default = "STG" }
variable "service"                { default = "web" }
variable "description"            { default = "AL2 AMI pre-pulls ECR & Docker Hub images for faster cold start" }
 
locals {
  name            = "${var.ami_name_prefix}-${timestamp()}"
  ecr_registry    = "${var.ecr_account_id}.dkr.ecr.${var.region}.amazonaws.com"
  ecr_image       = "${local.ecr_registry}/${var.ecr_repo}:${var.ecr_tag}"
  dockerhub_csv   = join(" ", var.dockerhub_images)
 
  # 공통 태그(AMI/런인스턴스)
  tags = {
    Name          = local.name
    Owner         = var.owner
    Environment   = var.environment
    Service       = var.service
    Description   = var.description
    ProvisionedBy = "Packer"
  }
}
 
source "amazon-ebs" "al2" {
  region                 = var.region
  ami_name               = local.name
  instance_type          = var.instance_type
  ssh_username           = "ec2-user"
  iam_instance_profile   = var.iam_instance_profile
 
  # Amazon Linux 2 최신 AMI 필터
  source_ami_filter {
    filters = {
      name                = "amzn2-ami-hvm-*-x86_64-gp2"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    owners      = ["137112412989"] # Amazon EKS 공식 계정
    most_recent = true
  }
 
  ami_description        = var.description
  tags                   = local.tags        # 최종 AMI 태그
  run_tags               = local.tags        # 빌드 시 생성되는 리소스(EC2 등) 태그
 
  ami_users              = var.share_with_accounts  # AMI를 특정 계정에 공유
  ami_org_arns           = var.org_arns             # 또는 AWS Organization 전체 공유
 
  launch_block_device_mappings {
    device_name            = "/dev/xvda"
    volume_type            = "gp3"
    volume_size            = 30
    throughput             = 125
    delete_on_termination  = true
  }
 
  aws_polling {
    delay_seconds = 20
    max_attempts  = 200
  }
}
 
build {
  name    = "al2-ecr-prepull"
  sources = ["source.amazon-ebs.al2"]
 
  provisioner "shell" {
    environment_vars = [
      "REGION=${var.region}",
      "ECR_REGISTRY=${local.ecr_registry}",
      "ECR_IMAGE=${local.ecr_image}",
      "DOCKERHUB_IMAGES=${local.dockerhub_csv}"
    ]
    inline = [
      "set -euo pipefail",
      # Docker 설치 및 시작 (Amazon Linux 2)
      "sudo amazon-linux-extras install -y docker || sudo yum install -y docker",
      "sudo systemctl enable docker",
      "sudo systemctl start docker",
 
      # ECR 로그인
      "aws ecr get-login-password --region \"$REGION\" | sudo docker login --username AWS --password-stdin \"$ECR_REGISTRY\"",
 
      # 이미지 프리풀(ECR + Docker Hub)
      "sudo docker pull \"$ECR_IMAGE\"",
      "for img in $DOCKERHUB_IMAGES; do sudo docker pull \"$img\"; done",
 
      # 필요 시 부팅 시 자동시작 비활성화(환경에 맞게 결정)
      "sudo systemctl disable docker || true"
    ]
  }
 
  post-processor "manifest" {
    output = "manifest.json"
    strip_path = true
    custom_data = {
      ecr_image        = "${local.ecr_image}"
      dockerhub_images = "${local.dockerhub_csv}"
      environment      = "${var.environment}"
    }
  }
}

HCL 파일 설명

1. 플러그인 명시: packer { required_plugins { } }

Packer는 VM 이미지를 빌드할 때 "플러그인(빌더, 프로비저너 등)"을 사용합니다. 여기서는 Amazon 빌더(AMI 만드는 플러그인)를 쓰겠다고 선언한 것을 확인 할 수 있습니다. version 조건을 달아두면, Packer가 packer init 할 때 정확한 버전을 받아옵니다. 따라서 사용하는 개발자나 환경에 상관 없이 동일한 동작을 수행합니다. 플러그인 명시를 통해 환경 의존성을 최소화 할 수 있습니다.


2. 변수 → locals 가공

  • 변수(variable)로 외부에서 바꿀 값(리전, 이미지 태그, 공유 계정 등)을 선언합니다.
  • locals에서 파생 값(예: ecr_registry, ecr_image, 공통 tags)을 한 번만 계산해 재사용합니다.
  • 이 패턴을 쓰면 템플릿이 짧아지고 실수 가능성이 줄어듭니다.

3. AWS ECR + Docker Hub 사전 Pull

인스턴스가 처음 부팅될 때 이미지를 당기면 수십 초에서 수분이 걸릴 수 있습니다. AMI 안에 로컬 캐시로 이미지를 갖고 있으면 docker run 시 바로 레이어를 찾아 써서 기동이 훨씬 빨라집니다. 아래의 HCL 블록(provisioner "shell")은 **“AMI 빌드 과정에서 Docker와 컨테이너 이미지를 미리 설치·Pull해 두는 작업”**을 정의한 겁니다.

packer
  provisioner "shell" {
    environment_vars = [
      "REGION=${var.region}",
      "ECR_REGISTRY=${local.ecr_registry}",
      "ECR_IMAGE=${local.ecr_image}",
      "DOCKERHUB_IMAGES=${local.dockerhub_csv}"
    ]
    inline = [
      "set -euo pipefail",
      # Docker 설치 및 시작 (Amazon Linux 2)
      "sudo amazon-linux-extras install -y docker || sudo yum install -y docker",
      "sudo systemctl enable docker",
      "sudo systemctl start docker",
 
      # ECR 로그인
      "aws ecr get-login-password --region \"$REGION\" | sudo docker login --username AWS --password-stdin \"$ECR_REGISTRY\"",
 
      # 이미지 프리풀(ECR + Docker Hub)
      "sudo docker pull \"$ECR_IMAGE\"",
      "for img in $DOCKERHUB_IMAGES; do sudo docker pull \"$img\"; done",
 
      # 필요 시 부팅 시 자동시작 비활성화(환경에 맞게 결정)
      "sudo systemctl disable docker || true"
    ]
  }

3-1. 환경변수 주입

REGION, ECR_REGISTRY, ECR_IMAGE, DOCKERHUB_IMAGES 값을 Packer 변수/locals에서 받아 쉘 스크립트에 전달합니다.

3-2. Docker 설치 & 실행

Amazon Linux 2 기준으로 amazon-linux-extras install docker 또는 yum install docker통해 설치합니다. 그리고 systemctl enable/start docker로 도커 데몬을 켜줍니다.

3-3. ECR 로그인

aws ecr get-login-password로 얻은 토큰을 docker login에 전달 → 해당 레지스트리(ECR)에 접근 가능해짐.

3-4. 컨테이너 이미지 Pull

  • docker pull "$ECR_IMAGE" → 지정한 ECR 이미지 가져오기
  • for img in $DOCKERHUB_IMAGES; do docker pull "$img"; done → Docker Hub 이미지들을 반복해서 가져옵니다.

3-5. 필요시 Docker 자동시작 비활성화

  • systemctl disable docker → 빌드된 AMI가 부팅될 때 자동으로 도커가 켜지지 않도록 설정 (환경에 따라 유지/삭제 선택).

정리하면, 이 provisioner "shell" 블록은 Packer가 AMI를 빌드하는 중에 Docker를 설치하고, ECR/Docker Hub에서 필요한 이미지를 미리 가져와서 AMI 안에 캐시로 포함시키는 설정입니다. 그래서 최종적으로 이 AMI로 인스턴스를 띄우면 → 이미 이미지들이 로컬에 있어서 콜드 스타트 지연이 줄어든다는 효과가 있습니다.

주의할 부분은 IAM Instance Profile에 최소 AmazonEC2ContainerRegistryReadOnly 권한이 있어야 합니다. 또한, 프라이빗 Hub를 쓴다면 로그인 토큰/비밀값을 이미지에 남기지 않도록 주의하세요. 프리풀한 이미지를 바꾸면 AMI를 다시 빌드/배포해야 최신이 반영됩니다. 많이 프리풀할수록 AMI 스냅샷이 커져 EBS 스냅샷 비용이 늘 수 있습니다. 따라서 핵심 이미지 위주로 저장하도록 해야합니다.


4. 고급 태깅/권한 설정

4-1. tags / run_tags:

런타임(빌드 중) 리소스는 run_tags, 결과물(AMI)엔 tags를 사용합니다.

  • tags: 최종 결과물인 AMI(이미지) 에 붙는 태그.
  • run_tags: 빌드 중 잠깐 쓰였다가 사라지는 임시 리소스(EC2, 볼륨, 스냅샷 등) 에 붙는 태그.

4-2. AMI 공유:

여러 계정(DEV/STG/PRD)에서 같은 표준 AMI를 쓰게 하려면 ami_users, ami_org_arns로 권한을 부여합니다. 여러 환경(DEV/STG/PRD)·여러 계정에 표준 이미지를 안전하게 배포 할 수 있습니다.

  • ami_users: 특정 AWS 계정 ID 목록에 공유(계정 단위)
  • ami_org_arns: AWS Organizations 단위로 공유(조직/OU 단위)

4-3. 스토리지·성능( gp3 + throughput ):

AMI를 만들 때 사용할 루트 볼륨을 gp3 로 설정하고, iops 와 throughput 을 원하는 값으로 지정하면, 빌드(프로비저닝) 속도와 부팅 속도를 끌어올리면서 과하게 비싼 타입을 쓰지 않고 비용/성능 밸런스를 맞출 수 있습니다. gp3는 볼륨 크기와 독립적으로 IOPS/Throughput을 조절할 수 있다는 게 있습니다.


실행

1. HCL 파일 준비

*.pkr.hcl 확장자를 가진 파일(예: ami.pkr.hcl) 안에 source / build / provisioner 블록들을 작성합니다. 거기서 variable 블록으로 region, ecr_repo, dockerhub_images 같은 값들을 변수로 선언둡니다. 위에서 작성한 hcl 파일입니다.

2. 초기화 & 검증

# 1) 초기화 & 검증
packer init .
packer validate .
  • init: HCL 안에서 필요한 플러그인(예: amazon-ebs) 을 다운로드해서 준비하는 단계입니다.
  • validate: HCL 문법/변수/구조가 맞는지 확인하는 단계입니다.

3. 빌드 실행

# 2) 빌드
packer build \
  -var "region=ap-northeast-2" \
  -var "ecr_account_id=123456789012" \
  -var "ecr_repo=myapp/backend" \
  -var "ecr_tag=2025-08-01" \
  -var 'dockerhub_images=["nginx:1.25-alpine","curlimages/curl:8.9.0"]' \
  .
  • build: 실제로 HCL에 정의된 source(amazon-ebs) 를 실행해서 AMI 생성합니다.
  • -var 옵션: HCL 안에서 선언된 변수를 실행 시점에 구체적인 값으로 채워줍니다.
    • region → 만들 리전
    • ecr_repo, ecr_tag → 프리풀 할 ECR 이미지
    • dockerhub_images → Docker Hub에서 미리 당겨둘 이미지
  • 마지막 줄의 . 은 “현재 디렉토리의 HCL 파일들을 읽어라”라는 의미

Packer Build 명령어를 실행하면 아래의 순서로 작동하게 됩니다.

  1. EC2 인스턴스 생성 - 지정된 IAM Role / Subnet / Security Group 등을 적용하여, HCL에 정의된 amazon-ebs 소스를 기반으로 임시 빌드용 EC2를 띄웁니다.
  2. Provisioner 실행 - Docker 설치, docker pull (ECR + Docker Hub), 패키지 설치, 설정파일 배치 등등 **"프로젝트에서 정의한 커스터마이징 작업"**이 실제로 인스턴스 안에서 수행되어 프로젝트 빌드가 완료 됩니다.
  3. AMI 생성 - 프로비저너가 끝나면, 그 상태 그대로 EC2 인스턴스의 EBS 볼륨을 스냅샷하여 AMI로 등록합니다
  4. 임시 리소스 정리 - 빌드용 EC2, 볼륨, 스냅샷 같은 중간 리소스는 삭제되고, 최종적으로 AMI만 리전에 남습니다.

Jenkins 연동

Jenkins는 CI/CD 파이프라인 툴이고, packer init / packer validate / packer build는 로컬에서 사람이 치는 명령어일 뿐입니다. 따라서 Jenkinsfile 안에서는 sh 단계로 넣어서 자동화합니다.

pipeline {
  agent any
  stages {
    stage('Build AMI') {
      steps {
        sh '''
          packer init .
          packer validate .
          packer build \
            -var "region=ap-northeast-2" \
            -var "ecr_account_id=123456789012" \
            -var "ecr_repo=myapp/backend" \
            -var "ecr_tag=2025-08-01" \
            -var 'dockerhub_images=["nginx:1.25-alpine","curlimages/curl:8.9.0"]' \
            .
        '''
      }
    }
  }
}

정리

이번 글에서는 Packer로 Amazon Linux 2 기반의 AMI를 만들면서 플러그인 버전 고정, 변수와 locals 패턴을 통한 가독성 개선, ECR/Docker Hub 이미지 사전 Pull을 통한 콜드 스타트 단축, 태깅과 공유 권한 설정, 스토리지 성능 최적화까지 포함한 베스트 프랙티스를 다뤘습니다. 이렇게 빌드한 AMI는 단순한 스냅샷이 아니라 서비스 구동에 필요한 도커 및 컨테이너 이미지가 준비된 “완성형 베이스 이미지”로서, CI/CD 파이프라인(Jenkins 등)에 packer init → validate → build 단계를 포함시켜 팀 단위로 일관되고 재현 가능한 배포가 가능합니다. 결과적으로 Packer는 단순한 이미지 생성 도구를 넘어 표준화된 인프라 아티팩트를 만드는 핵심 도구이며, 이를 기반으로 ASG, ECS, EKS와 같은 실행 환경에 Immutable Infrastructure 패턴을 적용할 수 있습니다.

참고 자료