Terraformで構築したECSをCodePipeline等でローリング更新するとタスク定義のリビジョンがずれる問題をdataを使って回避する

2022.08.31

「CodePipelineでECSをローリング更新した時に、Terraformと実際のリビジョンがずれてplanやapply時に差異が出る」

TerraformでECSを管理して、デプロイは他のツールを使っているパターンがあると思います。(例: CodePipeline,GithubActions,CircleCI等)

TerraformでECSを作成して、コンテナのデプロイにはCodePipeline(ローリング更新)を使っていました。

CodePipelineとTerraformタスク定義のリビジョンがズレてしまって、Terrformを変更していないのに terraform plan の際に差分が出てしまうことがありました。

結論: dataを使ってECSタスク定義の最新のarnを取得しましょう。

resource "aws_ecs_service" "mongo" {
  name          = "mongo"
  cluster       = aws_ecs_cluster.foo.id
  desired_count = 2

  # Track the latest ACTIVE revision
  task_definition = data.aws_ecs_task_definition.mongo.arn
}

aws_ecs_task_definition | Data Sources | hashicorp/aws | Terraform Registry

事象: タスク定義のリビジョンがずれる

タスク定義のリビジョンがずれるパターンのECSのtfファイルです。

ecs.tf

resource "aws_ecs_cluster" "this" {
  name = "${local.name_prefix}-cluster"
  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

resource "aws_ecs_service" "sample_app" {
  name             = local.name_prefix
  cluster          = aws_ecs_cluster.this.arn
  launch_type      = "FARGATE"
  # terraformで作成したタスク定義のARNを直接指定
  task_definition  = aws_ecs_task_definition.sample_app.arn
  desired_count    = 1
  platform_version = "1.4.0"

  network_configuration {
    assign_public_ip = false
    security_groups  = [module.ecs_sg.security_group_id]
    subnets          = module.vpc.private_subnets
  }
  load_balancer {
    target_group_arn = aws_lb_target_group.sample_app.arn
    container_name   = "httpd"
    container_port   = 80
  }
  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  lifecycle {
    ignore_changes = [ desired_count ]
  }
}

resource "aws_ecs_task_definition" "sample_app" {
  family                   = local.name_prefix
  cpu                      = 256
  memory                   = 512
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  container_definitions = templatefile("./file/container_definitions.json", {
    ecr_repository_url : aws_ecr_repository.httpd.repository_url,
  })
  execution_role_arn = aws_iam_role.ecs_tasks.arn
}

このファイルを使って、リソースを作成してCodePipelineでローリング更新を行います。

その後、terraform側のコードは変更せず terraform planで差分を確認します。

$ terraform plan
# 出力結果から抜粋
  # aws_ecs_service.sample_app will be updated in-place
  ~ resource "aws_ecs_service" "sample_app" {
        id                                 = "arn:aws:ecs:ap-northeast-1:00000000000:service/ecs-rolling-update-cluster/ecs-rolling-update"
        name                               = "ecs-rolling-update"
        tags                               = {}
      ~ task_definition                    = "arn:aws:ecs:ap-northeast-1:00000000000:task-definition/ecs-rolling-update:12" -> "arn:aws:ecs:ap-northeast-1:00000000000:task-definition/ecs-rolling-update:11"
        # (14 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

terraform側でデプロイしたタスク定義のリビジョンが11で、CodePipelineでローリング更新した時に12になったため差分が発生しています。

解決策: dataを使って最新のタスク定義を取得するように取得するように修正

サービスにタスク定義を渡す部分を変更して、dataから最新のタスク定義を取得するようにします。

ecs.tf

resource "aws_ecs_cluster" "this" {
  name = "${local.name_prefix}-cluster"
  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

resource "aws_ecs_service" "sample_app" {
  name        = local.name_prefix
  cluster     = aws_ecs_cluster.this.arn
  launch_type = "FARGATE"
  # CodePipelineでデプロイ時にリビジョンが更新されるため、最新のrevisionをdataで取得
  task_definition  = data.aws_ecs_task_definition.sample_app.arn
  desired_count    = 1
  platform_version = "1.4.0"

  network_configuration {
    assign_public_ip = false
    security_groups  = [module.ecs_sg.security_group_id]
    subnets          = module.vpc.private_subnets
  }
  load_balancer {
    target_group_arn = aws_lb_target_group.sample_app.arn
    container_name   = "httpd"
    container_port   = 80
  }
  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  lifecycle {
    ignore_changes = [desired_count]
  }
}

resource "aws_ecs_task_definition" "sample_app" {
  family                   = local.name_prefix
  cpu                      = 256
  memory                   = 512
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  container_definitions = templatefile("./file/container_definitions.json", {
    ecr_repository_url : aws_ecr_repository.httpd.repository_url,
  })
  execution_role_arn = aws_iam_role.ecs_tasks.arn
}

data "aws_ecs_task_definition" "sample_app" {
  task_definition = aws_ecs_task_definition.sample_app.family
}

差分が出るパターンと同様の環境で、planを実行して差分が出ないことを確認できました。

$ terraform plan

補足: aws_ecs_serviceのlifecycleのignore_changesにタスク定義を含めるパターン

ECS Service側でタスク定義をlifecycleでignore_changesにすることで、差分が出なくはなります。

今回はタスク定義をTerraformで管理しているためTerraform側で変更した時にはECSのサービスも更新したかったため、dataを使いました。

ローリング更新の場合、CodeBuild上でコンテナをビルドしてファイル(imagedefinitions.json)にイメージのURIを書き込みます。 そのファイルを元に、タスク定義のイメージの部分を更新します。

この際に更新対象となるタスク定義は、サービスで稼働しているタスク定義になります。

lifecycleの方法では、タスク定義のリビジョンは増えますが稼働しているサービスのタスク定義は変わりません。 (更新したタスク定義を使って、CodePipelineでデプロイできない)

そのため、タスク定義を更新した場合はサービスのタスク定義を手動で更新する必要があります。

おわりに

Terraform管理のタスク定義をTerraform以外でECSデプロイするとリビジョンがずれる問題でした。

AWS Providerバージョン 3.70 まではdataのAttributesにarnが無かったため、以下の書き方をする必要がありました。

現在は、arnも追加されてスッキリ書けるようになっています。

resource "aws_ecs_service" "mongo" {
  name          = "mongo"
  cluster       = aws_ecs_cluster.foo.id
  desired_count = 2

  # Track the latest ACTIVE revision
  task_definition = "${aws_ecs_task_definition.mongo.family}:${max(aws_ecs_task_definition.mongo.revision, data.aws_ecs_task_definition.mongo.revision)}"
}

この件でサンプルコードの1行修正ですが、terraform-provider-awsにコントリビュートしました。 今後もチャンスがあれば、貢献していきたいです。

fix example does not use data ecs_tasks_definition doc by msato0731 · Pull Request #26476 · hashicorp/terraform-provider-aws

以上、AWS事業本部の佐藤(@chari7311)でした。