[AWS] Packer로 AMI 말아올리기

Packer는 하시코프에서 제공하는 패킹 도구다.
대표적인 사용례 중 하나는 EC2 AMI를 빌드하는 것이다.

이 AMI란 것이 EC2 리소스 자체에 대한 스냅샷이라고 볼 수 있고, Docker Image와도 비슷한 용도로 쓰는데, 정작 빌드 시스템을 직접 제공하지 않아서 CI/CD에서 이걸 다루기가 별로 쉽지는 않다.
로컬 빌드가 되는 Docker와 다르게 실제 AWS에 결과물을 올린 다음에 스냅샷을 떠야하는거라서 또 이래저래 빌드를 구성하기도 비효율적인 편이다.

그나마 그런 작업을 좀 자동화할 수 있게 해주는게 Packer라는 도구다.
유의할 점은, 이런 도구 쓴다고 해서 저런 근본적인 한계가 사라지진 않는다.

packer의 동작 방식은 간단한 매크로 수준이다. AMI 기반으로 EC2를 띄워서, 접속한 다음에 이것저것 하고 AMI를 추출하는 것이다. 우리가 할 수 없는걸 하게 해주는건 아니다.




AMI 베이스 이미지 준비

일단 빌드에 사용할 베이스 이미지가 하나는 있어야 한다.

packer에서는 기본 이미지 사용 기능을 제공하지 않으니까, 대충 우분투 실행한 다음에 이미지 바로 떠서 쓰기만 하면 된다.

여기서는 Github Action 기반으로 Packer 워크플로를 구성하는 방법을 간단히만 다뤄본다.




Packer 설치

우분투 환경의 경우에는 다음과 같이 설치할 수 있다.

      - name: Setup `packer`
        run: |
          curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
          sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
          sudo apt-get update && sudo apt-get install packer

이게 모든 유닉스에 공통되는 설치 스크립트 그런건 없더라. 리눅스 배포판마다 또 다르다.




packer 파일 작성

이게 Dockerfile과 비슷한 패킹 파일이라고 보면 된다. 이걸 정의해서 packer에 밀어넣으면, 거기에 맞춰서 이미지를 빌드해준다.

내 경우에는 Dockerfile을 최대한 비슷하게 베껴보려고 이것저것 넣은게 있어서 좀 복잡하다.

variable "ami_name" {
  type = string
}

packer {
  required_plugins {
    amazon = {
      version = ">= 1.2.8"
      source  = "github.com/hashicorp/amazon"
    }
  }
}

source "amazon-ebs" "ubuntu" {
  ami_name      = var.ami_name
  instance_type = "t2.micro"
  region        = "us-east-1"
  source_ami_filter {
    filters = {
      name                = "ubuntu-base"
      root-device-type    = "ebs"
      virtualization-type = "hvm"
    }
    most_recent = true
    owners      = ["..."]
  }
  ssh_username = "ubuntu"
}

build {
  name    = "packer"
  sources = [
    "source.amazon-ebs.ubuntu"
  ]

  provisioner "shell" {
    inline = [
      "mkdir /home/ubuntu/server"
    ]
  }

  provisioner "file" {
    source = "./"
    destination = "/home/ubuntu/server"
  }

  provisioner "file" {
    source = "server.service"
    destination = "/etc/systemd/system/server.service"
  }

  provisioner "shell" {
    inline = [
      "cd /home/ubuntu/server", 
      "sudo docker build -t server:latest .", 
      "sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT",
      "sudo systemctl enable server",
      "sudo systemctl start server"
    ]
  }
}

이건 변수다.

출력할 ami 이미지 이름은 외부에서 제어할 수 있게 해뒀다.

소스는 빌드할때 사용할 기본 이미지다.
FROM 절이라고 보면 된다.

필터는 귀찮더라도 보안상 넣게 해둬서 넣어야 한다.

packer는 이 영역을 바탕으로 ami 이미지를 받아서 빌드용 EC2를 실행하고, ssh로 들어가는 것까지를 처리한다.

그 다음이 이제 빌드 프로세스다.

여기에서는 provisioner 단위를 통해서 빌드 프로세스 중간에 할 일들을 구겨넣을 수 있다.
shell은 셸 명령을 집어넣는 부분이고, file이 파일을 복사-붙여넣기 하는 부분이다.

shell은 실행된 시점의 유저의 권한을 따른다. sudo 권한이 있는 유저면 sudo 명령 집어넣어서 다 시킬 수 있으니 대체로 문제는 없다.

file 복사는 현재 디렉터리 기준으로 파일/디렉터리 복사가 다 된다.

위에는 현재 디렉터리에 있는 파일들을 전부 /home/ubuntu/server로 복붙하는거고, 아래는 특정 파일만 옮기는 거다.
여기서 유의할 점은 이것도 실행한 시점의 user 권한을 따르는데, shell과는 다르게 자유 입력 명령이 아니라서 sudo 처리가 불가능하다는 것이다.
만약 저 특정 파일 경로에 대해서만 열어줘야 한다거나 하면 미리 권한을 붙여줘야 한다.

그리고 이건 Docker와 다르게 CMD나 ENTRYPOINT 같은 단위가 없다.
그래서 만약 시작시에 동작하는 뭔가가 필요하다면

그냥 systemctl 같은 데몬을 쓰는 편이 간단하다.




빌드 및 업로드

init과 validate는 유효성 정도만 검증하는 단순한 기능들이다.
아까 정의한 packer 파일을 전달하고, 빈 구멍이 있으면 그거만 채워서 실행하면 된다.

이 경우에는 ami_name이 variable로 뚫려있었으니 그걸 메웠다.

실제 build->업로드까지 진행하려면 build 명령을 쓰면 된다.
업로드 명령이 따로 있다거나 하진 않고 불가피하게 일체형이다.

AWS 접속정보를 환경변수로 넘겨야 한다는 것만 빼면 init, validate와 큰 차이는 없다. 변수만 채워서 실행했다.

내 경우에는 이렇게 해서 실행했을때

빌드 자체에만 4분 넘는 시간이 걸렸다.
사실 이건 AMI 빌드 자체도 그렇지만, AMI 업로드 후에 사용가능 상태가 되기까지 기다리는 것도 조금 걸린다.

  1. EC2 프로비저닝

  2. 접속해서 이것저것 처리

  3. EC2 기반으로 AMI 업로드

  4. 업로드된 AMI가 준비되기까지 대기...



속터지는 프로세스다.
서버리스 컨테이너를 쓸 수 없는 상황에서 다들 그냥 쿠버로 가는데는 이유가 있는 것 같다.



여기까지의 Github Action 워크플로 예시다.

name: ".Service Server"

on:
  workflow_dispatch:
    inputs:
      environment:
        required: true
        type: choice
        description: "배포 환경 ⚠️ prod 배포 시 tag 사용"
        default: "dev"
        options:
          - "prod"
      ami_version_name:
        required: false
        type: string
        description: "AMI 버전명"
        default: "1234"

env:
  ENVIRONMENT: ${{ inputs.environment }}
  AWS_REGION: us-east-1
  IMAGE_TAG: ${{ github.sha }}
  AMI_NAME: foo-${{ inputs.ami_version_name }}

run-name: Service ${{ inputs.environment }}

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2

      - name: Setup `packer`
        run: |
          curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add -
          sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
          sudo apt-get update && sudo apt-get install packer

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: packer init and validate
        run: |
          packer init -var "ami_name=${{ env.AMI_NAME }}" packer.pkr.hcl
          packer validate -var "ami_name=${{ env.AMI_NAME }}" packer.pkr.hcl

      - name: Build AMI Image
        id: ami
        run: |
          export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY }}
          export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_KEY }}
          packer build -var "ami_name=${{ env.AMI_NAME }}" packer.pkr.hcl


참조
https://developer.hashicorp.com/packer/tutorials/cloud-production/github-actions
https://techblog.woowahan.com/2624/