[小ネタ] TerraformでECSリソースを管理するときのちょっとした問題と解決案

これは「[「はじめに」の Advent Calendar 2021](https://adventar.org/calendars/7037)」14日目の記事です。

## TerraformでECSリソースを作成

terraformでECSのリソースを作る場合、例えばコードは以下のような感じになります。(※リソースは省略しているので、以下のコードそのままでは`apply`は出来ません。ご了承ください)

```ecs.tf
resource "aws_ecs_cluster" "foo" {
  name = "foo"
}

resource "aws_ecs_task_definition" "web" {
  family = "web"
  container_definitions = jsonencode([
    {
      name      = "web"
      image     = "example/web:1.2.3"
      cpu       = 128
      memory    = 512
      essential = true
      portMappings = [
        {
          containerPort = 80
          hostPort      = 80
        }
      ]
    }
  ])
}

resource "aws_ecs_service" "foo" {
  name            = "web"
  cluster         = aws_ecs_cluster.foo.id
  task_definition = aws_ecs_task_definition.web.arn
  desired_count   = 1
  iam_role        = aws_iam_role.foo.arn
}
```

### ECS Taskの問題点

- Taskが利用するDockerイメージを更新したい
- TaskのCPU,Memoryを変更したい
- 環境変数、Secretsなどを追加したい

これらの場合、Task定義`aws_ecs_task_definition`を更新する必要があります。
例として、イメージのタグを変更する場合

```ecs.tf
(略)
resource "aws_ecs_task_definition" "web" {
  family = "web"
  container_definitions = jsonencode([
    {
      name      = "web"
      # 1.2.3 → 1.2.4
      image     = "example/web:1.2.4"
      cpu       = 128
      memory    = 512
      essential = true
      portMappings = [
        {
          containerPort = 80
          hostPort      = 80
        }
      ]
    }
  ])
}
(略)
```
このtfを`apply`すると、Task定義は再作成されるという差分が出ます。

```
-/+ aws_ecs_task_definition.web (new resource required)
...
```

これは、AWSではTask定義はリビジョンごとに別リソースとして扱っており、terraformはAWSのAPIを呼び出していることに起因します。
同じFamilyだとしても別リビジョンは別リソースなので、古いリビジョンを消して新しいリビジョンを作成することになります。

この問題は、terraformのissueでも議論されています。
過去のリビジョンを保持しておきたい場合、terraformでTask定義を更新するのができなくなります。

https://github.com/hashicorp/terraform-provider-aws/issues/258

解決策としては、
- stateを作成直後に消してterraform管理外にする
- `lifecycle.ignore_changes` に `container_definitions` を追加することで更新をしない
  - 更新が必要な場合は、`ignore_changes`をコメントアウトして`apply`する

などがありますが、どちらもハック的なワークアラウンドとなります。

## ECS Serviceの問題
`aws_ecs_service` もtaskのARNが変わるため、差分が出てしまいます。
ま\Task定義を変更しない場合でも、差分が出るときがあります。

デプロイを別のCIツール（CodePipelineやGithub Actionsなど)で実行していることが多いと思われます。
この場合、そもそもterraformで作成したTask定義のリビジョンと実際に利用されているTask定義のリビジョンに差分が出ており、気づかずにそのままapplyすると稼働中のアプリケーションが先祖還りするリスクがあります。

CIでデプロイされている場合など、最新のTask定義を常に参照したい場合、以下のようなコードにすることで実現できます。

```ecs.tf
(略)
data "aws_ecs_task_definition" "web" {
  task_definition = aws_ecs_task_definition.web.family
}

resource "aws_ecs_service" "foo" {
  name            = "web"
  cluster         = aws_ecs_cluster.foo.id
  task_definition = "${aws_ecs_task_definition.web.family}:${max(aws_ecs_task_definition.web.revision, data.aws_ecs_task_definition.web.revision)}"
  desired_count   = 1
  iam_role        = aws_iam_role.foo.arn
}
```
これは、以下のリンクを参考にしています。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ecs_task_definition

デプロイしたリビジョンを、Parameter Store等に入れて同じように`data source`として参照する方法も有効です。

ECSサービスをDataSourceとして使えば、デプロイ中のRevisionをそのまま参照することができそうですが・・・(自己参照的なことになるけど、できるのかな・・・)

`task_definition` を `ignore_changes` に入れることでそもそも差分を出さないようにすることは出来ますが、`ignore_changes`に頼りすぎるのは可能であれば避けるべきでしょう。

ECS Serviceはそもそも`desired_count`というAutoScalingで変更されうるリソースを持っていたりするので、terraformでECS Serviceを管理すると`desired_count`は`ignore_changes`に追加せざるを得ない状況です。このあたり、Serviceとしてどのリソースを使うのか、という情報と実際に動いているServiceの状態を別々に管理したさがありますね・・・

※ AWSに Task定義は `ecs_task_family` というリソースがあったり、 `ecs_service_deployment` というリソースがあればこういった問題は起きづらい気がします。