TerraformでのECSのコンテナ定義の表記方法の違いの比較

# 概要
Amazon ECSをTerraformで構築する際にコンテナ定義の表記方法が公式ドキュメント[^1]に複数載っていたのですが、使い分けがよく分からなかったので調べてみました。例としてはECSを扱っていますが、TerraformでJSONを扱う時全般の話です。

# jsonencode関数を使用する場合
Terraformではjsonencodeという関数を使用すると、HCLの形式でJSONをインラインで定義することが可能です。以下は、jsonencode関数を使用してタスク定義を記述した例です。

```tf
resource "aws_ecs_task_definition" "service" {
  family = "service"
  container_definitions = jsonencode([
    {
      name      = "awesome-container"
      image     = "epic-image:latest"
      cpu       = 10
      memory    = 512
      essential = true
      portMappings = [
        {
          containerPort = 80
          hostPort      = 80
        }
      ]
    }
  ])
}
```

jsonencode関数はTerraform実行時に内部で記述内容を検証して型チェックやエスケープを行うので、安全にJSONデータを生成することが可能です[^2]。例として、上のタスク定義のCPUの部分で`cpu = "hoge"`のように文字列を入れると、整数値を入れろと怒られます。

```
Error: ECS Task Definition container_definitions is invalid: decoding JSON: json: cannot unmarshal string into Go struct field ContainerDefinition.Cpu of type int64
  with aws_ecs_task_definition.service,
  on ecs.tf line 121, in resource "aws_ecs_task_definition" "service":   
 121:   container_definitions = jsonencode([
 122:     {
 123:       name      = "awesome-container"
 124:       image     = "epic-image:latest"
 125:       cpu       = "hoge"
 126:       memory    = 512
 127:       essential = true
 128:       portMappings = [
 129:         {
 130:           containerPort = 80
 131:           hostPort      = 80
 132:         }
 133:       ]
 134:     }
 135:   ])
```

また、jsonencode関数ではTerraform内の変数や他のリソースの属性を動的にJSON内に埋め込むことも可能です。以下は、`image_name`と`image_version`という変数を定義し、コンテナ定義のimage属性の値として参照している例です。


```tf
variable "image_name" {
  default = "epic-image"
}

variable "image_version" {
  default = "latest"
}

resource "aws_ecs_task_definition" "service" {
  family = "service"
  container_definitions = jsonencode([
    {
      name      = "awesome-container"
      image     = "${var.image_name}:${var.image_version}"
      cpu       = 10
      memory    = 512
      essential = true
      portMappings = [
        {
          containerPort = 80
          hostPort      = 80
        }
      ]
    }
  ])
}
```
上記のように、`${var.image_name}:${var.image_version}`という式を使用することで、`image_name`と`image_version`の値がJSON内に動的に組み込まれます。

# ヒアドキュメントを使う場合
JSONに限らず、Terraformで複数行の文字列を記述する場合はヒアドキュメントというものを使うことが出来ます。ヒアドキュメント自体はUNIX系のOSで使用されているものなので、シェルスクリプト等を書いたことのある人なら馴染みがあるかと思います。

以下の例のように`<<`の後に`TASK_DEFINITION`のような識別子を記述すると、それ以降の行から`TASK_DEFINITION`が現れる行までの間に書かれた一連の内容が文字列として扱われます。

```tf
resource "aws_ecs_task_definition" "service" {
  family                = "service"
  container_definitions = <<TASK_DEFINITION
[
    {
        "name": "awesome-container",
        "image": "epic-image:latest",
        "cpu": 10,
        "memory": 512,
        "essential": true,
        "portMappings": [
            {
                "containerPort": 80,
                "hostPort": 80
            }
        ]
    }
]
TASK_DEFINITION
}
```

こちらの手法もjsonencode関数を使用する場合と同様にインラインでJSONを記述できますが、単純に記述されたとおりの文字列をJSONとして扱うだけなので、実行時にJSONの型チェックが効かなかったり、jsonencode関数のように動的に変数を埋め込むといったこともできません。また、インデントが崩れたりシンタックスハイライトも表示されないので、エディタ上での視認性も低いです。ただし、インデントは`<<-`を使うと整えることが出来ます[^3]。
ちなみに、HashiCorp社は、このヒアドキュメントを**JSON**で使用する方法は推奨していません[^4]。
>Don't use "heredoc" strings to generate JSON or YAML. Instead, use the jsonencode function or the yamlencode function so that Terraform can be responsible for guaranteeing valid JSON or YAML syntax.

https://developer.hashicorp.com/terraform/language/expressions/strings#generating-json-or-yaml

が、実際のTerraformのAPI参照のドキュメントの例では使用されていました。なぜ非推奨の書き方を公式ドキュメントに書いているのか理解に苦しみますが、JSON以外で文字列を使用するケースでは使いどころがあるかと思います。

# 外部のJSONファイルに定義する場合
コンテナ定義はJSON形式で別のファイルに記述して、Terraform内からそのファイルを参照することが可能です。以下は`service.json`というファイルに定義した例です。

```tf
resource "aws_ecs_task_definition" "service" {
  family                = "service"
  container_definitions = file("task-definitions/service.json")
```

この方法ではコンテナ定義は純粋なJSONファイルなので、エディタ上でJSONの構文チェックを実施することが可能です。
また、コンテナ定義が大規模で複雑な場合や、同様のコンテナを複数の場所で再利用したい場合にはこのように外部ファイルとして定義した方がコードの可読性と再利用性を高められます。

上の例ではfile関数を使用していますが、templatefile関数を使用すると、jsonencode関数と同様に動的にTerraform内の変数を埋め込むことが可能です[^5]。埋め込む変数は第二引数にオブジェクトとして指定します。

```tf
resource "aws_ecs_task_definition" "service" {
  family                = "service"
  container_definitions = templatefile(
      "task-definitions/service.json", 
      {
          cpu = var.cpu
    　}
  )
}
```

```json
{
    "name": "awesome-container",
    "image": "epic-image:latest",
    "cpu": ${cpu},
    "memory": 512,
    "essential": true,
    "portMappings": [
        {
            "containerPort": 80,
            "hostPort": 80
        }
    ]
}
```

# まとめ
ヒアドキュメントはHashiCorpも非推奨の書き方のようなので、jsonencode関数を使用するか、外部ファイルに定義するやり方がよさそうです。（なんで非推奨の書き方を公式ドキュメントに載せてるんだ...）

# 参考文献
[^1]: Terraform Registry. "aws_ecs_task_definition". 2023. https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition. (参照 2023-12-25).

[^2]: HashiCorp Developer. "jsonencode - Functions - Configuration Language". 2023. https://developer.hashicorp.com/terraform/language/functions/jsonencode. (参照 2023-12-25).

[^3]: Jeff Brown Tech. "Terraform Heredoc & JsonEncode: Best Practices". 2022. https://jeffbrown.tech/terraform-heredoc-jsonencode/. (参照 2023-12-25).

[^4]: HashiCorp Developer. "Strings and Templates - Configuration Language". 2023. https://www.terraform.io/language/expressions/strings#generating-json-or-yaml. (参照 2023-12-25).

[^5]: HashiCorp Developer. "templatefile - Functions - Configuration Language". 2023. https://developer.hashicorp.com/terraform/language/functions/templatefile. (参照 2023-12-25).