Programatic Terraform(新: 実践Terraform)を読んでみた


## 概要

インフラの勉強がてら、[Programatic Terraform on AWS](https://booth.pm/ja/items/1318735)を読んでみたので、感想やエラーの対処などをまとめておこうと思います。

「Terraform 入門」とかで調べるとこちらの書籍の評判がものすごく良いことがわかります。

ただ、「実践Terraform」という商業誌版があったんですね...！😂

[実践Terraform](https://www.amazon.co.jp/%E5%AE%9F%E8%B7%B5Terraform-AWS%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E8%A8%AD%E8%A8%88%E3%81%A8%E3%83%99%E3%82%B9%E3%83%88%E3%83%97%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%82%B9-%E6%8A%80%E8%A1%93%E3%81%AE%E6%B3%89%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA%EF%BC%88NextPublishing%EF%BC%89-%E9%87%8E%E6%9D%91-%E5%8F%8B%E8%A6%8F/dp/4844378139)

同人誌版と比べて追加された章もあるようなので、新規に購入される方にはこちらがおすすめです。


## 全体的な感想

わかりづらいところがありません😌

Terraformのファイル・コマンド・文法など基本的なところの説明から始まって、AWSリソースの作成に移り、TerraformやAWSのベストプラクティスにまで紹介されています。

リソースの作成においても、各サービスを段階的に積み上げながら作成していくので、つまづくことがありませんでした。

また、各サービスの設計に関してもわかりやすく説明されており、AWS自体の勉強になります。

注意点としては、途中からALB・ECS(Fargate)などで構成された全体的なシステムを構築していくのですが、こちらはできるだけ細かくデプロイしていった方が良さそうです（依存関係もあるので全部そうできるかはわかりませんが）

私は全てが終わってからまとめてデプロイしようとしてエラーになり、前の箇所にもどって「あれ、これなんだっけ？」と忘れるようなところが多かったからです。

一つ一つ小さくデプロイしながら、動作確認・リソースの削除を繰り返して行った方が、エラーにつまづくのが早いため、手戻りが少なそうです。

## Terraformに関する感想

>逆にAWSある程度触ったり、資格の勉強で包括的に知識を入れていたりすると、復習にもなるし
「GUIでぽちぽちやってよくわかってなかったけど、裏ではこんなリソースが出来ているのか」
と整理もできる内容になっています。
[「Pragmatic Terraform on AWS」が神本だったので紹介する](https://qiita.com/fukubaka0825/items/53cd168730815a33f51f)

私はまさにこれで、コンソールでぽちぽちやっていても、細かい設定項目などがよくわかっていなかったんですよね。

それが全て均一にコード化されるので、学習にも良いし、何よりリソースを把握しやすいです。

そして、インターフェイスというか記法がシンプルで、デプロイは`terraform apply`・削除は `terraform remove`のコマンド一発で済んでしまうのもすごいですね。。。

## 遭遇したエラーなど

#### 環境変数の参照

```tf:これじゃだめ
variable my_domain {
  type = "string"
  default = "${MY_DOMAIN}"
}
```

最初「こんな感じかな？」と適当に上のようにやったらダメで

```tf:OK
variable my_domain {
}
```

変数の箱を用意した上で、環境変数として`TF_VAR_`で始まる変数名をセットすると、その値を参照してくれるようでした。

```
$ export TF_VAR_my_domain=example.com
```

[TF_VAR_name](https://www.terraform.io/cli/config/environment-variables#tf_var_name)

ちなみに、変数の値はコマンドラインでは`-var`オプション、他にも`terraform.tfvars`という専用のファイルで値を上書きできるようですが、優先順位としては環境変数が一番低いようです。

>Terraform loads variables in the following order, with later sources taking precedence over earlier ones:
・Environment variables
・The terraform.tfvars file, if present.
・The terraform.tfvars.json file, if present.
・Any *.auto.tfvars or *.auto.tfvars.json files, processed in lexical order of their filenames.
Any -var and -var-file options on the command line, in the order they are provided. (This includes variables set by a Terraform Cloud workspace.)
[Variable Definition Precedence](https://www.terraform.io/language/values/variables#variable-definition-precedence)

#### S3関係の設定方法の変更

```
Error: Value for unconfigurable attribute
│ 
│   with aws_s3_bucket.private,
│   on main.tf line 28, in resource "aws_s3_bucket" "private":
│   28: resource "aws_s3_bucket" "private" {
│ 
│ Can't configure a value for "server_side_encryption_configuration": its value will be decided automatically based on the result of applying this configuration.
```

```tf:該当部分
resource "aws_s3_bucket" "private" {
  bucket = "private-pragmatic-terraform-on-aws-mk-private"

  versioning {
    enabled = true
  }

  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        sse_algorithm = "AES256"
      }
    }
  }
}
```

>要は「**`aws_s3_bucket`**がデカくなりすぎて大変だから、細かく分けようぜ！」ということです。
御存知の通りS3は非常に多機能です。そしてTerraformではその機能の殆どを**`aws_s3_bucket`**のattributeで設定していました。
v4では各Attribute毎にresourceが用意されているので、こちらを使います。
[Terraform AWS Provider Version 4がリリースされました](https://dev.classmethod.jp/articles/terraform-aws-provider-version-4/)

最近(2022/2)のアップデート(v3→v4)でS3関係の定義の仕方が変わったらしく、 `aws_s3_bucket` リソースの属性として定義していたものを、別途独立したリソースとして定義する必要があるようです。


```tf:修正後
resource "aws_s3_bucket" "private" {
  bucket = "private-pragmatic-terraform-on-aws-mk-private"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "private" {
  bucket = aws_s3_bucket.private.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}
```

他にも、`Can't configure a value for`と出てきたらこれを疑っても良いかもしれません。


#### ALBのアクセスログを保存するS3バケットの設定

```
Error: failure configuring LB attributes: InvalidConfigurationRequest: Access Denied for bucket: alb-log-pragmatic-terraform-on-aws-mk. Please check S3bucket permission
│       status code: 400, request id: f99bb29a-e2c2-4c0e-b08a-0a822179c108
│ 
│   with aws_lb.example,
│   on main.tf line 66, in resource "aws_lb" "example":
│   66: resource "aws_lb" "example" {
```


```tf:関連箇所
variable aws_account_id {}
...
resource "aws_s3_bucket_policy" "alb_log" {
  bucket = aws_s3_bucket.alb_log.id
  policy = data.aws_iam_policy_document.alb_log.json
}

data "aws_iam_policy_document" "alb_log" {
  statement {
    effect    = "Allow"
    actions   = ["s3:PutObject"]
    resources = ["arn:aws:s3:::${aws_s3_bucket.alb_log.id}/*"]

    principals {
      type        = "AWS"
      identifiers = [var.aws_account_id]　→この値を自分アカウントIDにしてしまっていた
    }
  }
}

...

resource "aws_lb" "example" {
  name                       = "example"
  load_balancer_type         = "application"
  internal                   = false
  idle_timeout               = 60
  enable_deletion_protection = false

  subnets = [
    aws_subnet.public_0.id,
    aws_subnet.public_1.id,
  ]

  access_logs {
    bucket  = aws_s3_bucket.alb_log.id
    enabled = true
  }

  security_groups = [
    module.http_sg.security_group_id,
    module.https_sg.security_group_id,
    module.http_redirect_sg.security_group_id,
  ]
}

```

>ロードバランサーのリージョンに対応するAWSアカウントIDとは、要するに、S3のバケットポリシーで使用するアカウントIDのことのようです。
AWSアカウントを作成時に割り当てられるアカウントIDとは別物です。
執筆時点で、東京リージョンのアカウントIDは、582318560864でした。
[ALBのアクセスログをS3バケットへ保存するように設定して、terraform実行すると権限エラー](https://zenn.dev/program_panda/articles/e5640ebe90b5c9)

>ALBの場合は、AWSが管理しているアカウントから書き込みます。そこで、14行目で書き込みを行うアカウントID（582318560864）を指定しています。なお、このアカウントIDはリージョンごとに異なります*2。
Programatic Terraform p.32

本文にも書いてありましたが、こちらはリージョンごとの固定値なんですね。

自分のアカウントIDかと思って変数に書き出しましたが、固定値として値をそのまま書き込んでしまって良さそうです。

```terraform
data "aws_iam_policy_document" "alb_log" {
  statement {
    effect    = "Allow"
    actions   = ["s3:PutObject"]
    resources = ["arn:aws:s3:::${aws_s3_bucket.alb_log.id}/*"]

    principals {
      type        = "AWS"
      identifiers = ["582318560864"]　→この値を自分アカウントIDにしてしまっていた
    }
  }
}
```


#### ACMの`domain_validation_options`のデータ型が変更

```terraform
Error: Invalid index
│ 
│   on main.tf line 321, in resource "aws_route53_record" "example_certificate":
│  321:   name    = aws_acm_certificate.example.domain_validation_options[0].resource_record_name
│ 
│ Elements of a set are identified only by their value and don't have any separate index or key to select with, so it's only possible to perform operations across all elements of the set.
```

```terraform
resource "aws_route53_record" "example_certificate" {
  name    = aws_acm_certificate.example.domain_validation_options[0].resource_record_name
  type    = aws_acm_certificate.example.domain_validation_options[0].resource_record_type
  records = [aws_acm_certificate.example.domain_validation_options[0].resource_record_value]
  zone_id = data.aws_route53_zone.example.id
  ttl     = 60
}
```

>しかし、最新版のawsプロバイダーではList型からSet型へ変わってしまうことで、以下のように差分が発生します。
[TerraformのバージョンアップでInvalid indexが出た時の対処](TerraformのバージョンアップでInvalid indexが出た時の対処)

こちらもTerraformのバージョンアップ時の変更に伴うようですね。
確かに、公式を見ると、データ型が`Set`になっています。

>Set of domain validation objects which can be used to complete certificate validation. Can have more than one element, e.g., if SANs are defined. Only set if **`DNS`**-validation was used.
[domain_validation_options](https://registry.terraform.io/providers/hashicorp%20%20/aws/latest/docs/resources/acm_certificate#domain_validation_options)

確かに`Set`型のようです。

同じページに`Set`型でループを回して、`domain_validation_options`の要素数分`resource`を作成するサンプルが載っていました。

```terraform:修正後
resource "aws_route53_record" "example_certificate" {
  for_each = {
    for dvo in aws_acm_certificate.example.domain_validation_options : dvo.domain_name => {
      name    = dvo.resource_record_name
      record  = dvo.resource_record_value
      type    = dvo.resource_record_type
      zone_id = data.aws_route53_zone.example.id
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = each.value.zone_id
}
```

以下の参考リンク先を見ていただけるとわかるのですが、`Set`型を`Map`型に変換して`for_each`を併用することで、`domain_validation_options`の要素の分だけACM検証用のレコードを作成しています。

>for_eachはresourceやmoduleでしか書けず、イメージとしてはresourceブロックごと繰り返すという感じになります。
`each.key` や `each.value` でkey-valueを参照でき、setの場合はどちらも同じ値になります。
[Terraformでのloop処理の書き方（for, for_each, count）](https://zenn.dev/wim/articles/terraform_loop#for%E3%81%A8for_each(%E3%81%A8count))

>listからmapを生成することもできます。
[Terraformでのloop処理の書き方（for, for_each, count）
](https://zenn.dev/wim/articles/terraform_loop#list%E3%82%92for%E3%81%A7%E5%9B%9E%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B)

>map
連想配列です。他の言語でいうところの、Hash, HashMap ですね。
[AWS Terraform 基本コード まとめ](https://takelg.com/terraform_basic_code/)

##　終わりに

リソースを作成したら、削除することを忘れないようにしましょう。
こちらの本はまったく関係ないのですが、試しに作ってみたリソースでだいぶ費用が発生してしまったことがありました😅

※本書にも記載がありますが、ALBやRDSを削除する場合は、`deletion_protection`を`false`(ついでに`skip_final_snapshot`も必要に応じて)に変更した上で、一旦`terraform apply`する必要があります。設定変更を反映した上で、`terraform destroy`する必要があるということですね。