[ アップデート ] aws_ecs_task_definition に CI/CD との競合を防ぐ track_latest 引数がリリースされました

aws_ecs_task_definition を定義する際、いつも指定しているあの ignore_changes が、不要になるかもしれません。 tfstate への反映、コードとの差分があった場合の挙動に違いがあるため、理解しておくと今後役立つかもです。
2024.02.20

こんにちは! AWS 事業本部コンサルティング部のたかくに(@takakuni_)です。

aws_ecs_task_definition で CI/CD との競合を防ぐために、 track_latest 引数がリリースされました。

r/aws_ecs_task_definition: add track_latest attribute by GerardSoleCa · Pull Request #30154 · hashicorp/terraform-provider-aws · GitHub

今まで

Terraform で ECS のタスク定義を作成するケースを想定します。(側だけ作ってタスク定義の中身はアプリ側で管理するイメージ)

以下のように、 CI/CD パイプラインからの更新を上書きしないよう、変更を検知しない表現をよくするのではないでしょうか。

ecs.tf

resource "aws_ecs_task_definition" "main" {
  family = "track-latest"
  requires_compatibilities = ["FARGATE"]
  cpu    = "256"
  memory = "512"
  network_mode = "awsvpc"
  execution_role_arn = aws_iam_role.task_exec.arn
  runtime_platform {
    cpu_architecture = "X86_64"
    operating_system_family = "LINUX"
  }
  container_definitions = <<TASK_DEFINITION
[
  {
    "name": "nginx",
    "image": "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/track-latest:1.0",
    "portMappings": [
      {
        "name": "nginx-80-tcp",
        "protocol": "tcp",
        "containerPort": 80,
        "hostPort": 80
      }
    ]
  }
]
TASK_DEFINITION
  lifecycle {
    ignore_changes = all # または ignore_changes = [container_definitions]
  }
}

何のためにやっているのか

この表現を行わない場合、何が起こるのかをおさらいしましょう。

まず、コンテナイメージの CI/CD パイプラインから、新しいイメージをデプロイするべく、以下のタスク定義の部分が少なからず更新されていきます。

ecs.tf

resource "aws_ecs_task_definition" "main" {
  family = "track-latest"
  requires_compatibilities = ["FARGATE"]
  cpu    = "256"
  memory = "512"
  network_mode = "awsvpc"
  execution_role_arn = aws_iam_role.task_exec.arn
  runtime_platform {
    cpu_architecture = "X86_64"
    operating_system_family = "LINUX"
  }
  container_definitions = <<TASK_DEFINITION
[
  {
    "name": "nginx",
    "image": "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/track-latest:1.0",
    "portMappings": [
      {
        "name": "nginx-80-tcp",
        "protocol": "tcp",
        "containerPort": 80,
        "hostPort": 80
      }
    ]
  }
]
TASK_DEFINITION
}

CI/CD から上記の変更が行われた後に terraform apply を打つと、次のようにコードとリソースの変更を検知し、コンテナイメージのバージョンが v1.1 から、 v1.0 に巻き戻ろうとしてしまいます。

takakuni@taskdef-update % terraform apply
aws_ecr_repository.main: Refreshing state... [id=track-latest]
aws_ecs_cluster.main: Refreshing state... [id=arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:cluster/track-latest]
aws_vpc.main: Refreshing state... [id=vpc-03f80213655df4c60]
aws_iam_role.task_exec: Refreshing state... [id=track-latest-task-exec]
aws_subnet.public_c: Refreshing state... [id=subnet-0d95295b1edb19a41]
aws_internet_gateway.main: Refreshing state... [id=igw-0124e471c35dd03f7]
aws_subnet.public_a: Refreshing state... [id=subnet-097529c6bf6e81dd5]
aws_security_group.main: Refreshing state... [id=sg-0acabaf720ab6f8c6]
aws_route_table.main: Refreshing state... [id=rtb-070f4239a361d3be2]
aws_vpc_security_group_egress_rule.main_allow_all_traffic_ipv4: Refreshing state... [id=sgr-04f28007c1fbd257b]
aws_route.main: Refreshing state... [id=r-rtb-070f4239a361d3be21080289494]
aws_route_table_association.public_c: Refreshing state... [id=rtbassoc-09fec389983334f70]
aws_route_table_association.public_a: Refreshing state... [id=rtbassoc-0eab83b46dcfdc306]
aws_ecs_task_definition.main: Refreshing state... [id=track-latest]
aws_ecs_service.main: Refreshing state... [id=arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:service/track-latest/track-latest]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # aws_ecs_task_definition.main must be replaced
-/+ resource "aws_ecs_task_definition" "main" {
      ~ arn                      = "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/track-latest:9" -> (known after apply)
      ~ arn_without_revision     = "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/track-latest" -> (known after apply)
      ~ container_definitions    = jsonencode(
          ~ [
              ~ {
                  - cpu          = 0
                  - environment  = []
                  - essential    = true
                  ~ image        = "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/track-latest:1.1" -> "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/track-latest:1.0"
                  - mountPoints  = []
                    name         = "nginx"
                  - volumesFrom  = []
                    # (1 unchanged attribute hidden)
                },
            ] # forces replacement
        )
      ~ id                       = "track-latest" -> (known after apply)
      ~ revision                 = 9 -> (known after apply)
      - tags                     = {} -> null
      ~ tags_all                 = {} -> (known after apply)
        # (8 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

ECS サービス側でタスク定義リビジョンの更新が行われなければ、イメージバージョンが巻き戻ったタスク定義が使われることはないですが、あまりいい状態ではないですよね。

また、 Terraform の仕様上、更新前のタスク定義リビジョンに関しては「非アクティブ」な状態に変更するため、見栄えもあまり良くないです。

この辺りの issue は以下の URL でディスカッションされているので興味がある方はご覧ください。

aws_ecs_task_definition overwrites previous revision · Issue #258 · hashicorp/terraform-provider-aws · GitHub

この事象を解決するためのワークアラウンドとして、先ほどのような ignore_changes によって、引数の変更検知を無視する表現が必要でした。

今から

AWS Provider v5.37.0 のアップデートで、新たに track_latest 引数が増えました。

terraform-provider-aws/CHANGELOG.md at main · hashicorp/terraform-provider-aws · GitHub

この引数は、 aws_ecs_task_definitioncontainer_definitions を追跡するかどうかを指定する引数です。デフォルトは false になります。

track_latest が false の場合、 tfstate へ container_definitions の追跡及び、最新状態の反映を行わない挙動になります。

aws_ecs_task_definition | Resources | hashicorp/aws | Terraform | Terraform Registry

ignore_changes と何が違うのか?

ignore_changes は、指定した引数の tfstate の値と、コードの差分を無視するかどうかを決めるものです。したがって、コードの差分がどうであれ、基本的には tfstate には最新のリソースの設定値が反映されるような挙動をとります。

  • コードと tfstate で差分があった場合
    • 指定した引数は apply 時に ignore される
    • 指定していない場合は apply 時にコードの値が反映される
  • tfstate の状況
    • 最新状況が反映される

詳しくは ちゃだいん さんがまとめてくれた、以下の記事をご覧ください。

[Terraform] 誤解されがちなignore_changesの動き・機密情報はstateに保持されるのか? | DevelopersIO

今回の track_latest 引数は、 tfstate 側の反映を行うかどうかの引数であるため、コードと最新状況を追跡しなくなった時点の tfstate に差分があった場合は、変更が検知されます。

track_latest が false の場合

  • コードと tfstate で差分があった場合
    • 変更検知が発生し、 apply 時にコードの値が反映される
  • tfstate の状況
    • aws_ecs_task_definitioncontainer_definitions 引数の最新状況が反映されなくなる

ちょっと小難しい話をしましたが、以下のような対応で OK だと個人的には思います。

新規で作る場合

  • 特に要件なければ track_latest はデフォルトの false で OK
    • ここでいう要件とは、 ECS タスク及びタスク定義のデプロイは Terraform 側で完全に管理したいパターン
  • その他、変更を検知したくない(Git 側に寄せたい)項目は、 ignore_changes を反映させる
    • ほぼほぼ意味はないけど container_definitions に ignore_changes していてもお守りにはなりそう

既存から更新する場合

  • 特に要件なければ track_latest はデフォルトの false で OK
    • ここでいう要件とは、 ECS タスク及びタスク定義のデプロイは Terraform 側で完全に管理したいパターン
    • アップデート時にコードと実物の値を合わせてあげる
  • その他、変更を検知したくない(Git 側に寄せたい)項目は、 ignore_changes を反映させる
  • ほぼほぼ意味はないけど container_definitions に ignore_changes していてもお守りにはなりそう

まとめ

以上、 「CI/CD との競合を防ぐ track_latest 引数がリリースされました」でした。 ECS 使いの方々には、嬉しいアップデートなのではないでしょうか。

なお、 ECS タスクのデプロイを CI/CD で管理したい場合は、 aws_ecs_servicetask_definition および desired_count あたりは引き続き、 ignore_changes に指定する必要があるためご注意を。

この記事がどなたかの参考になれば幸いです。 AWS 事業本部コンサルティング部のたかくに(@takakuni_)でした!