Open14

[TIL] 12. Terraform

wakakawakaka

terraform planの挙動

terraform planは-refresh-onlyオプションの有無で2種類がある。オプションの有無で異なる要素間の差分を表示することに注意する。

  1. terraform plan : AWS環境とterraformソースコード間の差分を表示する
  2. terraform plan -refresh-only : AWS環境とtfstateファイルの差分を表示する

同様に、terraform applyも-refresh-onlyオプションの有無の2種類がある。terraform planとは異なり、オプション無しのコマンドが-refresh-onlyオプション有りのコマンドに内包される。

  1. terraform apply : terraformソースコードで定義したリソースをAWS環境にデプロイする。その後、AWS環境の状態をtfstateファイルに更新する。
  2. terraform apply -refresh-only : AWS環境の状態をtfstateファイルに更新する。

https://blog.serverworks.co.jp/terraform-tfstate-plan-apply

wakakawakaka

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

wakakawakaka

for_eachでlist型を受け入れるためにforループを使用する

Subnetを複数定義する際に以下のようにlist型を使用する構成を取ることが多い。for_eachが受け取れる型はset(string)型かmap型であるため、このような場合にはforループを使用してlist型をmap型に変換してfor_eachを利用できる形にする。

variable.tf
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"
        }
    ]
  }
}
main.tf
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 i, s in ...} の結果
for_each = {
  0 = {
    az = "a"
    cidr = "192.168.10.0/24"
  },
  1 = {
    az = "c"
    cidr = "192.168.20.0/24"
  }
}

https://zenn.dev/kasa/articles/8fe998e04cb916
https://qiita.com/hikaru_motomiya/items/fdd784adb5134c31120c

wakakawakaka

上記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 (抜粋)
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"
  }
}
wakakawakaka

各moduleのoutputもmap型オブジェクト!

main.tfで参照する各moduleのoutput (module.network) は、以下のようなKey, Valueを持つmap型のオブジェクトである。

  • Key : outputブロックの名称
  • Value : outputブロックのvalue
output.tf (network module)
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
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
}
wakakawakaka

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のコンテナイメージを使ったコンテナが存在するというように更新される

https://dev.classmethod.jp/articles/note-about-terraform-ignore-changes/

wakakawakaka

provider設定で for_each は使えない

複数リージョンでのGuardDuty設定をfor_eachを用いて実装したかったが、そのためには下に示したaws.us-east-1の部分をaws.each.keyとする必要がある。だが、この書き方をTerraformは認識してくれずエラーとなってしまう。このようにprovider設定でfor_eachを使用することはできない

main.tf (guardduty module)
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
  }
}

https://stackoverflow.com/questions/72541598/using-for-each-with-provider-setting

wakakawakaka

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

https://qiita.com/ymmy02/items/e7368abd8e3dafbc5c52

wakakawakaka

file関数、templatefile関数を用いて外部ファイルを読み込む

templatefile関数を使用すれば、引数を渡す動的参照も可能であるため便利

UserData用のスクリプト上でドメイン名を動的参照
# 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}"
  }
}
script.sh
# 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

https://dev.classmethod.jp/articles/terraform-ec2-linux-userdata-file-function/

wakakawakaka

コンソールで自動作成されるIAM PolicyをTerraformで定義したい場合にもtemplatefile関数を使用するといい。自動作成されたIAM PolicyのJSONをそのまま保存し、templatefile関数で参照するだけでいいので、HCL形式に変換する必要がないmain.tfがシンプルに書ける

IAM Policyの定義
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
  })
}

https://dev.classmethod.jp/articles/writing-iam-policy-with-terraform/

wakakawakaka

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で参照する

https://zenn.dev/shonansurvivors/articles/6bda9deb152f8a

wakakawakaka

terraformでのIAM定義

Before

aws_iam_rolemanaged_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
}