[TIL] 12. Terraform

terraform planの挙動
terraform planは-refresh-onlyオプションの有無で2種類がある。オプションの有無で異なる要素間の差分を表示することに注意する。
- terraform plan : AWS環境とterraformソースコード間の差分を表示する
- terraform plan -refresh-only : AWS環境とtfstateファイルの差分を表示する
同様に、terraform applyも-refresh-onlyオプションの有無の2種類がある。terraform planとは異なり、オプション無しのコマンドが-refresh-onlyオプション有りのコマンドに内包される。
- terraform apply : terraformソースコードで定義したリソースをAWS環境にデプロイする。その後、AWS環境の状態をtfstateファイルに更新する。
- terraform apply -refresh-only : AWS環境の状態をtfstateファイルに更新する。

優先順位としては、terraformソースコード > AWS環境 > tfstateファイルとなる

for_eachでlist型を受け入れるためにforループを使用する
Subnetを複数定義する際に以下のようにlist型を使用する構成を取ることが多い。for_each
が受け取れる型はset(string)型かmap型であるため、このような場合にはforループを使用してlist型をmap型に変換してfor_eachを利用できる形にする。
variable "network" {
default = {
cidr = "192.168.0.0/16"
public_subnets = [
{
az = "a"
cidr = "192.168.10.0/24"
},
{
az = "c"
cidr = "192.168.20.0/24"
}
]
}
}
resource "aws_subnet" "public_ingress" {
for_each = { for i, s in var.network.public_subnets : i => s }
vpc_id = aws_vpc.main.id
availability_zone = "${var.common.region}${each.value.az}"
cidr_block = each.value.cidr
tags = {
Name = "${var.common.env}-public-${each.key}"
}
}
上記forループの結果、以下のようなmap型のオブジェクトが作成され、これをfor_each
で回す。
for_each = {
0 = {
az = "a"
cidr = "192.168.10.0/24"
},
1 = {
az = "c"
cidr = "192.168.20.0/24"
}
}

上記for_each
の結果として得られるaws_subnet.public_ingress
は、以下のようなKey, Valueを持つmap型のオブジェクトである。
- Key :
resource
ブロックのfor_each
における Key - Value :
resource
ブロックで定義したVPC・CIDRなどの属性を持つSubnetオブジェクト
(公式リファレンスにおけるArgument、Attributeがオブジェクトの属性となる)
aws_subnet.public_ingress = {
0 = {
vpc_id = xxxxxxxxxx
availability_zone = "ap-northeast-1a"
cidr_block = "192.168.10.0/24"
},
1 = {
vpc_id = xxxxxxxxxx
availability_zone = "ap-northeast-1c"
cidr_block = "192.168.20.0/24"
}
}

output
もmap型オブジェクト!
各moduleのmain.tfで参照する各moduleのoutput (module.network) は、以下のようなKey, Valueを持つmap型のオブジェクトである。
- Key :
output
ブロックの名称 - Value :
output
ブロックのvalue
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_for_ingress_ids" {
value = values(aws_subnet.public_ingress)[*].id
}
output "private_subnet_for_management_ids" {
value = values(aws_subnet.private_management)[*].id
}
module.network = {
vpc_id = xxxxxxxxxx # aws_vpc.main.id
public_subnet_for_ingress_ids = [xxxxxxxxxx, xxxxxxxxxx] # values(aws_subnet.public_ingress)[*].id
private_subnet_for_management_ids = [xxxxxxxxxx, xxxxxxxxxx] # values(aws_subnet.private_management)[*].id
}

ECSタスク定義にてTerraformとCI/CDとの競合を避ける
ECSでは一般的にコンテナイメージをCICDにより更新する。これにより、terraformソースコードと実際の環境の間で、ECSサービスで指定されているタスク定義が異なるという事象が発生してしまう。
この状態でterraform apply
を実行すると、CI/CDで更新した最新のタスク定義(v1.1)を用いたECSサービスから、terraformで定義されている旧タスク定義(v1.0)を用いたECSサービスへ巻き戻そうとする挙動が発生してしまう。
これを回避するためにignore_changes
を使用することで、引数の変更検知を無視する必要がある。
# ECS(Elastic Container Service)のサービス作成
resource "aws_ecs_service" "with_alb" {
for_each = var.config.services_with_alb
name = each.value.service_name
launch_type = "FARGATE"
platform_version = "1.4.0"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.with_alb[each.key].arn
desired_count = each.value.service_num
health_check_grace_period_seconds = 30
enable_execute_command = true
force_new_deployment = true
propagate_tags = "TASK_DEFINITION"
load_balancer {
target_group_arn = each.value.alb_arn
container_name = each.value.task.container_definitions[0].name
container_port = each.value.security_group.ingress_rules[0].from_port
}
network_configuration {
subnets = var.network.private_subnet_ids
security_groups = [module.ecs_service_sg_with_alb[each.key].security_group_id]
assign_public_ip = false
}
# AWS環境とのtask_definitionにおける変更差分を検知したとしても無視する
lifecycle {
ignore_changes = [task_definition]
}
}
ignore_changes
は「Terraformソースコードの変更によるAWS環境の更新を無視する」という挙動であるため、その後に実行される「AWS環境の状態をtfstateファイルに更新する」という処理は通常通りにおこなわれる。
Terraformソースコードでv1.0のコンテナイメージを用いてコンテナを定義しており、CI/CDでv1.1のコンテナイメージを使用するように変更されたという状態でterraform applyを実行した場合には、AWS環境ではv1.1のコンテナイメージを使用したコンテナで変化はなく、tfstateファイルは実体に合わせてv1.1のコンテナイメージを使ったコンテナが存在するというように更新される。

provider設定で for_each は使えない
複数リージョンでのGuardDuty設定をfor_each
を用いて実装したかったが、そのためには下に示したaws.us-east-1
の部分をaws.each.key
とする必要がある。だが、この書き方をTerraformは認識してくれずエラーとなってしまう。このようにprovider設定でfor_each
を使用することはできない。
module "guardduty_spoke" {
source = "../../modules/guardduty/guardduty_spoke"
common = local.common
s3 = {
bucket_arn = module.guardduty_hub.bucket_for_guardduty_arn
kms_key_arn = module.guardduty_hub.kms_key_arn
}
providers = {
aws = aws.us-east-1
}
}

変数の定義はLocal Valuesを使用する
基本的に、変数の定義にはLocal Valuesを使用する。terraform.tfvarsで外部から値を代入する場合やmodules/***/variable.tfで値を待ち構える場合にVariableを用いる。

backendに変数 (Variables) は利用できない
tfstateファイルを管理するためのS3バケットを指定するbackend
ブロックではVariableは使用不可。
terraform {
backend "s3" {
bucket = "${var.bucket_name}"
key = "${var.key}"
region = "${var.region}"
}
}
別ファイルに設定内容を記載して、以下のようにterraform init
実行時に-backend-config
にて指定することで解決することができる。
$ terraform init -reconfigure -backend-config=dev.tfbackend

file
関数、templatefile
関数を用いて外部ファイルを読み込む
templatefile
関数を使用すれば、引数を渡す動的参照も可能であるため便利
# Define EC2 instance
resource "aws_instance" "main" {
for_each = { for i, s in var.network.private_subnet_for_gitlab_ids : i => s }
ami = data.aws_ami.rhel_9_2.id
instance_type = "m5.large"
vpc_security_group_ids = [var.network.security_group_for_gitlab_id]
subnet_id = each.value
root_block_device {
volume_type = "gp3"
volume_size = "20"
encrypted = true
tags = {
Name = "${var.common.env}-ebs-${each.key}"
}
}
user_data = templatefile("../../files/script.sh", {
domain_name = "gitlab.${var.network.public_hosted_zone_name}"
})
iam_instance_profile = aws_iam_instance_profile.main.name
tags = {
Name = "${var.common.env}-ec2-${each.key}"
}
}
# Install GitLab
sudo dnf update -y
sudo dnf install -y curl policycoreutils openssh-server perl bind-utils
curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.rpm.sh | sudo bash
sudo EXTERNAL_URL="https://${domain_name}" dnf install -y gitlab-ee

コンソールで自動作成されるIAM PolicyをTerraformで定義したい場合にもtemplatefile
関数を使用するといい。自動作成されたIAM PolicyのJSONをそのまま保存し、templatefile
関数で参照するだけでいいので、HCL形式に変換する必要がない+main.tfがシンプルに書ける。
resource "aws_iam_policy" "policy_for_codebuild" {
name = "${var.common.env}-policy-for-codebuild"
policy = templatefile("../iam_policy_json_files/policy_codebuild.json", {
build_arn = aws_codebuild.backend.arn
})
}

TerraformにおけるAMIの指定方法
以下のリポジトリに記載のようにfilter
を使用してAMIを限定していく。

terraform import
とData Source
terraform import
を使用することで、Terraform管理されていない既存リソースをTerraform管理下に置く(tfstate に import する)ことができる。対して、Data SourceはTerraform管理下にないリソースをTerraform上で他リソースから参照可能にする機能であり、tfstate への import は行わない。
terraform import
によりtfstateに取り込むことによって、terraform destroy
などによる対象リソース削除時に、再び手動でのリソース作成+terraform import
を実行する必要があるため、基本的には以下のような基準で使い分ける。
- 実案件のように、環境削除などは基本的に起こり得ない場合には
terraform import
を実行する - 検証などで利用し、環境全体の作り直しを頻繁に行う場合にはData Sourceで参照する

terraformでのIAM定義
Before
aws_iam_role
のmanaged_policy_arns
でIAM RoleにIAM Policyをアタッチする。
resource "aws_iam_role" "test" {
name = var.test_role_name
assume_role_policy = data.aws_iam_policy_document.test.json
managed_policy_arns = [
"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
aws_iam_policy.secret_manager_read_policy.arn,
]
}
After
aws_iam_role_policy_attachment
でIAM RoleにIAM Policyをアタッチする。
resource "aws_iam_role" "test" {
name = var.test_role_name
assume_role_policy = data.aws_iam_policy_document.test.json
}
resource "aws_iam_role_policy_attachment" "test" {
for_each = {
ssmaccess = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
secret = aws_iam_policy.secret_manager_read_policy.arn,
}
role = aws_iam_role.test.name
policy_arn = each.value
}