Packer + Terraform でイメージを管理する

PackerTerraform と同じく HashiCorp が提供する、マシンイメージ (EC2 AMI, GCE Machine Image など) を構成管理するツールです。実用上は Terraform の構成のなかで Packer で作成されたマシンイメージを data で参照することが多いです。これを発展させて、terraform apply しただけで (Packer による) イメージの作成まで実行されるとより体験がよいですが、Packer と Terraform は直接のインテグレーションがありません。

これは terraform_data を利用して Terraform から packer コマンドを実行することで実現可能です。

# Google Cloud Compute Engine machine image の例

# Packer によるマシンイメージの作成
# data/my_image.pkr.hcl が存在すると仮定する
resource "terraform_data" "packer_my_image" {
  triggers_replace = {
    image_name = "my-image-${substr(filesha256("${path.module}/data/my_image.pkr.hcl"), 0, 8)}"
    project_id = var.project_id
    region     = var.region
    zone       = var.zone
  }

  provisioner "local-exec" {
    when        = create
    working_dir = "${path.module}/data"
    command     = <<-EOT
    packer init my_image.pkr.hcl
    packer build \
      -var 'image_name=${self.triggers_replace.image_name}' \
      -var 'project_id=${self.triggers_replace.project_id}' \
      -var 'region=${self.triggers_replace.region}' \
      -var 'zone=${self.triggers_replace.zone}' \
      my_image.pkr.hcl
    EOT
  }
  provisioner "local-exec" {
    when    = destroy
    command = <<-EOT
    gcloud compute images delete \
      --project ${self.triggers_replace.project_id} \
      --quiet \
      ${self.triggers_replace.image_name}
    EOT
  }
}

# 作成されたイメージの参照
data "google_compute_image" "my_image" {
  name    = terraform_data.packer_my_image.triggers_replace.image_name
  project = terraform_data.packer_my_image.triggers_replace.project_id
}

この実装では Packer ソースファイルの編集 (hash の変化) をトリガーにイメージを作成しなおします。Destroy provisioner により旧イメージは replace/destroy タイミングで削除されます。

注意点として destroy provisioner は configuration が残っている状態でしか実行されないという制限があります。すなわち、構成を削除することを意図して terraform_data リソースの記述を削除またはコメントアウトして apply しても destroy provisioner は実行されません。これは通常のリソースと挙動が異なるので改善を期待したいところです。

上記は GCE の例ですが、AWS でも同様に構成することができるでしょう。

resource "terraform_data" "packer_my_ami" {
  triggers_replace = {
    ami_name = "my-ami-${substr(filesha256("${path.module}/data/my_ami.pkr.hcl"), 0, 8)}"
    profile  = var.aws_profile
    region   = var.aws_region
  }

  provisioner "local-exec" {
    when        = create
    ...
  }
  provisioner "local-exec" {
    when    = destroy
    command = <<-EOT
    IMAGE_ID=$(aws ec2 describe-images \
      --profile ${self.triggers_replace.profile} \
      --region ${self.triggers_replace.region} \
      --filters 'Name=name,Values=${self.triggers_replace.ami_name}' \
      --query 'Images[0].ImageId' \
      --output text)
    aws ec2 deregister-image \
      --profile ${self.triggers_replace.profile} \
      --region ${self.triggers_replace.region} \
      --image-id "$IMAGE_ID" \
      --delete-associated-snapshots
    EOT
  }
}

data "aws_ami" "my_ami" {
  owners = ["self"]
  region = terraform_data.packer_my_ami.triggers_replace.region

  filter {
    name   = "name"
    values = [terraform_data.packer_my_ami.triggers_replace.ami_name]
  }
}

このようにすることで、例えば GitHub Actions を利用した Terraform のための CI/CD ワークフローに Packer をシームレスに統合可能です。

name: Terraform Apply for Google Cloud

on:
  push:
    branches:
      - main
    paths:
      - "**.tf"
      - "**.pkr.hcl" # Packer ソースファイルの変更でもトリガーする

jobs:
  apply:
    permissions:
      contents: read
      id-token: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: hashicorp/setup-terraform@v3
      - uses: hashicorp/setup-packer@v3 # Packer のインストール
      - uses: google-github-actions/auth@v3
      - uses: google-github-actions/setup-gcloud@v3 # Packer で gcloud コマンドを利用しているためインストールする
      # このステップで Packer によるイメージの作成 + IaC 反映がすべて行われる
      - run: terraform apply -auto-approve