Terraform 0.13 の新機能を試してみた

# 1. はじめに

terraformのバージョン0.13が8/10にリリースされました。
普段terraformを使用しているため、今回のアップデートによりどんな機能が追加になったのか個人的に気になるところを検証してみます。<br>私が最近Azureを使っていることから、Azureを使用して検証します。

# 2. バージョンアップによって追加された新機能

新しく追加された機能は以下の5つでした。<br>

1. `module`ブロック内で`count`や`for_each`が使用可能になり、複数リソースを作成可能<br>
https://www.terraform.io/docs/configuration/modules.html#multiple-instances-of-a-module
1. `module`ブロック内で`depends_on`が使用可能になり、moduleの依存関係を定義可能<br>
https://www.terraform.io/docs/configuration/modules.html#other-meta-arguments
1. `variable`ブロック内で`validation`が可能に<br>
https://www.terraform.io/docs/configuration/variables.html#custom-validation-rules
1. サードパーティプロバイダーの自動インストールが可能に<br>
https://www.terraform.io/docs/configuration/provider-requirements.html
1. stateファイルの保存先をkubernetesに変更可能に<br>
https://www.terraform.io/docs/backends/types/kubernetes.html


1~3は今後使うことになりそうなので、試してみた結果をまとめていきます。


# 3, 実践

## 3.1. terraform アップデート

新機能を使っていくために、まずはterraformのアップデートをします。
私はhomebrewを使ってインストールしたので、以下コマンドにてterraformをアップデートしました。

~~~bash 
$ brew upgrade terraform
~~~

次に公式サイトの[Upgrading to Terraform v0.13](https://www.terraform.io/upgrade-guides/0-13.html)を参考に、`terraform 0.13upgrade`コマンドを実行してみます。

~~~bash
$ terraform 0.13upgrade

This command will update the configuration files in the given directory to use
the new provider source features from Terraform v0.13. It will also highlight
any providers for which the source cannot be detected, and advise how to
proceed.

We recommend using this command in a clean version control work tree, so that
you can easily see the proposed changes as a diff against the latest commit.
If you have uncommited changes already present, we recommend aborting this
command and dealing with them before running this command again.

Would you like to upgrade the module in the current directory?
  Only 'yes' will be accepted to confirm.

  Enter a value: yes

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

Upgrade complete!

Use your version control system to review the proposed changes, make any
necessary adjustments, and then commit.
~~~

こちらを実行すると`versions.tf`というファイルが実行したパス上にできていました。
中身を見ると`required_providers`ブロックが追加されていました。providerの指定をこのブロックで行うよう、変更になったようです。

~~~
terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
    }
  }
  required_version = ">= 0.13"
}
~~~

Azureプロバイダーは、v2.0.0以上から`feature`ブロックが必須となっているため、以下のように追記しておきます。

~~~
terraform {
  required_providers {
    azurerm = {
      version = ">=2.0.0"
      source = "hashicorp/azurerm"
    }
  }
  required_version = ">= 0.13"
}

# 以下ブロックを追加してます
provider "azurerm" {
  features {}
}
~~~

## 3.2. module内のcountとfor_each

module内でcountとfor_eachを使って、リソースの複数作成を検証してみます。
Azureの[リソースグループ](https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/management/manage-resource-groups-portal#what-is-a-resource-group)を複数作成するシナリオを想定して、ソースコードを書きます。

フォルダ構成は以下のようにしています。

~~~
terraform_test
├── development
│   ├── main.tf
│   ├── variable.tf
│   └── versions.tf
└── modules
    └── resource_group
        ├── main.tf
        ├── output.tf
        └── variable.tf
~~~

development配下のmain.tfから、modules配下のソースを参照してAzureのリソースを作成します。

modules配下のソースは以下のようになっています。

■ modules/resource_group/main.tf

~~~
resource azurerm_resource_group common_rg {
  name     = var.name
  location = var.location

  tags = {
    environment = var.environment
  }
}
~~~

■ modules/resource_group/variable.tf

~~~
variable name {
  description = "リソースグループ名"
}

variable location {
  description = "リージョン名"
}

variable environment {
  description = "環境名"
}
~~~

上記のソースを参照してリソースグループを作成していきます。
結論から言うと、countとfor_each にてリソースを作成した際の差分は、書き方に多少差があることと、module名に以下のような差分が出ています。

- count : module.resource_group[0]... 
- for_each : module.resource_group["terraform013-test-1"]...

それでは、この結論に至るまでのプロセスを実際のソースとterraform実行結果を見ながら確認していきます。

develop配下のソースをそれぞれのパターンで書いていきます。

### 3.2.1. for_eachパターン

■ development/main.tf

~~~
module resource_group {
  source = "../modules/resource_group"

  for_each    = toset(var.name)
  name        = each.key
  location    = var.location 
  environment = var.environment
}
~~~

■ development/variable.tf

~~~
variable name {
  type        = list
  default     = [
      "terraform013-test-1",
      "terraform013-test-2"
    ]
  description = "description"
}

variable location {
  type        = string
  default     = "Japan East"
  description = "リージョン名"
}

variable environment {
  type        = string
  default     = "dev"
  description = "環境"
}
~~~

■ 実行結果

~~~
$ terraform apply

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:

  # module.resource_group["terraform013-test-1"].azurerm_resource_group.common_rg will be created
  + resource "azurerm_resource_group" "common_rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "terraform013-test-1"
      + tags     = {
          + "environment" = "dev"
        }
    }

  # module.resource_group["terraform013-test-2"].azurerm_resource_group.common_rg will be created
  + resource "azurerm_resource_group" "common_rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "terraform013-test-2"
      + tags     = {
          + "environment" = "dev"
        }
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.resource_group["terraform013-test-2"].azurerm_resource_group.common_rg: Creating...
module.resource_group["terraform013-test-1"].azurerm_resource_group.common_rg: Creating...
module.resource_group["terraform013-test-1"].azurerm_resource_group.common_rg: Creation complete after 3s [id=/subscriptions/f9d36b58-96cd-48d1-9dc8-f760b1cf174e/resourceGroups/terraform013-test-1]
module.resource_group["terraform013-test-2"].azurerm_resource_group.common_rg: Creation complete after 3s [id=/subscriptions/f9d36b58-96cd-48d1-9dc8-f760b1cf174e/resourceGroups/terraform013-test-2]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
~~~

### 3.2.2. countパターン

■ development/main.tf

~~~
module resource_group {
  source = "../modules/resource_group"

  count       = length(var.name)
  name        = element(var.name, count.index)
  location    = var.location 
  environment = var.environment
}
~~~

■ development/variable.tf

~~~
variable name {
  type        = list
  default     = [
      "terraform013-test-1",
      "terraform013-test-2"
    ]
  description = "description"
}

variable location {
  type        = string
  default     = "Japan East"
  description = "リージョン名"
}

variable environment {
  type        = string
  default     = "dev"
  description = "環境"
}
~~~

■実行結果

~~~
$ terraform apply

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:

  # module.resource_group[0].azurerm_resource_group.common_rg will be created
  + resource "azurerm_resource_group" "common_rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "terraform013-test-1"
      + tags     = {
          + "environment" = "dev"
        }
    }

  # module.resource_group[1].azurerm_resource_group.common_rg will be created
  + resource "azurerm_resource_group" "common_rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "terraform013-test-2"
      + tags     = {
          + "environment" = "dev"
        }
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.resource_group[1].azurerm_resource_group.common_rg: Creating...
module.resource_group[0].azurerm_resource_group.common_rg: Creating...
module.resource_group[0].azurerm_resource_group.common_rg: Creation complete after 3s [id=/subscriptions/f9d36b58-96cd-48d1-9dc8-f760b1cf174e/resourceGroups/terraform013-test-1]
module.resource_group[1].azurerm_resource_group.common_rg: Creation complete after 3s [id=/subscriptions/f9d36b58-96cd-48d1-9dc8-f760b1cf174e/resourceGroups/terraform013-test-2]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
~~~

上記のように、countとfor_eachで2つのリソースグループが作成できました。<br>
2つの差分を見てみると、書き方に多少差があることと、module名に以下のような差分が出ています。

- count : module.resource_group[0]... 
- for_each : module.resource_group["terraform013-test-1"]...

可読性を考えるとfor_eachの方が何のためのリソースなのかがわかりやすいため、個人的にはfor_eachを積極的に使っていこうと思います。

## 3.3. module内のdepends_on

次に`module`ブロック内での`depends_on`を検証してみます。
通常依存関係はterraformが解消してくれますが、依存関係が見えないリソースがあった場合、意図的に作成順序を変更する必要がありました。依存関係を解消するために、この機能は便利です。

今回は検証として、depends_onを記載することでの動作の違いを見ていきます。
3.2と同様にAzureの[リソースグループ](https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/management/manage-resource-groups-portal#what-is-a-resource-group)を2つ作成するシナリオを想定します。
それぞれのソースを書いていきます。

### 3.3.1 depends_on なし

こちらのソースコードは、以下のように書き、実行します。

■ development/main.tf

~~~
module first_resource_group {
  source = "../modules/resource_group"

  name        = var.resource_group_name.first
  location    = var.location 
  environment = var.environment
}

module secound_resource_group {
  source = "../modules/resource_group"

  name        = var.resource_group_name.secound
  location    = var.location 
  environment = var.environment
}
~~~

■ development/variable.tf

~~~
variable resource_group_name {
  type        = map(string)
  description = "リソースグループ名"
  default     = {
    first   = "terraform013-test-1"
    secound = "terraform013-test-2"
  }
}

variable location {
  type        = string
  default     = "Japan East"
  description = "リージョン名"
}

variable environment {
  type        = string
  default     = "dev"
  description = "環境"
}
~~~

■実行結果

~~~
$ terraform apply

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:

  # module.first_resource_group.azurerm_resource_group.common_rg will be created
  + resource "azurerm_resource_group" "common_rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "terraform013-test-1"
      + tags     = {
          + "environment" = "dev"
        }
    }

  # module.secound_resource_group.azurerm_resource_group.common_rg will be created
  + resource "azurerm_resource_group" "common_rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "terraform013-test-2"
      + tags     = {
          + "environment" = "dev"
        }
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.secound_resource_group.azurerm_resource_group.common_rg: Creating...
module.first_resource_group.azurerm_resource_group.common_rg: Creating...
module.first_resource_group.azurerm_resource_group.common_rg: Creation complete after 1s [id=/subscriptions/f9d36b58-96cd-48d1-9dc8-f760b1cf174e/resourceGroups/terraform013-test-1]
module.secound_resource_group.azurerm_resource_group.common_rg: Creation complete after 1s [id=/subscriptions/f9d36b58-96cd-48d1-9dc8-f760b1cf174e/resourceGroups/terraform013-test-2]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
~~~

どちらのリソースグループも並列に作成されました。<br>
次に depends_on を追加した際の動作を確認します。

### 3.3.2 depends_on あり

こちらのソースコードは、以下のようになってます。

■ development/main.tf

~~~
module first_resource_group {
  source = "../modules/resource_group"

  name        = var.resource_group_name.first
  location    = var.location 
  environment = var.environment
}

module secound_resource_group {
  source = "../modules/resource_group"

  name        = var.resource_group_name.secound
  location    = var.location 
  environment = var.environment
  depends_on  = [ module.first_resource_group ]
}
~~~

■ development/variable.tf

3.3.1と同様のため割愛


■実行結果

~~~
$ terraform apply

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:

  # module.first_resource_group.azurerm_resource_group.common_rg will be created
  + resource "azurerm_resource_group" "common_rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "terraform013-test-1"
      + tags     = {
          + "environment" = "dev"
        }
    }

  # module.secound_resource_group.azurerm_resource_group.common_rg will be created
  + resource "azurerm_resource_group" "common_rg" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "terraform013-test-2"
      + tags     = {
          + "environment" = "dev"
        }
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

module.first_resource_group.azurerm_resource_group.common_rg: Creating...
module.first_resource_group.azurerm_resource_group.common_rg: Creation complete after 1s [id=/subscriptions/f9d36b58-96cd-48d1-9dc8-f760b1cf174e/resourceGroups/terraform013-test-1]
module.secound_resource_group.azurerm_resource_group.common_rg: Creating...
module.secound_resource_group.azurerm_resource_group.common_rg: Creation complete after 0s [id=/subscriptions/f9d36b58-96cd-48d1-9dc8-f760b1cf174e/resourceGroups/terraform013-test-2]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
~~~

実行結果を見ると、`first_resource_group`が作成された後に`secound_resource_group`が作成されました。

## 3.4. variable内のvalidation

こちらの機能を使うと、定義している変数をバリデーションチェックできるようになりました。
こちらの機能を試してみます。

今回は、環境名に `dev`,`stg`,`prod` 以外を指定した場合、バリデーションチェックエラーとなるようなシナリオを想定し、コードを書いてみます。

■development/variable.tf

~~~
variable environment {
  type        = string
  default     = "dev"
  description = "環境"

  validation {
    condition     = var.environment == "dev" || var.environment == "stg" || var.environment == "prod"
    error_message = "The environment must be dev, stg, or prod."
  }
}
~~~

■実行結果

~~~
$ terraform plan -var 'environment=test'

Error: Invalid value for variable

  on variable.tf line 16:
  16: variable environment {

The environment must be dev, stg, or prod.

This was checked by the validation rule at variable.tf:21,3-13.
~~~

実行結果からわかるように、バリデーションチェックができ、自分の指定したエラーメッセージを出力することができました。
チームで決まっている、ネーミングルールを統一する際や、クラウドの文字数制限等を事前にチェックできる便利な機能です。


# 4. まとめ

terraform 0.13のバージョンアップ後の新機能を3つ検証しました。
3つとも待ち望んでいたような機能だったので、今後有効活用していきます。
特にvalidationは、ネーミングルールの統一だったり、クラウドの制約だったりを簡単にチームメンバーにお知らせできるようになるので、どんどん使っていこうと思います。

また、kubernetesにもstateファイルを保持できるようになっているので、kubernetesを本格導入する際に使っていこうと思いました。