ECSタスク定義をTerraformから切り離す3つの方法(ignore_changes / Provider 5.x / ecspresso)
ECSのタスク定義をTerraformから切り離してCI/CDに移管するには、ignore_changes、Provider 5.xのtrack_latest、ecspressoの3つの方法があります。どれを選ぶかはチーム構成とデプロイ頻度で決まります。
タスク定義をCI/CDに移管したかった
ECSをTerraformで管理していて、「インフラはTerraform、アプリのデプロイはCI/CD」って責務を分けたくなったんですよね。タスク定義にはコンテナイメージのタグが入っているので、デプロイのたびにTerraformを回すのはどう考えてもおかしい。
で、タスク定義だけTerraformから切り離そうとしたら、これが構造的に難しかったんですよ。
aws_ecs_serviceがタスク定義のARNを握っている問題
調べてたら、サービスとタスク定義の関係がこうなっていて、ここが厄介なポイントでした。
┌─────────────────────────────────────┐
│ aws_ecs_service │
│ │
│ task_definition = "app:3" ←──┐ │
│ desired_count = 2 │ │
│ load_balancer { ... } │ │
└────────────────────────────────┼───┘
│
│ ARN参照
│
┌────────────────────────────────┼───┐
│ aws_ecs_task_definition │ │
│ │
│ family = "app" │
│ container_definitions = [...] │
│ revision = 3 │
└────────────────────────────────────┘
aws_ecs_serviceのtask_definition引数にはタスク定義のARNやfamily:revision形式を渡します。
Terraformでは通常、aws_ecs_task_definitionリソースの出力から取ることになります。
つまりこういうことが起きます。
- タスク定義をTerraformから消すと、サービス側の参照先がなくなる
dataソースで外部のタスク定義を参照できるが、CI/CDがリビジョンを上げるたびにstateとの差分が出るterraform planのたびに「タスク定義を元に戻そうとする」差分が表示される
サービスがタスク定義のARNを直接持っている以上、片方だけ管理を移すと、そのままではdriftしやすい構造なんですよね。
ECSタスク定義をTerraformから切り離す3つの方法
方法1: ignore_changesで差分を無視する(従来型)
resource "aws_ecs_service" "app" { name = "app" task_definition = aws_ecs_task_definition.app.arn lifecycle { ignore_changes = [task_definition] } }
初回だけTerraformでタスク定義を作り、以降はCI/CDが新リビジョンを登録してデプロイする方式です。Terraform側はignore_changesで差分を見て見ぬふりをします。
ただ、ignore_changes = [task_definition] と属性を明示指定していれば、無視されるのはその属性だけです。
ただし terraform plan がクリーンに見えるので、CI/CD側で登録した最新リビジョンとの乖離に気づきにくいのが厄介なんですよね。
方法2: track_latest + ignore_changes(AWS Provider 5.37+)
AWS Provider 5.37.0 で aws_ecs_task_definition に track_latest 引数が追加されました。これを使うと、CI/CDが新リビジョンを登録したとき、Terraform側も最新リビジョンを自動追跡します。
resource "aws_ecs_task_definition" "app" { family = "app" container_definitions = file("task-definition.json") track_latest = true } resource "aws_ecs_service" "app" { name = "app" task_definition = aws_ecs_task_definition.app.arn deployment_circuit_breaker { enable = true rollback = true } lifecycle { ignore_changes = [task_definition] } }
track_latest = true はタスク定義リソース側のオプションで、terraform plan 実行時に最新のACTIVEリビジョンのARNを参照します。CI/CDが新リビジョンを登録してもTerraform側が自動的にstateを更新するわけではなく、次回のplan時に最新を見に行く挙動です。
サービス側の ignore_changes と組み合わせることで、「plan時にCI/CDが上げたリビジョンを認識しつつ、plan差分は出さない」という状態が作れます。
なお、terraform-aws-modules/ecs モジュールを使っている場合は、モジュール側の変数 ignore_task_definition_changes でこの組み合わせをラップしてくれます。
ただし ignore_task_definition_changes を有効にすると、load_balancer ブロックの変更まで無視されるという副作用が報告されています(terraform-aws-modules/terraform-aws-ecs#165)。モジュールのバージョンと挙動を確認してから使ってください。
素のproviderを使っているか、モジュールを使っているかで書き方が変わる点は注意してください。
方法3: サービスごとCI/CD管理に移す(ecspresso等)
Terraform管理: CI/CD管理(ecspresso): ┌──────────────┐ ┌──────────────────────┐ │ VPC │ │ aws_ecs_service │ │ ECS Cluster │ │ aws_ecs_task_definition│ │ ALB │ │ デプロイ │ │ IAM Role │ └──────────────────────┘ │ Security Group│ └──────────────┘
ecspressoのようなECS専用デプロイツールを使うと、サービスとタスク定義をまるごとCI/CD側に移せます。Terraformはクラスターやネットワークなどのインフラ層だけを管理する形です。
ecspressoはTerraformのstateファイルを参照するtfstateプラグインを持っていて、VPC IDやサブネットIDなどをTerraformの出力から引っ張ってこれるんですよ。管理境界を切りつつも値の受け渡しは維持できる。
これが地味にありがたい。
どの方法を選ぶか(判断基準)
方法1(ignore_changes)が向いているケース: - 既存のTerraform構成を大きく変えたくない - デプロイ頻度がそこまで高くない(週数回程度) - stateの乖離を許容できる運用体制がある
方法2(track_latest + ignore_changes)が向いているケース: - AWS Provider 5.37以上を使っている(または上げられる) - Terraformでサービスの他の設定(オートスケーリング、LB等)も管理したい - 方法1の副作用が気になる
方法3(ecspresso等)が向いているケース: - デプロイ頻度が高い(日に複数回) - インフラチームとアプリチームの責務を明確に分けたい - ECSサービスの設定変更もCI/CDパイプラインに乗せたい
一つ注意点があって、方法1・2では「Terraformが管理しているはずなのに実態と違う」というグレーゾーンが残ります。
定期的にterraform planの差分を確認して、意図しないドリフトが起きていないかチェックする運用は入れておいた方がいいです。
まとめ
ECSタスク定義をTerraformから切り離しにくいのは、aws_ecs_serviceがタスク定義ARNを直接参照するというリソース間の依存構造に起因しています。
Provider 5.37+のtrack_latestでかなり改善されましたが、根本的には「サービスとタスク定義の管理境界をどこに引くか」という設計判断の問題です。
「タスク定義だけ切り離す」と考えるより、「管理境界をサービスのどこに引くか」で考えた方が、筋の良い構成に辿り着けます。