クソみたいなリソースのterraform定義

なるべく`variable`で定義した内容で構築出来るようにしたかったので、クソみたいなリソース定義になったけどある程度設定で定義出来る形を考えてみた。

## 定義例

```instance.tf
variable "project" {
  type = "map"
  default {
    prefix = "main"
  }
}

variable "ec2_list" {
  type = "list"
  default = [
    "bastion-1",
    "bastion-2",
  ]
}

variable "ec2_options" {
  type = "map"
  default {
    // デフォルトの定義
    default.ami = "ami-ceafcba8"
    default.availability_zone = "ap-northeast-1a"
    default.instance_type = "t2.micro"
    default.key_name = "default"

    default.tags_keys  = "amirotate"
    default.tags_value = "{\"NoReboot\": true, \"Retention\": {\"Count\": 2}}"

    // グループ "bastion" の定義
    bastion.tags_keys  = "Type"
    bastion.tags_value = "bastion"

    // インスタンス "bastion-1" の定義
    bastion-1.group = "bastion"
    bastion-1.tags_keys  = "Mode"
    bastion-1.tags_value = "master"
    
    // インスタンス "bastion-2" の定義
    bastion-2.group = "bastion"
    bastion-2.tags_keys  = "Mode"
    bastion-2.tags_value = "standby"
    bastion-2.availability_zone = "ap-northeast-1c"
  }
}


resource "aws_instance" "main" {
    count = "${length(var.ec2_list)}"

    ami = "${lookup(
        var.ec2_options,
        "${element(var.ec2_list, count.index)}.ami",
        lookup(
            var.ec2_options,
            "${lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.group")}.ami",
            lookup(var.ec2_options, "default.ami", "false")
        )
    )}"

    instance_type = "${lookup(
        var.ec2_options,
        "${element(var.ec2_list, count.index)}.instance_type",
        lookup(
            var.ec2_options,
            "${lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.group")}.instance_type",
            lookup(var.ec2_options, "default.instance_type", "false")
        )
    )}"
    key_name = "${lookup(
        var.ec2_options,
        "${element(var.ec2_list, count.index)}.key_name",
        lookup(
            var.ec2_options,
            "${lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.group")}.key_name",
            lookup(var.ec2_options, "default.key_name", "default")
        )
    )}"
    availability_zone = "${lookup(
        var.ec2_options,
        "${element(var.ec2_list, count.index)}.availability_zone",
        lookup(
            var.ec2_options,
            "${lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.group")}.availability_zone",
            lookup(var.ec2_options, "default.availability_zone", "ap-northeast-1a")
        )
    )}"

    tags = "${merge(
        map("Name", "${lookup(var.project, "prefix", "default")}-${element(var.ec2_list, count.index)}"),
        zipmap(
            compact(split("#", lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.tags_keys", ""))),
            compact(split("#", lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.tags_value", "")))
        ),
        zipmap(
            compact(split("#", lookup(var.ec2_options, "${lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.group")}.tags_keys", ""))),
            compact(split("#", lookup(var.ec2_options, "${lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.group")}.tags_value", "")))
        ),
        zipmap(
            compact(split("#", lookup(var.ec2_options, "default.tags_keys", ""))),
            compact(split("#", lookup(var.ec2_options, "default.tags_value", "")))
        )
    )}"
}
```

planはこんな感じに。

```
  + aws_instance.main[0]
      id:                           <computed>
      ami:                          "ami-ceafcba8"
      associate_public_ip_address:  <computed>
      availability_zone:            "ap-northeast-1a"
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     "default"
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      placement_group:              <computed>
      primary_network_interface_id: <computed>
      private_dns:                  <computed>
      private_ip:                   <computed>
      public_dns:                   <computed>
      public_ip:                    <computed>
      root_block_device.#:          <computed>
      security_groups.#:            <computed>
      source_dest_check:            "true"
      subnet_id:                    <computed>
      tags.%:                       "4"
      tags.Mode:                    "master"
      tags.Name:                    "main-bastion-1"
      tags.Type:                    "bastion"
      tags.amirotate:               "{\"NoReboot\": true, \"Retention\": {\"Count\": 2}}"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>

  + aws_instance.main[1]
      id:                           <computed>
      ami:                          "ami-ceafcba8"
      associate_public_ip_address:  <computed>
      availability_zone:            "ap-northeast-1c"
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     "default"
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      placement_group:              <computed>
      primary_network_interface_id: <computed>
      private_dns:                  <computed>
      private_ip:                   <computed>
      public_dns:                   <computed>
      public_ip:                    <computed>
      root_block_device.#:          <computed>
      security_groups.#:            <computed>
      source_dest_check:            "true"
      subnet_id:                    <computed>
      tags.%:                       "4"
      tags.Mode:                    "standby"
      tags.Name:                    "main-bastion-2"
      tags.Type:                    "bastion"
      tags.amirotate:               "{\"NoReboot\": true, \"Retention\": {\"Count\": 2}}"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>
```

## 使用している技

### lookupでの値取得

`lookup(<変数名>, <mapキー名>, <デフォルト値>)`でmapに定義したデータを引っ張る。
これを多段にする事で、`bastion-1.ami`→`bastion.ami`→`default.ami`のような優先順位で設定値を取っている。

```
    ami = "${lookup(
        var.ec2_options,
        "${element(var.ec2_list, count.index)}.ami",
        lookup(
            var.ec2_options,
            "${lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.group")}.ami",
            lookup(var.ec2_options, "default.ami", "false")
        )
    )}"
```

グループの値を取得しているときにデフォルト値に`default`と入れて`default.ami`を引っ張らせようとしたけど、なぜか上手く展開してくれなくて泣いた。

### 動的なmap構築

タグには役割とか各種ツール用の情報入れたりをするんだけども、一つ一つで書くのが面倒なので、全体定義・グループ定義・個別定義をマージしたmapを作るように書いてみた。

```
    tags = "${merge(
        map("Name", "${lookup(var.project, "prefix", "default")}-${element(var.ec2_list, count.index)}"),
        zipmap(
            compact(split("#", lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.tags_keys", ""))),
            compact(split("#", lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.tags_value", "")))
        ),
        zipmap(
            compact(split("#", lookup(var.ec2_options, "${lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.group")}.tags_keys", ""))),
            compact(split("#", lookup(var.ec2_options, "${lookup(var.ec2_options, "${element(var.ec2_list, count.index)}.group")}.tags_value", "")))
        ),
        zipmap(
            compact(split("#", lookup(var.ec2_options, "default.tags_keys", ""))),
            compact(split("#", lookup(var.ec2_options, "default.tags_value", "")))
        )
    )}"
```

mergeはそのまんまで、羅列したmapをマージしてくれるので定義別にmapを渡してあげれば良い。
でも、map定義の中で値がstringのものとmapのものを混ぜるとエラーになってしまうので、キーと値を別々にstringで定義してsplitで分割。今回は途中にカンマを入れたかったので`#`を区切り文字として書いてみた。
splitで作成したlistをzipmapでmapに変換している。

で、定義が無い時は空文字列を取ってくるようにしているけど、これをそのままsplitに通すと空文字が含まれたリストになって、zipmapでmap化する際に空文字キーと空文字値のmapになってしまうので、compactに通して空リストにしてあげている。

空リスト同士でzipmapを通すと空mapになるので、mergeを通した時は無視されて、定義されている部分のみがちゃんとmapになる。

## 思った事
最初の定義は大変だけど、terraformをあまり知らない人にちょっとした修正をお願いする時とかには`variables.tf`のここ直してーと言えば済むのが良いとは思うんだけど、あまりに読みづらい。

あとinterpolation組み合わせまくるの大変なので関数定義ほしい。

## 参考資料
- [Interpolation Syntax - Terraform by HashiCorp](https://www.terraform.io/docs/configuration/interpolation.html)
- [Terraform Best Practices in 2017](https://qiita.com/shogomuranushi/items/e2f3ff3cfdcacdd17f99)