[Terraform] EC2 Image Builderでゴールデンイメージを配布し、別AWSアカウントの起動テンプレートに登録する

マルチアカウント環境のAMI運用を集中管理できるよ
2022.08.24

この記事は公開されてから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配布先

手順

  1. Image Builderを構築する[Sharedアカウント作業]
  2. 起動テンプレートを構築する[Workloadアカウント作業]
  3. Image Builderにより新しいAMIを作成・配布する[Sharedアカウント作業]
  4. 起動テンプレートを更新する[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

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カーネルを拡張リポジトリからインストールしています。

files/build.yml

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

iam-role.tf

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

sg.tf

## 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に記述します。

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)でした。