![[Terraform] EC2 Image Builderでゴールデンイメージを配布し、別AWSアカウントの起動テンプレートに登録する](https://devio2023-media.developers.io/wp-content/uploads/2022/08/ec2-image-builder.png)
[Terraform] EC2 Image Builderでゴールデンイメージを配布し、別AWSアカウントの起動テンプレートに登録する
この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
どうも、ちゃだいん(@chazuke4649)です。
ゴールデンイメージ管理やってますか?
ゴールデンイメージ管理の課題として、組織が大きくなりAWSアカウントが増えると、ゴールデンイメージ管理は煩雑になっていきます。
EC2 Image Builder は、OSイメージであるAMIのビルドから配布まで集中管理・自動化を実現してくれるサービスです。AMIの配布については、RAMによりOrganizations連携ができるため、マルチアカウント環境下におけるAMI管理を1つのアカウントで集中管理・配布することが可能です。
EC2 Image BuilderのアップデートでAMIの配布から拡張し、同じアカウントなら起動テンプレート自体の更新まで行ってくれるようになりました。ここまでやってくれると、あとは EC2 AutoScaling でインスタンス更新をすれば、latestバージョンの起動テンプレートを使用することによる、新しいAMIへの差し替えが容易に行えます。
ただし、本機能はまだマルチアカウントでは未サポートです。
今回は、ここの部分をTerraformによってもう少しやりやすくしてみようと思います。
具体的には、配布先のアカウントで terraform plan すれば、新しく配布されたAMI IDを検知し、起動テンプレートのバージョン更新を change として上げてくれる状態にすることです。
やってみる
前提
- AWS Organizations環境下に2つのアカウントが存在する
- 管理アカウント 000000000000
- Sharedアカウント 111122223333: AMI配布元
- Workloadアカウント 444455556666: AMI配布先
手順
- Image Builderを構築する[Sharedアカウント作業]
- 起動テンプレートを構築する[Workloadアカウント作業]
- Image Builderにより新しいAMIを作成・配布する[Sharedアカウント作業]
- 起動テンプレートを更新する[Workloadアカウント作業]
1. Image Builderを構築する[Sharedアカウント作業]
% tree . ├── backend.tf ├── files │ └── build.yml ├── iam-role.tf ├── image-builder.tf └── sg.tf
メインとなるimage-builder.tfは以下の通りです。
image-builder.tf
data "aws_partition" "current" {}
data "aws_region" "current" {}
## Image Pipeline
resource "aws_imagebuilder_image_pipeline" "sample" {
  name                             = "sample"
  image_recipe_arn                 = aws_imagebuilder_image_recipe.sample.arn
  infrastructure_configuration_arn = aws_imagebuilder_infrastructure_configuration.sample.arn
  distribution_configuration_arn   = aws_imagebuilder_distribution_configuration.sample.arn
}
## Image recipe
resource "aws_imagebuilder_image_recipe" "sample" {
  name         = "sample"
  parent_image = "arn:${data.aws_partition.current.partition}:imagebuilder:${data.aws_region.current.name}:aws:image/amazon-linux-2-x86/x.x.x"
  version      = "1.0.0"
  block_device_mapping {
    device_name = "/dev/xvdb"
    ebs {
      delete_on_termination = true
      volume_size           = 100
      volume_type           = "gp3"
    }
  }
  component {
    component_arn = aws_imagebuilder_component.build.arn
  }
}
## Builder component "Build"
resource "aws_imagebuilder_component" "build" {
  name     = "build"
  platform = "Linux"
  version  = "1.0.0"
  data = file(
    "./files/build.yml"
  )
}
## Infrastructure configration
resource "aws_imagebuilder_infrastructure_configuration" "sample" {
  name                          = "sample"
  description                   = "this is sample"
  instance_profile_name         = aws_iam_instance_profile.image_builder.name
  instance_types                = ["t3.small"]
  security_group_ids            = [aws_security_group.test_sg.id]
  subnet_id                     = "subnet-xxxxxxxxxxxxxxxxxxxx"
  terminate_instance_on_failure = true
}
## Distribution configration
resource "aws_imagebuilder_distribution_configuration" "sample" {
  name = "sample"
  distribution {
    ami_distribution_configuration {
      name = "sample_v{{imagebuilder:buildVersion}}_{{imagebuilder:buildDate}}"
      ami_tags = {
        ImageBuilder = true
      }
      launch_permission {
        organization_arns = ["arn:aws:organizations::000000000000:organization/o-xxxxxxxxxx"]
      }
    }
    region = data.aws_region.current.id
  }
}
いくつか補足します。
Builder component "Build"
Builder componentは、YAMLファイルを外出しています。
## Builder component "Build"
resource "aws_imagebuilder_component" "build" {
  name     = "build"
  platform = "Linux"
  version  = "1.0.0"
  data = file(
    "./files/build.yml"
  )
}
build.ymlはこちらから拝借し「jq」コマンドを Amazon Linux2のリポジトリ、 新しいLinuxカーネルを拡張リポジトリからインストールしています。
name: yum_install
description: jq_install
schemaVersion: 1.0
phases:
  - name: build
    steps:
      - name: UpdateOS
        action: UpdateOS
      - name: yum_update
        action: ExecuteBash
        inputs:
          commands:
            - yum update -y
      - name: jq_install
        action: ExecuteBash
        inputs:
          commands:
            - yum install jq -y
      - name: kernel_ng_install
        action: ExecuteBash
        inputs:
          commands:
            - amazon-linux-extras install -y kernel-ng
  - name: validate
    steps:
      - name: jq_install
        action: ExecuteBash
        inputs:
          commands:
            - rpm -qi jq
Infrastructure configration
インフラ設定では、配布するAMIの元となるEC2を実際に起動するVPCサブネットやセキュリティグループを指定します。今回サブネットはパブリックサブネットにし、後述するセキュリティグループでアウトバウンド通信のみ許可しています。(VPCサブネットはIDを直接指定しているため、コードを利用する際は適宜置き換えが必要です)
## Infrastructure configration
resource "aws_imagebuilder_infrastructure_configuration" "sample" {
  name                          = "sample"
  description                   = "this is sample"
  instance_profile_name         = aws_iam_instance_profile.image_builder.name
  instance_types                = ["t3.small"]
  security_group_ids            = [aws_security_group.test_sg.id]
  subnet_id                     = "subnet-xxxxxxxxxxxxxxxxxxxx"
  terminate_instance_on_failure = true
}
Distribution configration
配布設定では、配布するAMI名(※Nameタグではない)を指定できます。以下サンプルの場合例えば sample_v4_2022-08-10T09-01-50.490Z のように指定できます。
 {{imagebuilder:buildVersion}} は同じイメージ名のビルドバージョンであり、Image recipeで指定するバージョンとは異なる点に注意です。
また、Launch permissionにて共有先組織を指定しており、今回は組織全体へ共有しています。
## Distribution configration
resource "aws_imagebuilder_distribution_configuration" "sample" {
  name = "sample"
  distribution {
    ami_distribution_configuration {
      name = "sample_v{{imagebuilder:buildVersion}}_{{imagebuilder:buildDate}}"
      ami_tags = {
        ImageBuilder = true
      }
      launch_permission {
        organization_arns = ["arn:aws:organizations::000000000000:organization/o-xxxxxxxxxx"]
      }
    }
    region = data.aws_region.current.id
  }
}
iam-role.tf
IAMロールは以下を参考に作成します。
Prerequisites - EC2 Image Builder
resource "aws_iam_instance_profile" "image_builder" {
  name = "image_builder"
  role = aws_iam_role.image_builder.name
}
resource "aws_iam_role" "image_builder" {
  name = "ImageBuilderRole"
  path = "/"
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilder",
    "arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilderECRContainerBuilds",
    "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  ]
  assume_role_policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "sts:AssumeRole",
            "Principal": {
               "Service": "ec2.amazonaws.com"
            },
            "Effect": "Allow",
            "Sid": ""
        }
    ]
}
EOF
}
sg.tf
セキュリティグループは、以下ブログと同じパターンで、アウトバウンド通信のみ許可します。
Terraformのfor_eachとnullで、効率的にAWSのセキュリティグループを定義する | DevelopersIO
## Security Groups
locals {
  test_sg = {
    ## [ type, from_port, to_port, protocol, sg-id, cidr_blocks, description ]
    "rule_3" = ["egress", 0, 0, "-1", null, ["0.0.0.0/0"], "Allow any outbound traffic"]
  }
}
resource "aws_security_group" "test_sg" {
  vpc_id      = module.vpc.vpc_id
  description = "For test"
  name        = "test-sg"
  tags        = { Name = "test-sg" }
}
resource "aws_security_group_rule" "test" {
  security_group_id        = aws_security_group.test_sg.id
  for_each                 = local.test_sg
  type                     = each.value[0]
  from_port                = each.value[1]
  to_port                  = each.value[2]
  protocol                 = each.value[3]
  source_security_group_id = each.value[4]
  cidr_blocks              = each.value[5]
  description              = each.value[6]
}
これらを terraform plan し、問題なければ terraform apply すれば完了です。
2. 起動テンプレートを構築する[Workloadアカウント作業]
起動テンプレートは以下 launch-template.tfに記述します。
## AMI
data "aws_ami" "sample" {
  owners = ["111122223333"] 
  most_recent = true 
  filter {
    name   = "name"
    values = ["sample*"]
  }
}
## Launch template
resource "aws_launch_template" "web" {
  name_prefix   = "sample-lt-"
  image_id      = data.aws_ami.sample.id
  instance_type = "t3.small"
  ebs_optimized = true
  block_device_mappings {
    device_name = "/dev/xvda"
    ebs {
      volume_size           = "50"
      volume_type           = "gp3"
      delete_on_termination = true
    }
  }
  update_default_version = true
  vpc_security_group_ids = [aws_security_group.test_sg.id]
  }
  lifecycle {
    create_before_destroy = true
    ignore_changes = [
      default_version,
      latest_version,
      block_device_mappings,
    ]
  }
}
## Security Groups
locals {
  test_sg = {
    ## [ type, from_port, to_port, protocol, sg-id, cidr_blocks, description ]
    "rule_3" = ["egress", 0, 0, "-1", null, ["0.0.0.0/0"], "Allow any outbound traffic"]
  }
}
resource "aws_security_group" "test_sg" {
  vpc_id      = module.vpc.vpc_id
  description = "For test"
  name        = "test-sg"
  tags        = { Name = "test-sg" }
}
resource "aws_security_group_rule" "test" {
  security_group_id        = aws_security_group.test_sg.id
  for_each                 = local.test_sg
  type                     = each.value[0]
  from_port                = each.value[1]
  to_port                  = each.value[2]
  protocol                 = each.value[3]
  source_security_group_id = each.value[4]
  cidr_blocks              = each.value[5]
  description              = each.value[6]
}
output "ami_id" {
  value = data.aws_ami.sample.id
}
output "ami_name" {
  value = data.aws_ami.sample.name
}
補足
- AMIのデータソースが肝となりますが、4行目にて、Image BuilderによるAMI配布元AWSアカウントで絞ります。
- 5行目の、most_recent=trueによって、複数の結果が返された場合は、最新のAMIを使用することができます。
- さらに、8、9行目のフィルターにてAMI名で絞ります。
- セキュリティグループは一旦先ほどと全く同じにしてます。
これらを terraform plan し、問題なければ terraform apply すれば完了です。
3. Image Builderにより新しいAMIを作成・配布する[Sharedアカウント作業]
マネジメントコンソールにて、作成したImage Builderイメージパイプラインを実行してイメージを作成します。
補足として、今回構成ではAMIのビルド自体はTerraformで行っていません。AMIのビルドは内容によって例えば30分など処理完了まで時間がかかり、かつ、コンソール画面の方が進捗状況も確認できるため、今回はコンソール作業としています。

実行すると、作成が開始されたイメージのステータスで進行していることが確認できます。
完了するとステータスは以下のように「使用可能」になりました。

(事前にいくつか作ってましたが)直近のイメージは sample|1.0.0/5 となっています。この場合、イメージレシピのバージョンが 1.0.0、それによるビルドバージョンが 5 となります。
作成されたイメージのIDは ami-084c4299... 、 AMI名は sample-v5_2022_08_14... であることが確認できます。

ディストリビューション設定にて、該当の組織IDが共有アクセスできる状態が確認できます。

4. 起動テンプレートを更新する[Workloadアカウント作業]
それでは次に、共有されたWorkloadアカウント側を見てみます。
現時点で前準備で、既に一度共有されたAMIの1つ前のビルドバージョン sample-v4... を起動テンプレートに追加済みで、起動テンプレート済みであることが以下確認できます。

この状態で、 terraform plan するとどうなるか見てみます。
terraform plan を実行すると、以下の通り、AMIを手順3にて作成した新しいAMI ID ami-084c4299... に置き換えようとする挙動を確認することができました。
% terraform plan
~~~中略~~~
Terraform will perform the following actions:
  # aws_launch_template.web will be updated in-place
  ~ resource "aws_launch_template" "web" {
        id                      = "lt-0db85135510292ead"
      ~ image_id                = "ami-009eb3b9fa9967c9d" -> "ami-084c429949f8663b0"
        name                    = "sample-lt-20220810084742373400000001"
        tags                    = {}
        # (11 unchanged attributes hidden)
        # (2 unchanged blocks hidden)
    }
Plan: 0 to add, 1 to change, 0 to destroy.
Changes to Outputs:
  ~ ami_id   = "ami-009eb3b9fa9967c9d" -> "ami-084c429949f8663b0"
  ~ ami_name = "sample_v4_2022-08-10T09-01-50.490Z" -> "sample_v5_2022-08-24T04-13-00.262Z"
これで terraform apply すれば、起動テンプレートのAMIが最新のものに置き換えることできます。
これによって、Workloadsアカウント側のコードを変更せずに、新しいAMIを検知し差し替えることが可能であることが確認できました。
検証は以上です。
終わりに
EC2 Image BuilderをSharedアカウントにてビルド・配布し、配布されたWorkloadアカウントにて terraform plan/apply により起動テンプレートを自動更新することができました。マルチアカウント環境のAMI管理の効率化には、EC2 Image Builderが役立ちそうです。一度お試しあれ。
それでは今日はこの辺で。ちゃだいん(@chazuke4649)でした。












