【Terraform】can関数を利用して汎用的なコードを書く

# 1. はじめに
[前回の記事](https://qiita.com/risa_someya/items/b060eaab381a5ec60660)でTerraformのcan関数について軽く触れていたので、今回はその紹介のための記事を投稿しようと思います。
使ってみた感想としては、汎用的なコードの書き方ができそうだなと思いました。

# 2. can関数について
公式ドキュメント：https://developer.hashicorp.com/terraform/language/functions/can

can関数で指定した式に対してboolに変換して値を返してくれるような関数になります。
公式ドキュメントから以下の例を参照します。
```
> local.foo
{
  "bar" = "baz"
}
> can(local.foo.bar)
true
> can(local.foo.boop)
false
```
上記の例の場合、```local.foo```では```bar```という変数が定義されているため
```can(local.foo.bar)```では正しく値を取得できるので返り値が```true```となります。
反対に```can(local.foo.boop)```とすると、```boop```変数というのは定義されておらず、
正しく値を取得できずエラーとなるため、```false```が返ってきます。

今回はこちらの関数を活用して、コードの作成を行っていきます。

# 3. 構成
## 3-1. 作成するリソース
S3バケットを2つ作成して、それぞれ別のキー値を持つタグの設定をしていこうと思います。

AのS3バケット
・Name：temp-bucket-a
・**TempA：true**

BのS3バケット
・Name：temp-bucket-b
・**TempB：true**

## 3-2. フォルダ構成
以下のようなフォルダ構成で実装していきます。
リソース名などは変数管理としたいため、Local Valuesを利用します。
参考：[Local Values](https://developer.hashicorp.com/terraform/language/values/locals)
```
Terraform/
└─ dev
    ├─ s3.tf
    ├─ locals.tf
    └─ terraform.tf
```

# 4. 実装
## 4-1. terraform.tfとlocals.tf
can関数を利用することでどのくらいコードに差が出るのかを比較したいので、2通りのコードを紹介します。
```terraform.tf```と```locals.tf```は以下のコードを共通で利用します。
```terraform.tf
# providerの定義
provider "aws" {
  region = "ap-northeast-1"
}

terraform {
  required_version = ">=1.0"
  required_providers {
    awscc = {
      source  = "hashicorp/awscc"
      version = "0.45.0"
    }
  }
}

# 実行するAWSアカウント情報取得用
data "aws_caller_identity" "self" {}

# 実行するリージョン情報取得用
data "aws_region" "self" {}
```
```locals.tf
locals {
    s3_bucket = {
        temp-bucket-a = {
            tags = {
                name = "temp-bucket-a"
                temp-a = "true"
            }
        }
        temp-bucket-b = {
            tags = {
                name = "temp-bucket-b"
                temp-b = "true"
            }
        }
    }
}
```

## 4-2. can関数を使わない場合

キーの異なるタグを付与する場合は、resourceを2つに分けて作成する必要があるかと思います。
```s3.tf
resource "aws_s3_bucket" "temp-a" {
    for_each = local.s3_bucket.temp-bucket-a
    bucket = "temp-bucket-a-${data.aws_caller_identity.self.account_id}"
    tags = {
        Name = each.value.name
        TempA = each.value.temp-a
    }
}

resource "aws_s3_bucket" "temp-b" {
    for_each = local.s3_bucket.temp-bucket-b
    bucket = "temp-bucket-b-${data.aws_caller_identity.self.account_id}"
    tags = {
        Name = each.value.name
        TempB = each.value.temp-b
    }
}
```
上記の書き方でも問題なく実行はできますが、ほとんど同じコードを複数書く必要があるのであまりスマートなコードとは言えないような気がします。
そこでcan関数の出番です。

## 4-3. can関数を使う場合
can関数を使うことで、タグのキーが異なっていても```resource "aws_s3_bucket"```を一つ書いておけば、複数のバケットが作成できるようになります。
以下のようにコードを書き換えていきます。

```s3.tf
resource "aws_s3_bucket" "this" {
    for_each = local.s3_bucket
    bucket = "${each.key}-${data.aws_caller_identity.self.account_id}"
    tags = {
        Name = each.value.tags.name
        TempA = can(each.value.tags.temp-a) ? each.value.tags.temp-a : null
        TempB = can(each.value.tags.temp-b) ? each.value.tags.temp-b : null
    }
}
```
```resource "aws_s3_bucket"```の記載が一つで済み、行数が格段に少なくなりました。
can関数を利用している箇所について、以下の通り簡単に解説します。
```
TempA = can(each.value.tags.temp-a) ? each.value.tags.temp-a : null
TempB = can(each.value.tags.temp-b) ? each.value.tags.temp-b : null
```
Local ValuesにTempAもしくはTempBのキーが存在するかどうかをcan関数の部分で確認しています。
AのS3バケットでは```temp-a```という変数を定義しているのでその値を利用し、```temp-b```という変数は定義していないため```null```を使用します。
BのS3バケットについてはAと逆の結果となります。

## 4-4. AWSマネジメントコンソールに展開
4-3のコードで実際に```terraform apply```してみます。
```
PS C:\terraform\dev\s3_qiita> terraform apply
data.aws_region.self: Reading...
data.aws_caller_identity.self: Reading...
data.aws_region.self: Read complete after 0s [id=ap-northeast-1]
data.aws_caller_identity.self: Read complete after 0s [id={accountid}]

Terraform used the selected providers to generate the following execution plan. Resource actions are  
indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.this["temp-bucket-a"] will be created
  + resource "aws_s3_bucket" "this" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "temp-bucket-a-{accountid}"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags                        = {
          + "Name"  = "temp-bucket-a"
          + "TempA" = "true"
        }
      + tags_all                    = {
          + "Name"  = "temp-bucket-a"
          + "TempA" = "true"
        }
~~~中略~~~

  # aws_s3_bucket.this["temp-bucket-b"] will be created
  + resource "aws_s3_bucket" "this" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "temp-bucket-b-{accountid}"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags                        = {
          + "Name"  = "temp-bucket-b"
          + "TempB" = "true"
        }
      + tags_all                    = {
          + "Name"  = "temp-bucket-b"
          + "TempB" = "true"
        }
~~~中略~~~

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

aws_s3_bucket.this["temp-bucket-b"]: Creating...
aws_s3_bucket.this["temp-bucket-a"]: Creating...
aws_s3_bucket.this["temp-bucket-b"]: Creation complete after 1s [id=temp-bucket-b-{accountid}]
aws_s3_bucket.this["temp-bucket-a"]: Creation complete after 1s [id=temp-bucket-a-{accountid}]

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

問題なくコマンドが完了しました。
AWSコンソールの方も見てみましょう。
![キャプチャ01.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/aaa59064-92c5-49b3-6abf-3d50bdc41f21.jpeg)
Aのバケット
![キャプチャ02.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/a986916d-ba45-4ef1-b0f6-bce0929156bd.jpeg)
Bのバケット
![キャプチャ03.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629857/03798a33-1698-852d-96f9-3c97790a3b85.jpeg)

バケットが2つ作成され、タグもTerraformで設定した通りにそれぞれのバケットごとにキー値が設定できていることが確認できました。

# 5. おわりに
今回はcan関数を利用してコーディングする方法について記事にしてみました。
コードの内容によって出力結果を分岐させる方法はcan関数以外にもたくさんあると思うので、もしよりよい方法などあれば教えていただければと思います。
1つの方法としてこちらの記事が参考になれば幸いです。