mapを使った読みやすいterraform variablesの書き方

<img width="1024" alt="スクリーンショット 2018-01-19 0.51.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/157454/4226932e-5709-8440-561e-70991fc9b55b.png">

# ディレクトリ構成
以下のように、`terraform/provider/aws/env/stg`としました。
変数ファイルである`variables.tf`もメイン処理をする`ec2.tf`も同じディレクトリに置きます。

環境ごとに完全にファイルを分断するイメージで、tfstateファイルも環境ごとに別にします。（保管場所はS3です）
なお、module化はしません。

```
 %tree 
.
└── provider
    └── aws
        └── env
            └── stg
                ├── backend.tf
                ├── ec2.tf
                └── variables.tf
```


# 変数の書き方

## Before
これまでterraformの変数ファイルはこんな感じで書いてました。

```before_variables.tf
variable "ami" {
  default = "ami-4af5022c"
}

variable "instance_type" {
  default = "t2.micro"
}

variable "instance_key" {
  default = "id_rsa"
}
```
実際のEC2のパラメータはこれだけではありません。もっとたくさんある上に、AWSのリソースは他にもあるとなるとかなり長い変数ファイルとなります。

特徴としては、variableが毎回並んでしまって行数が増える。読みにくい見にくい探しづらい。

**これを解決するのに[map](https://www.terraform.io/docs/configuration/variables.html#maps)を使いました。**

> A map value is a lookup table from string keys to string values. This is useful for selecting a value based on some other provided value.
A common use of maps is to create a table of machine images per region, as follows:

```sample.tf
variable "images" {
  type    = "map"
  default = {
    "us-east-1" = "image-1234"
    "us-west-2" = "image-4567"
  }
}
```

## After
一つのvariableに対してリソースの値をまとめます。
map型の宣言は省略可能みたいです。

```after_variables.tf
variable "ec2_config" {
  type = "map" #省略化
  default = {
    ami = "ami-4af5022c" 
    instance_type = "t2.micro" 
    instance_key = "id_rsa" 
  }
}
```

こうすることで、リソースごとに値がまとまるので読みやすくなりました。
ではメイン処理をするec2.tfを見ていきましょう。

# map型でのec2.tfの書き方

## Before
これまでのリソース定義の書き方はこちらです。

```before_ec2.tf
resource "aws_instance" "vtryo-web01" {
  ami              = "${var.ami}"
  instance_type    = "${var.instance_type}"
  instance_key     = "${var.instance_key}"

　　　　tags {
    Name = "vtryo-web01"
  }
}
```

そしてafter版に対するvariableに対応するリソース定義の仕方です。
map型で変数を格納したので、lookup関数を使って値を参照させます。

beforeのときよりやや長くなったように見えますが、ルールを覚えれば（後述）そこまで複雑ではない上、柔軟性は良くなっています。

```after_ec2.tf
resource "aws_instance" "vtryo-web" {
  ami              = "${lookup(var.ec2_config, "ami")}" 
  instance_type    = "${lookup(var.ec2_config, "instance_type")}" 
  key_name         = "${lookup(var.ec2_config, "instance_key")}" 

  tags {
    Name = "vtryo-${format("web%02d", count.index + 1)}" 
  }
```
## 注意点
resource定義内のami, instance_type, key_nameはterraform側で名前が決まっています。variables.tf内の変数名は任意ですが、こちらは自由には決められないので注意しましょう。
[aws_instance](https://www.terraform.io/docs/providers/aws/r/instance.html#argument-reference)
### lookup
上記の書き方は、lookup関数でKeyを直接指定する方法を取っています。
こちらを参考にしています。
[Terraformのoutputでmapを利用する方法](https://qiita.com/shogomuranushi/items/cc387a1233cc7f4e6e60)

たとえば以下であれば
`ami = "${lookup(var.ec2_config, "ami")}" `

「`variables.tf`内の`ec2_config`の`ami`の値を参照する」という意味になります。

## format
最終行の`"vtryo-${format("web%02d", count.index + 1)}" `についてですが、変数格納後の値は`vtryo-web01`になります。
ちなみに`"web%02d"`を、`"web%01d"`にすると`vtryo-web1`になってしまうので注意です。

# terraform init
さて実行です。

```
% terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (1.7.0)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 1.7"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
```

# terraform plan

```
 % terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.vtryo-web
      id:                           <computed>
      ami:                          "ami-4af5022c"
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     "id_rsa"
      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.%:                       "1"
      tags.Name:                    "vtryo-web01"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

```
変数が格納されました！

# ちょっと応用
さきほどのec2.tfでNameには`"vtryo-${format("web%02d", count.index + 1)}" `を書きました。
この出力結果は`vtryo-web01`です。
これを`stg-vtryo-web01`のように、環境を先頭に入れる方法を書いておきます。

## variable "env"
variables.tfに`variable "env { }"`を書きます。

```variables.tf
variable "env" { }

variable "ec2_config" {
  default = {
    ami           = "ami-4af5022c"
    instance_type = "t2.micro"
    instance_key  = "id_rsa"
  }
}
```
`{ }`の中にdefaultを入れてもよいです。その場合は以下のように書きます。

```variables.tf
variable "env" { 
  default = "text message..."
 }
```

## ${var.env}
一方でec2.tfには以下を書きます。

```
resource "aws_instance" "vtryo-web" {
  ami                      = "${lookup(var.ec2_config, "ami")}"
  instance_type            = "${lookup(var.ec2_config, "instance_type")}"
  key_name                 = "${lookup(var.ec2_config, "instance_key")}"
  tags {
    Name = "${var.env}-vtryo-${format("web%02d", count.index + 1)}"
  }
}
```
${var.env}をつけることで、`terraform plan`したときに入力を求められます。
入力した内容が、`${var.env}`に格納されるという仕組みです。

## terraform plan
`Enter a value`を求められるので今回はstgにしました。
すると、stgという文字列が格納されます。

```
% terraform plan
var.env
  Enter a value: stg

Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.vtryo-web
      id:                           <computed>
      ami:                          "ami-4af5022c"
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     "id_rsa"
      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.%:                       "1"
      tags.Name:                    "stg-vtryo-web01"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
```
ちなみに`terraform plan -var "env=stg"`とすることで入力なしで格納することが可能です。

# 今回の補足というか余談

**「terraformのbest-practice、わかりづらくない？」**

開発者が推奨している構成だとはわかっていますが、素直にそんな感想がありました。
[terraform best-practice](https://github.com/hashicorp/best-practices/tree/master/terraform)
（もちろんQiitaにかかれている記事も読んでいます
[Terraform Best Practices in 2017](https://qiita.com/shogomuranushi/items/e2f3ff3cfdcacdd17f99)）

ファイルの中に何度も宣言をしないといけないようですし、どうにも読みづらい印象があります。
また、「Environmentで環境を分ける」構成になっていますが、[公式](https://www.terraform.io/docs/state/workspaces.html#best-practices)曰く

> Workspaces can be used to manage small differences between development, staging, and production, but they should not be treated as the only isolation mechanism.

とあります。我らがGoogle翻訳を使うとこんなことを言ってます。

> ワークスペースは、開発、ステージング、およびプロダクションの小さな違いを管理するために使用できますが、唯一の分離メカニズムとして扱うべきではありません。

初見で解読するには骨がいる内容です。何度もterraformを使い込んだらわかるんでしょうか。
ま、単純に私の技術力が追いついていない可能性の方が高いですが（爆）

**とはいえ誰でも彼でもterraformの技術力が高いわけではないので、より読みやすくメンテナンスしやすい書き方をしても良いと思っています。**

terraformは一歩間違えるとインフラそのものが削除されることがあるので、それを踏まえて**確実な**方が良いだろうというきもちです。

*今回はQiitaの記事にだいぶ助けられたこともあったので、Qiitaに書くことにしました。*

## シンプルに書きたい欲
terraformにはmodule化が出来る機能があって、それがやや難易度を上げているように思います。

変数をmodule化できることでメリットもありますが、利用ができなければ意味がない。terraformにはmoduleの[レジストリ](https://registry.terraform.io/)がありますが、仕組みを理解しないとやっぱり使うのは大変です。

学習コストばかりかかるくらいなら、いっそ使わずにシンプルにコードを書いて行こうということでした。

## best-practiceに固執しない
重要なのは可読性ととっつきやすさだと思ったので、あえて固執せずにディレクトリを構成し、terraformを見やすく書く方法を考えました。
誰かの参考になれば幸いです。

# 参考
[Terraformのoutputでmapを利用する方法 - Qiita](https://qiita.com/shogomuranushi/items/cc387a1233cc7f4e6e60)
[Terraform Best Practices in 2017 - Qiita](https://qiita.com/shogomuranushi/items/e2f3ff3cfdcacdd17f99)
[TerraformでのAWS環境構築の設定を分ける - Qiita](https://qiita.com/tjun/items/f153cd9a6ff050b14724)
[terraform](https://www.terraform.io/docs/)
[さらに一歩楽しむterraform. moduleでIAM UserとPolicy管理を簡素化しよう](https://qiita.com/gamisan9999/items/ccfc983f23d20193ef38)
本記事は[こちら](https://blog.vtryo.me/infra-engneering/terraform-variable)のクローンです。