TerraformでJSONにコメントを書きたいだけの人生だった

# はじめに
JSONにコメント書きたいというのは全人類の共通の悩みです。

Terraformでインフラコードを管理するメリットの１つは、設定値にコメントが書けることですが、設定値がJSONを受け付けるようなリソースの場合は、なぜその設定値になっているのか、JSON内部の各パラメータにコメントを書きたすぎる問題が発生します。
たとえば、AWSのECSタスク定義とか複雑めなJSONで、環境変数とかパフォーマンスチューニング系のパラメータがなぜこの値になってるのかとか、JSONの中にコメント書きたいじゃないですか。

この記事ではTerraformでJSONにコメントを書きたいみんなのために、いくつかの方法を紹介します。

# 環境

手元の環境は以下のとおりです。

```
$ terraform version
Terraform v0.12.3
+ provider.aws v2.17.0
```

# コメントを書く方法あれこれ
## ヒアドキュメント

たぶんみんながすぐに思いつく方法として、 [ヒアドキュメント](https://www.terraform.io/docs/configuration/expressions.html#string-literals) で書いて、近くにコメントを残すという方法があります。

```main.tf
provider "aws" {
  region  = "ap-northeast-1"
}

resource "aws_ecs_task_definition" "test" {
  family                = "test"
  # ほげ
  container_definitions =  <<-EOF
  [
    {
      "name": "test",
      "image": "nginx:latest",
      "essential": true,
      "memoryReservation": 50
    }
  ]
  EOF
}
```

ただこの方法のデメリットは、JSONのシンタックスハイライトが効かなくなるので、できればファイルを分けて読み込みたいところです。
シンタックスハイライトが効かないと、JSONのケツカンマでシンタックスエラーになるという、人類の２番目の悩みが生まれます。

つまりこうしたい。

```main.tf
resource "aws_ecs_task_definition" "test" {
  family                = "test"
  container_definitions = file("test.json")
```

```test.json
[
  {
    "name": "test",
    "image": "nginx:latest",
    "essential": true,
    "memoryReservation": 50
  }
]
```

しかしこうするとJSONにコメントが書けない。つらい。
まぁJSONのファイルがこれぐらい短ければ、ヒアドキュメントでもまぁ実運用上はそんなに困らないでしょう。

## 専用のリソースタイプを使う

汎用的な方法ではないのですが、たとえばAWSでよく使われる [aws_iam_policy](https://www.terraform.io/docs/providers/aws/r/iam_policy.html) リソースの場合は、[aws_iam_policy_document](https://www.terraform.io/docs/providers/aws/d/iam_policy_document.html) データソースのような、特別なデータソースを使うことで、素のJSONを書くことを避けて、HCLで設定値ができるので、HCLでコメントが書けます。HCLなのでリストのケツカンマもエラーになりません。

```main.tf
data "aws_iam_policy_document" "example" {
  statement {
    sid = "1"

    # ほげ
    actions = [
      "s3:ListAllMyBuckets",
      "s3:GetBucketLocation",
    ]

    resources = [
      "arn:aws:s3:::*",
    ]
  }
}

resource "aws_iam_policy" "example" {
  name   = "example_policy"
  path   = "/"
  policy = data.aws_iam_policy_document.example.json
}
```

さらにうれしいことに、HCLでスキーマのバリデーションが効くので、キー名をタイポしてもエラーになります。専用のリソースがあれば積極的に使うべきです。
デメリットはもちろん、すべてのリソースで使えるわけではなく、汎用的な方法ではないことです。

## YAMLで書いてJSONに変換する

以前から [jsonencode](https://www.terraform.io/docs/configuration/functions/jsonencode.html) という関数があったのですが、最近リリースされたTerraform v0.12.2に [yamldecode](https://www.terraform.io/docs/configuration/functions/yamldecode.html) という関数が追加されてることに気づきました。
ということはもうおわかりですね？

つまりこういうことです。

```main.tf
resource "aws_ecs_task_definition" "test" {
  family                = "test"
  container_definitions = jsonencode(yamldecode(file("test.yaml")))
}
```

```test.yaml
- name: test
  image: nginx:latest
  essential: true
  # 特に根拠はないですが、メモリは50MBもあればたぶん十分でしょう。
  memoryReservation: 50
```

この方法のいいところは、設定をYAMLで書いて、Terraformの組み込み関数でYAML=>JSONに変換するので、どのようなリソースタイプでも汎用的に使えることです。
デメリットは、もはやJSONではないので、公式ドキュメントからサンプルとかをコピペするときにちょっとめんどうぐらい。

JSONからYAMLに変換するツールはいろいろあると思いますが、 参考までに、[yq](https://github.com/kislyuk/yq) という[jq](https://github.com/stedolan/jq)のラッパーコマンドが便利なので、ついでに使い方を説明しておきます。

macOSの場合は、 `brew install python-yq` でインストールできます。
※ jqのラッパーなので別途jqも必要です。なければ別途 `brew install jq` して下さい。

JSONをYAMLに変換するだけなら、使い方はこんなかんじ。

```
$ yq -y "." test.json > test.yaml
```

# まとめ

３行まとめ

- JSONにはコメントを書けない（知ってた）
- YAMLで書いてJSONに変換すると汎用的に使えてよいのではなかろうか
- Terraform v0.12.2以降で `jsonencode(yamldecode(file("test.yaml")))`

なんかもっとよい方法知ってる人がいたら教えて下さい。

# 追記

JSONにコメント書くのにもっとよい方法を教えてもらいました。jsonencodeに直接JSONを渡せます。

[Terraformのjsonencode関数にはJSONを入れても動くよ](https://qiita.com/kanga/items/1ae96b7da2a7d76b070e)

先ほどの例を書き換えると、こんなかんじです。

```tf
resource "aws_ecs_task_definition" "test" {
  family = "test"
  container_definitions = jsonencode(
    [
      {
        "name" : "test",
        "image" : "nginx:latest",
        "essential" : true,
        # 特に根拠はないですが、メモリは50MBもあればたぶん十分でしょう。
        "memoryReservation" : 50,
      },
    ]
  )
}
```

なんでこれが動くかというと、mapリテラルが `=` だけじゃなくて `:` も受け付けるので、JSON風に書くことができるというハックです。なるほどですね。

https://www.terraform.io/docs/configuration/types.html#map-