Terraformで複雑なロジックを書く (1)



Terraformを使うとプログラミングに不慣れな人でも比較的簡単にインフラのコード化（IaC）ができますが、その反面、ちょっとでも複雑なロジックを書こうとするとクセの強さを実感するときがあります。

そもそもTerraformのウリは「本質的なコードだけを宣言的に記述できて、誰でも簡単に読みやすいコードが書けること」なので、複雑なロジックを書こうとすること自体が若干ナンセンスなのです。が、汎用性や再利用性を考慮したコードを書くとなると避けては通れません。

--------

今回は入門編として、Terraformで複雑なロジックを書く際の基本的な考え方やテクニックについて紹介してみたいと思います。（ちなみにTerraformのバージョンは0.12を前提とします）

## 単純なループは簡単に書ける

Terraformでも単純なループであれば簡単に書けます。

例えば、インスタンスを作るこんな定義について

```terraform
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
}
```

ループ処理をしたいときは `count` を使います。

```terraform
resource "aws_instance" "web" {
  count         = 2
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
}
```

これで2つのインスタンスが作られることになります。簡単ですね。

`count` 以外にも繰り返し処理を実現する機能はありますが、まずは基本として `count` をおさえましょう。

## 2重ループは無理、1重ループだけ。

例えば、あるリスト `web_list = ["web1", "web2"]` の各要素に対して繰り返し処理をしたいとき

```
locals {
  web_list = ["web1", "web2"]
}

resource "aws_instance" "web" {
  count         = length(local.web_list)
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  
  tags = {
    Name = local.web_list[count.index]
  }
}
```

このように書いてそれぞれのインスタンスに別々の名前を付けることができます。これは難しくありませんね。

では、例としては嘘くさくなりますが「 web_a と web_b と web_c、それぞれを t2.micro と t2.small の組で作りたい」ということで

- `web_list = ["web_a", "web_b", "web_c"] `
- `type_list = ["t2.micro", "t2.medium"]`

という2つのリストが与えられたとき、どのように書けばよいのでしょうか。

シェルスクリプト脳の僕らは「2つのリストの組み合わせだから、2重ループだな…」と考えてすぐにこんなコードを妄想するはずです。

```
locals {
  web_list = ["web_a", "web_b", "web_c"]
  type_list = ["t2.micro", "t2.medium"]
}

for name in local.web_list
do
  for type in local.type_list
  do
    resource "aws_instance" "web" {
      ami           = data.aws_ami.ubuntu.id
      instance_type = type
      tags = {
        Name = name
      }
    }
  done
done
```

しかし、先ほど説明したようにTerraformのループ表現はあくまでも `count` しかないので、こんなふうに2重ループを表現することはできません。

ではどうやるか。

2重ループを1重ループに変換することを考えます。

そのためには、処理する**データ自体を工夫する**必要があります。2重ループは…言い換えれば「2つのデータを組み合わせて処理をする」ということになるので、食わせるデータを事前に組み合わせておけば1重ループで処理が可能ということになります。

言葉にすると分かりづらいので端的に表現してしまうと、

```
web_list = ["web_a", "web_b", "web_c"]
type_list = ["t2.micro", "t2.medium"]
```

という別々のデータを処理するのではなく

```
web_type = [
  { "name" = "web_a", "type" = "t2.micro" },
  { "name" = "web_a", "type" = "t2.medium" },
  { "name" = "web_b", "type" = "t2.micro" },
  { "name" = "web_b", "type" = "t2.medium" },
  { "name" = "web_c", "type" = "t2.micro" },
  { "name" = "web_c", "type" = "t2.medium" },
]
```

という感じであらかじめ**組にしたデータ**を作っておけば、1重ループで処理できるということです。1列のデータになっているので1重ループで処理できるということですね。

じゃあこの組み合わせというのを、どのようにコードとして表現するか…

突然ですが、皆さんはPythonのリスト内包表記を知っていますか？

```python
squares = [i**2 for i in range(5)]
```

こんなやつです。

これは言い換えるとつまりこういうこと

````python
squares = [0, 1, 4, 9, 16]
````

リスト内でループを回して要素を1個ずつ作っていくイメージです。ループを回してデータを作っているんですね。

実はTerraformにもこれと似た機能があり、

```
squares = [for i in range(5): i*i]
```

というふうに同様の表現をすることができます。（ `range` 関数もある）

これを利用して、2重ループを表現するデータを作成します。

```
locals {
  web_list = ["web_a", "web_b", "web_c"]
  type_list = ["t2.micro", "t2.medium"]
  
  web_type = [
  	for name in web_list: [
  	  for type in type_list: {
  	    "name" = name
  	    "type" = type
  	  }
  	]
  ]
}
```

このとき、 `web_type` 変数の中身は以下のようになります。

```
[
  [
    { "name" = "web_a", "type" = "t2.micro" },
    { "name" = "web_a", "type" = "t2.medium" },
  ],
  [
    { "name" = "web_b", "type" = "t2.micro" },
    { "name" = "web_b", "type" = "t2.medium" },
  ],
  [
    { "name" = "web_c", "type" = "t2.micro" },
    { "name" = "web_c", "type" = "t2.medium" },
  ]
]
```

…まあ、それっぽくはなっていますが、これではまだダメですね。階層が無駄に深くなって凸凹しちゃっています。凸凹していると1重ループで淡々と処理することはできませんね。

では、階層をフラットにする加工を施しましょう。こういったデータの加工を行うため、[Terraformには多くの関数がバンドル](https://www.terraform.io/docs/configuration/functions.html)されています。関数はかなり充実しているので、柔軟にデータ加工を行うことができます。

そのなかの一つに [`flatten` 関数](https://www.terraform.io/docs/configuration/functions/flatten.html)があります。これを使うと文字通り、階層をフラットにすることができるので、さきほどのデータを

```
[
  { "name" = "web_a", "type" = "t2.micro" },
  { "name" = "web_a", "type" = "t2.medium" },
  { "name" = "web_b", "type" = "t2.micro" },
  { "name" = "web_b", "type" = "t2.medium" },
  { "name" = "web_c", "type" = "t2.micro" },
  { "name" = "web_c", "type" = "t2.medium" }
]
```

このように変換することができます。

つまり変数定義を書き直して、

```
locals {
  web_list = ["web_a", "web_b", "web_c"]
  type_list = ["t2.micro", "t2.medium"]
  
  web_type = flatten(
    [
  	  for name in web_list: [
  	    for type in type_list: {
  	      "name" = name
  	      "type" = type
  	    }
      ]
    ]
  )
}
```

と定義すれば、

```
resource "aws_instance" "web" {
  count         = length(local.web_type)
  ami           = data.aws_ami.ubuntu.id
  instance_type = local.web_type[count.index].type
  
  tags = {
    Name = local.web_type[count.index].name
  }
}
```

こんな感じで目的の処理を実装することができます。

## まとめ

- Terraformでループを表現する基本的な機能としては `count` があります。
- Terraformには入れ子のループを直接的に表現する仕組みがありません。
  - 複数のリストを組み合わせたループを実装したいときには、まず1重ループで処理できるようにローカル変数を作成することから始めます。
  - 組み込み関数が充実しているので、データの加工はかなり柔軟にできます。

今回の例に限らずなにか複雑な処理を実装するときは、**まず必要な情報を整理したローカル変数を作るところから始め**、あとは `resource` 内部でも適宜組み込み関数を駆使してロジックを組み立てていくという流れを覚えるとラクにコーディングできるようになります。

あと、HashiCorp社のツール全般に言えることですが、Terraformのドキュメントは平易な英語でコードサンプルも充実しているので、なにはともあれまずドキュメントを読んでみることを初心者にはオススメします！！！

以上