どうも、ちゃだいん(@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
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)でした。