Terraformのv0.13.0で遊んでみた【Terraform】

# TL; DR
今月にTerraoformの[v0.13.0-betaがリリース](https://github.com/hashicorp/terraform/blob/v0.13.0-beta2/CHANGELOG.md#0130-beta2-june-17-2020)されたのですこし遊んでみようと思います。
v0.12系との違いや`0.13upgrade`コマンドの実行による差分の検証などをやっていこうと思います。

# インストール
わたしは`tfenv`を使っているのでtfenv経由でインストールします。
[Dockerイメージ](https://hub.docker.com/layers/hashicorp/terraform/0.13.0-beta2/images/sha256-b372c6261d46f54469bbd737c24ffec1e6d4fdf7a94c1e76b2baa56d1a613063?context=explore)も既にきているのでDocker利用されている方はご利用ください。

```bash:install
$ tfenv list-remote | grep 0.13
0.13.0-beta2
0.13.0-beta1
$ 
$ 
$ tfenv install 0.13.0-beta2
[INFO] Installing Terraform v0.13.0-beta2
[INFO] Downloading release tarball from https://releases.hashicorp.com/terraform/0.13.0-beta2/terraform_0.13.0-beta2_darwin_amd64.zip
#################################################################################################################################################### 100.0%
[INFO] Downloading SHA hash file from https://releases.hashicorp.com/terraform/0.13.0-beta2/terraform_0.13.0-beta2_SHA256SUMS
tfenv: tfenv-install: [WARN] No keybase install found, skipping OpenPGP signature verification
Archive:  tfenv_download.C6RPXq/terraform_0.13.0-beta2_darwin_amd64.zip
  inflating: /usr/local/Cellar/tfenv/1.0.2/versions/0.13.0-beta2/terraform  
[INFO] Installation of terraform v0.13.0-beta2 successful
[INFO] Switching to v0.13.0-beta2
Error parsing command-line flags: flag provided but not defined: -version

tfenv: tfenv-use: [ERROR] 'terraform --version' failed. Something is seriously wrong
```
おや？なんかエラーが出ていますね。
どうやらv0.13系から`-version`の[オプションが無効](https://github.com/hashicorp/terraform/issues/25304)になり`version`(ハイフン抜き)になったようですね。

```bash:versions
$ tfenv list
  0.13.0-beta2
* 0.12.26 (set by /usr/local/Cellar/tfenv/1.0.2/version)
  0.12.17
$ 
$ terraform --version
Terraform v0.12.26
$ 
$ 
$ tfenv use 0.13.0-beta2
[INFO] Switching to v0.13.0-beta2
Error parsing command-line flags: flag provided but not defined: -version

tfenv: tfenv-use: [ERROR] 'terraform --version' failed. Something is seriously wrong
$ 
$ 
$ tfenv list
* 0.13.0-beta2 (set by /usr/local/Cellar/tfenv/1.0.2/version)
  0.12.26
  0.12.17
$ 
$ 
$ terraform version
Terraform v0.13.0-beta2
```
[tfenv側で未対応](https://github.com/tfutils/tfenv/blob/4f1ef20dd98caa9fd6da60ee2667404b6f2de352/libexec/tfenv-use#L102)のためエラーになるみたいですね。
ちなみに`-json`オプションが[追加された](https://github.com/hashicorp/terraform/pull/25252)のでこんな感じで出力することもできます。

```bash:option&nbsp;json
$ terraform version -json
{
  "terraform_version": "0.13.0-beta2",
  "terraform_revision": "",
  "provider_selections": {},
  "terraform_outdated": false
}
```
切り替えはできたので進めていきます。

# 変更点
v0.13系で個人的に大きな変更点は下記です。

 - **Automatic installation of third-party providers**
   - コミュニティプロバイダーも名前解決を実施してくれる
 - **depends_on for modules**
   - `module`ブロックでも`depends_on`が利用できる
 - **count and for_each for modules**
   - `module`ブロックでも`count`や`for_each`が利用できる

あとはv0.12系へのアップグレード同様に`terraform 0.13upgrade`コマンドも準備されているのでそれによる変更点などみていこうと思います。
わたしは利用する予定が無いのですが、バックエンドとしてS3を利用する際に`af-south-1`アフリカ(ケープタウン)リージョンが[利用できるようになった](https://github.com/hashicorp/terraform/pull/24744)みたいです。

# 0.13upgradeコマンドの実行
さくっとやってみました。

```bash:terraform&nbsp;0.13upgrade
$ 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.

$ 
$ 
$ git status
On branch master
Your branch is up to date with 'origin/master'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        versions.tf

nothing added to commit but untracked files present (use "git add" to track)
```
`versions.tf`が出力されました。

```go:versions.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
  required_version = ">= 0.13"
}
```
こっちでTerraformのバージョン管理が推奨なんですかね？
ここに[コミュニティプロバイダー](https://registry.terraform.io/browse/providers?tier=community)は追加してあげると名前空間で解決してインストールできるみたいです！
なので今後はこういう書き方をしていく方がよさそうですね。

# やってみる
では上で挙げたものを試していきます。

## コミュニティプロバイダーをインストールしてみる
プロバイダーのレジストリは[ここから](https://registry.terraform.io/browse/providers)確認できます。

<img width="1305" alt="sc.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/3ea81e91-15ff-4be4-f91b-592d6965f482.png">
赤枠の**USE PROVIDER**をクリックすると記述方法が出てくるので先ほどの`0.13upgrade`コマンドで生成された`versions.tf`に記述を追加します。

```go:versions.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
    sops = {
      source = "carlpett/sops"
      version = "0.5.1"
    }
  }
  required_version = ">= 0.13"
}
```
また`config.tf`にもプロバイダーつかうで、という宣言が必要なので同様に追加してあげます。

```go:config.tf
provider "aws" {
  region  = "us-east-1"
  version = "~> 2.67.0"
}

provider "sops" {}  // ここ

data "aws_caller_identity" "self" {}
```
ではinitしていきます。

```bash:terraform&nbsp;init
$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 2.67.0"...
- Finding carlpett/sops versions matching "0.5.1"...
- Installing hashicorp/aws v2.67.0...
- Installed hashicorp/aws v2.67.0 (signed by HashiCorp)
- Installing carlpett/sops v0.5.1...
- Installed carlpett/sops v0.5.1 (self-signed, key ID 1468AC14E6819667)

Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/plugins/signing.html

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.
```
なんかよさげ。
ディレクトリをみてみましょう。

```bash:tree
$ tree .terraform
.terraform
└── plugins
    ├── registry.terraform.io
    │   ├── carlpett
    │   │   └── sops
    │   │       └── 0.5.1
    │   │           └── darwin_amd64
    │   │               └── terraform-provider-sops_v0.5.1
    │   └── hashicorp
    │       └── aws
    │           └── 2.67.0
    │               └── darwin_amd64
    │                   └── terraform-provider-aws_v2.67.0_x4
    └── selections.json

10 directories, 3 files
```
おお！それっぽいのがきてる！！！

使えるのかさくっとやってみます。
sops用のkmsキーを作成します。

```go:kms
resource "aws_kms_key" "sops" {
  description             = "For sops key"
  enable_key_rotation     = true
  deletion_window_in_days = 7
}

resource "aws_kms_alias" "sops" {
  target_key_id = aws_kms_key.sops.id
  name          = "alias/sopsKey"
}

output "sops_key" {
  value = aws_kms_key.sops.arn
}
```
これを利用して暗号化されたymlファイルを作成します。

```bash:create&nbsp;credential.yml
$ export SOPS_KMS_ARN="arn:aws:kms:us-east-1:0123456789012:key/beddd63b-4b91-49f4-a865-3d668359cb28"
$ 
$ sops credential.yml
$ 
$ sops -d credential.yml
demo_credential: IyHQTp3CHsDs+AFG
$ 
$ ls -l credential.yml
-rw-r--r--  1 tetsu  staff  1013  6 21 08:56 credential.yml
```
作成後下記を`config.tf`に追記してもう一度`terraform init`を実行します。

```go:config.tf
data "sops_file" "demo_credential" {
  source_file = "credential.yml"
}
```
これで準備はできたのでパラメータストアにクレデンシャルを保存してみます。

```go:main.tf
resource "aws_ssm_parameter" "demo_credential" {
  name   = "demo-credential"
  type   = "SecureString"
  key_id = "alias/aws/ssm"
  value  = data.sops_file.demo_credential.data["demo_credential"]
}
```
<details><summary>plan && apply</summary><div>

```go
$ terraform plan -out plan.out
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.

data.sops_file.demo_credential: Refreshing state...
data.aws_caller_identity.self: Refreshing state... [id=2020-06-20 23:52:16.146895 +0000 UTC]
aws_kms_key.sops: Refreshing state... [id=beddd63b-4b91-49f4-a865-3d668359cb28]
aws_kms_alias.sops: Refreshing state... [id=alias/sopsKey]

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

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_ssm_parameter.demo_credential will be created
  + resource "aws_ssm_parameter" "demo_credential" {
      + arn     = (known after apply)
      + id      = (known after apply)
      + key_id  = "alias/aws/ssm"
      + name    = "demo-credential"
      + tier    = "Standard"
      + type    = "SecureString"
      + value   = (sensitive value)
      + version = (known after apply)
    }

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

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

This plan was saved to: plan.out

To perform exactly these actions, run the following command to apply:
    terraform apply "plan.out"

$ 
$ 
$ terraform apply "plan.out"
aws_ssm_parameter.demo_credential: Creating...
aws_ssm_parameter.demo_credential: Creation complete after 3s [id=demo-credential]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

Outputs:

sops_key = arn:aws:kms:us-east-1:0123456789012:key/beddd63b-4b91-49f4-a865-3d668359cb28
```

</div></details>

無事に登録できていました。ヤッタネ！
![sc 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423088/77ff2369-1747-9353-de85-f206e83473ff.png)


## moduleでcountを使ってみる
`count`を利用しつつ複数のユーザーを作成してみます。
下記のmoduleを参照させて作っていきます。

```go:modules/user/main.tf
resource "aws_iam_user" "default" {
  name          = var.name
  force_destroy = false

  tags = {
    Group = var.iam_group
  }
}
```
ルートディレクトリにある`main.tf`にて上記を参照するようなmoduleを記述し、その中でcountを利用してみます。

```go:main.tf
module "count_user" {
  count  = 2
  source = "./modules/user"

  name      = "hoge-${format("%02d", count.index + 1)}"
  iam_group = "hogehoge"
}
```
こんな感じですね。特に工夫はないです。
`terraform plan`するとこんな感じです。

```go
  # module.count_user[0].aws_iam_user.default will be created
  + resource "aws_iam_user" "default" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "hoge-01"
      + path          = "/"
      + tags          = {
          + "Group" = "hogehoge"
        }
      + unique_id     = (known after apply)
    }

  # module.count_user[1].aws_iam_user.default will be created
  + resource "aws_iam_user" "default" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "hoge-02"
      + path          = "/"
      + tags          = {
          + "Group" = "hogehoge"
        }
      + unique_id     = (known after apply)
    }
```
ちゃんとcountが効いてindexが追加されています。

## moduleでfor_eachを使ってみる
上記と同様のmoduleを参照して`for_each`でユーザーを作成するようなものを記述します。

```go:main.tf
locals {
  forEach = {
    demo01 = "fuga"
    demo02 = "piyo"
  }
}

module "for_each_user" {
  for_each = local.forEach
  source   = "./modules/user"

  name      = each.value
  iam_group = "fugapiyo"
}
```
同様に`terraform plan`を投げてみます。

```go
  # module.for_each_user["demo01"].aws_iam_user.default will be created
  + resource "aws_iam_user" "default" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "fuga"
      + path          = "/"
      + tags          = {
          + "Group" = "fugapiyo"
        }
      + unique_id     = (known after apply)
    }

  # module.for_each_user["demo02"].aws_iam_user.default will be created
  + resource "aws_iam_user" "default" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "piyo"
      + path          = "/"
      + tags          = {
          + "Group" = "fugapiyo"
        }
      + unique_id     = (known after apply)
    }
```
ちゃんと`key:value`が出力されているのがわかります。

## moduleでdepends_onを使ってみる
これは書かなくても解決してくれるので蛇足になるのですが、一応書いておきましょう。
先ほどのユーザー作成に対して先行してグループが作られ、かつtag等にmoduleから値が付与できるように書き直しました。

```go:main.tf
// IAMユーザー作成
module "count_user" {
  count  = 2
  source = "./modules/user"

  name      = "hoge-${format("%02d", count.index + 1)}"
  iam_group = module.count_group.group_name

  depends_on = [module.count_group.group_name]
}

module "for_each_user" {
  for_each = local.forEach
  source   = "./modules/user"

  name      = each.value
  iam_group = module.for_each_group.group_name

  depends_on = [module.for_each_group.group_name]
}

// IAMグループの作成
module "count_group" {
  source = "./modules/group"

  group      = "hogehoge"

  policy_arns = {
    default = module.gengeral_policy.default_policy
  }
}

module "for_each_group" {
  source = "./modules/group"

  group      = "fugapiyo"

  policy_arns = {
    default = module.gengeral_policy.default_policy
  }
}
```

## デプロイしてみる
一通り書き終わりましたのでひとまずv0.12系で確認してみます。

```go:v0.12.26&nbsp;plan
$ terraform -v
Terraform v0.12.26
$ 
$ 
$ terraform plan

Error: Reserved argument name in module block

  on main.tf line 10, in module "count_user":
  10:   count  = 2

The name "count" is reserved for use in a future version of Terraform.


Error: Reserved argument name in module block

  on main.tf line 16, in module "count_user":
  16:   depends_on = [module.count_group.group_name]

The name "depends_on" is reserved for use in a future version of Terraform.


Error: Reserved argument name in module block

  on main.tf line 20, in module "for_each_user":
  20:   for_each = local.forEach

The name "for_each" is reserved for use in a future version of Terraform.


Error: Reserved argument name in module block

  on main.tf line 26, in module "for_each_user":
  26:   depends_on = [module.for_each_group.group_name]

The name "depends_on" is reserved for use in a future version of Terraform.
```
Terraformによる予約語だよ？と怒られてしまいましたね。
ではv0.13系だとどうでしょうか？

```go:v0.13.0-beta2&nbsp;plan
$ terraform version
Terraform v0.13.0-beta2
+ provider registry.terraform.io/hashicorp/aws v2.67.0
$ 
$ 
$ 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.

data.aws_caller_identity.self: Refreshing state...
module.gengeral_policy.data.aws_iam_policy_document.basic_policy: Refreshing state...

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

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

=======================省略=======================

Plan: 9 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.
```
先ほどv0.12系で出ていたエラーもなくなり、問題なさそうなので`terraform apply`してみましょう。

```go:terraform&nbsp;apply
$ terraform apply "plan.out"
module.gengeral_policy.data.aws_iam_policy_document.basic_policy: Reading... [id=296661728]
module.gengeral_policy.data.aws_iam_policy_document.basic_policy: Read complete after 0s [id=296661728]
module.for_each_group.aws_iam_group.default: Creating...
module.count_group.aws_iam_group.default: Creating...
module.gengeral_policy.aws_iam_policy.default: Creating...
module.for_each_group.aws_iam_group.default: Creation complete after 1s [id=fugapiyo]
module.count_group.aws_iam_group.default: Creation complete after 1s [id=hogehoge]
module.for_each_user["demo02"].aws_iam_user.default: Creating...
module.count_user[1].aws_iam_user.default: Creating...
module.for_each_user["demo01"].aws_iam_user.default: Creating...
module.count_user[0].aws_iam_user.default: Creating...
module.for_each_user["demo01"].aws_iam_user.default: Creation complete after 2s [id=fuga]
module.gengeral_policy.aws_iam_policy.default: Creation complete after 3s [id=arn:aws:iam::881745222256:policy/basicPolicy]
module.count_group.aws_iam_group_policy_attachment.default["default"]: Creating...
module.for_each_group.aws_iam_group_policy_attachment.default["default"]: Creating...
module.count_user[0].aws_iam_user.default: Creation complete after 2s [id=hoge-01]
module.for_each_user["demo02"].aws_iam_user.default: Creation complete after 2s [id=piyo]
module.count_user[1].aws_iam_user.default: Creation complete after 2s [id=hoge-02]
module.count_group.aws_iam_group_policy_attachment.default["default"]: Creation complete after 3s [id=hogehoge-20200620131313923200000001]
module.for_each_group.aws_iam_group_policy_attachment.default["default"]: Creation complete after 3s [id=fugapiyo-20200620131313943400000002]
```
グループが先行して作られその後ユーザーが作成されているのがわかります。
このようにmoduleでも`count`や`for_each`といった繰り返し処理が可能になり、依存関係をこちら側で解決するようにできる`depends_on`が利用できるようになりました。

# 所感
特にこれといって難しさはないので、moduleでも使えるようになったんだなあ〜くらいでよいと思います。
つまづきポイントとしては`output`して利用する際の書き方は少し注意が必要かなと思いました。
個人的にはコミュニティプロバイダーが名前解決できるようなってくれたのでDXがめっちゃ上がりました。
あまり使い過ぎても可読性が下がるので見極めつつ利用していけばよいかと思います