Terraformでエディタに怒られずにJSONをテンプレート化する

# はじめに

Terraformのリソースの引数にJSONを渡す、というのはよくあるシチュエーションだと思います。AWSを使用してしる場合は特に。
また、引数として渡すJSONのパラメータの一部をテンプレートとして変数化して環境間で値を変える、というのもよくあるシチュエーションだと思います。

例えばAWSの[TaskDefinition](https://www.terraform.io/docs/providers/aws/r/ecs_task_definition.html)を例に挙げるとこんな感じです。

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


JSONファイルをテンプレート化して、CPUの値をvariableで変更するように書いています。
該当のテンプレートのJSONファイル、中身は以下のようになってます。

```json:service.json.tpl
[
    {
        "name": "alpine",
        "image": "alpine:latest",
        "command": [
            "env"
        ],
        "cpu": ${cpu},
        "memory": 128,
        "essential": true,
        "environment": [
            {
                "name": "TERRAFORM",
                "value": "BENRI"
            },
            {
                "name": "AWS",
                "value": "BENRI"
            }
        ]
    }
]
```

`"cpu": ${cpu}`がテンプレートとしてパラメータ化している部分ですね。
ここでひとつ問題がでます。このJSONファイルはnumber型のcpuの部分をパラメータ化したため、JSONとして正しい書式では無くなってしまったのです。
この場合エディタが警告を出しくるのでなんだか嫌だなぁという気持ちになります。また使ってるエディタ次第ですがフォーマッタの挙動がおかしくなり整形に手間がかかります。

そこでエディタに怒られずにJSONをテンプレート化する方法する方法を紹介します。

なお、実用的なテクニックかどうかは怪しいのであしからずw

# 環境

`Terraform v0.12.18`を使って本記事の検証を行いました。

# 方法その1:VariablesからJSONを生成する

JSONがJSONである以上、number型の所に数値以外のものを突っ込むと怒られるに決まっています。
つまり怒られずにJSONをテンプレート化するにはJSON以外でJSONを書けば良いのです(?)

terraform v0.12では豊富な変数の型をサポートしています。その中にはMap,List,Objectなどの複合型もサポートしており、またネストした変数もサポートしています。
そう、つまりはVariablesで求めるJSONを満たす複合型を作って`jsonencode`してやればよいのです。

こんな感じで複合型を作ります。

```main.tf
variable "ecs_tasks" {
  type = list(object({
    name      = string
    image     = string
    command   = list(string)
    essential = bool
    cpu       = number
    memory    = number
    environment = list(object({
      name  = string
      value = string
    }))
  }))

  default = [{
    name  = "alpine"
    image = "alpine:latest"
    command = [
      "env"
    ]
    essential = true
    cpu       = 128 //コメントも書けるよ
    memory    = 128
    environment = [
      {
        name  = "TERRAFORM"
        value = "BENRI"
      },
      {
        name  = "AWS"
        value = "BENRI"
      }
    ]
  }]
}
```

[JSONにコメント書きたいというのは全人類の共通の悩み](https://qiita.com/minamijoyo/items/18fa28132cf737c86ddf)ですが、この方法でもコメントを自由に書けます。やったね！！

ではconsoleで確認してみます。

```console
» terraform console
> var.ecs_tasks
[
  {
    "command" = [
      "env",
    ]
    "cpu" = 128
    "environment" = [
      {
        "name" = "TERRAFORM"
        "value" = "BENRI"
      },
      {
        "name" = "AWS"
        "value" = "BENRI"
      },
    ]
    "essential" = true
    "image" = "alpine:latest"
    "memory" = 128
    "name" = "alpine"
  },
]
> jsonencode(var.ecs_tasks)
[{"command":["env"],"cpu":128,"environment":[{"name":"TERRAFORM","value":"BENRI"},{"name":"AWS","value":"BENRI"}],"essential":true,"image":"alpine:latest","memory":128,"name":"alpine"}]
```

イエーイ！！目的のJSONがゲットできました。当然、main.tfは正しいHCL2の書式なので怒られません！！

が、この方法だと変数のdefaultの部分的な上書きがうまく動きません。

```tf:fail.tfvars
//cpuだけ上書きしたい
ecs_tasks = [{
  cpu = 256
}]
```

```console
» terraform console -var-file=fail.tfvars

Error: Invalid value for input variable

  on fail.tfvars line 1:
   1: ecs_tasks = [{
   2:   cpu = 256
   3: }]

The given value is not valid for variable "ecs_tasks": element 0:
attributes "command", "environment", "essential", "image", "memory", and
"name" are required.
```

全部を上書きする場合はうまく動きます。

```tf:success.tfvars
ecs_tasks = [{
  name  = "alpine"
  image = "alpine:latest"
  command = [
    "env"
  ]
  essential = true
  cpu       = 256
  memory    = 128
  environment = [
    {
      name  = "TERRAFORM"
      value = "BENRI"
    },
    {
      name  = "AWS"
      value = "BENRI"
    }
  ]
}]
```

```console
» terraform console -var-file=success.tfvars
> var.ecs_tasks
[
  {
    "command" = [
      "env",
    ]
    "cpu" = 256
    "environment" = [
      {
        "name" = "TERRAFORM"
        "value" = "BENRI"
      },
      {
        "name" = "AWS"
        "value" = "BENRI"
      },
    ]
    "essential" = true
    "image" = "alpine:latest"
    "memory" = 128
    "name" = "alpine"
  },
]
```

方法その１はパラメータの上書きという観点でちょっと使いづらいですね。そこで方法その２。

# 方法その2:ネストしたlocalsからJSONを生成する

方法その１と大体一緒ですが、今度はlocalsを使います。
テンプレートの置き換え、という意味ではこちらの方がテンプレートの使い勝手に近いです。
localsにネストした値を定義します。

```main.tf
variable "cpu" {
  type    = number
  default = 256
}

locals {
  ecs_tasks = [{
    name  = "alpine"
    image = "alpine:latest"
    command = [
      "env"
    ]
    essential = true
    cpu       = var.cpu //コメントも書けるよ
    memory    = 128
    environment = [
      {
        name  = "TERRAFORM"
        value = "BENRI"
      },
      {
        name  = "AWS"
        value = "BENRI"
      }
    ]
  }]
}
```

当然こちらの方法でもコメントを自由に書けます。

```console
» terraform console
> local.ecs_tasks
[
  {
    "command" = [
      "env",
    ]
    "cpu" = 256
    "environment" = [
      {
        "name" = "TERRAFORM"
        "value" = "BENRI"
      },
      {
        "name" = "AWS"
        "value" = "BENRI"
      },
    ]
    "essential" = true
    "image" = "alpine:latest"
    "memory" = 128
    "name" = "alpine"
  },
]
> jsonencode(local.ecs_tasks)
[{"command":["env"],"cpu":256,"environment":[{"name":"TERRAFORM","value":"BENRI"},{"name":"AWS","value":"BENRI"}],"essential":true,"image":"alpine:latest","memory":128,"name":"alpine"}]
```

こっちの方法だと部分的に変数で値を上書きすることが簡単です！やったね。

# おわりに

ケースにもよりますが、テンプレート化したJSONの代わりにlocalsでネストした変数を使う、というテクニックはそこそこ便利なのではないかな？と書いてて思いました。
HCLの変数はJSONの見た目にも近いですし、フォーマッタ効きますし、コメントも書けますし。

# おまけ

1つのローカル変数を使いまわしながら値を上書きしたい時は、forと[merge関数](https://www.terraform.io/docs/configuration/functions/merge.html)の組み合わせが使えます。
トリッキーすぎてイマイチですが、試してみたら動いたので紹介。もっとシンプルな方法あったら教えて下さい。

```main.tf
variable "cpu" {
  type    = number
  default = 256
}

locals {
  ecs_tasks = [{
    name  = "alpine"
    image = "alpine:latest"
    command = [
      "env"
    ]
    essential = true
    cpu       = var.cpu //コメントも書けるよ
    memory    = 128
    environment = [
      {
        name  = "TERRAFORM"
        value = "BENRI"
      },
      {
        name  = "AWS"
        value = "BENRI"
      }
    ]
  }]

  //ecs_tasksのcpuを上書き その1
  overwrite_a = [{ cpu = 512 }]
  ecs_tasks_a = [for key, value in local.ecs_tasks : merge(value, local.overwrite_a[key])]

  //ecs_tasksのcpuを上書き その2
  overwrite_b = [{ cpu = 1024 }]
  ecs_tasks_b = [for key, value in local.ecs_tasks : merge(value, local.overwrite_b[key])]
}
```

```console
» terraform console
> local.ecs_tasks_a
[
  {
    "command" = [
      "env",
    ]
    "cpu" = 512
    "environment" = [
      {
        "name" = "TERRAFORM"
        "value" = "BENRI"
      },
      {
        "name" = "AWS"
        "value" = "BENRI"
      },
    ]
    "essential" = true
    "image" = "alpine:latest"
    "memory" = 128
    "name" = "alpine"
  },
]
> local.ecs_tasks_b
[
  {
    "command" = [
      "env",
    ]
    "cpu" = 1024
    "environment" = [
      {
        "name" = "TERRAFORM"
        "value" = "BENRI"
      },
      {
        "name" = "AWS"
        "value" = "BENRI"
      },
    ]
    "essential" = true
    "image" = "alpine:latest"
    "memory" = 128
    "name" = "alpine"
  },
]
```