実践Terraformを読んでハマったこと

[実践Terraform　AWSにおけるシステム設計とベストプラクティス](https://nextpublishing.jp/book/10983.html)のコードを写経してみました。

感想は[こちら](https://qiita.com/1021ky@github/items/c9ac37fcda40ea8f03c6)
学びになったことは[こちら](https://qiita.com/1021ky@github/items/a1998c88e2a79f08fd28)

今回はやってみてハマったことについてです。

## 変数と文字列を混同する

Terraformにも型はあるため、そのようなミスはplan実行時に検出してもらえます。ただ、型さえ合えばplanは正常終了するため、
文字列の受け渡しについてはapplyの実行をしてはじめてエラーが発生して間違いがわかることちょこちょことありました。
以下はvpcのidを変数`aws_vpc.example.id`で参照しようとしたけど、クォートでくくってしまったために変数名の文字列が渡されてエラーになったときです。

```bash
Error: Error creating Security Group: InvalidVpcID.NotFound: The vpc ID 'aws_vpc.example.id' does not exist
        status code: 400, request id: 6f78b923-d690-42a6-b367-8fa4c71b3556

  on security_group/main.tf line 23, in resource "aws_security_group" "default":
  23: resource "aws_security_group" "default" {
```

初歩的といえば初歩的なミスなのですが、以下の例のようにTerraformではAWSで定義されている値を文字列で参照するときがあります。

```terraform
# AWSが管理しているポリシーを参照
data "aws_iam_policy" "ecs_task_execution_role_policy" {
  # ECSタスク実行IAMロールでの使用が想定されているポリシー
  arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
```

Terraformで定義されているのか、クラウドサービスで定義されているのか区別ができていないときは頻発しました。

## S3の名前はユニーク

S3バケット名は全世界でユニークでなければなりません。本に載っているサンプルコードとおりのバケット名にするとエラーになります。
ユニークにしないといけないことは本で説明がしっかりとされています。
しかし、バケット名を設定するすべてのサンプルコードに「※注意ここはユニークな値に書き換えること」とは書かれていないため、うっかりしているとエラーになります。
まあなってもそんなに大事にはならないですが、バケット名はユニークになるようにサフィックスに年月日（-2019-01-24）とかつけるとよいと思います。

## FARGATEでヘルスチェックが行われるタイミング

9章「コンテナオーケストラレーション」ではまったところです。

1節から3節ではECS上でnginxコンテナを起動します。その後9章4節でヘルスチェックのログを確認できるようにします。
ヘルスチェックはコンテナ起動時にしかされないため、ログの設定をapplyで反映してもコンテナの再起動はされません。
したがってコンテナのヘルスチェックログを確認したい場合は一度destroyしてapplyし、作り直さないとヘルスチェックログを確認できません。

## RDSインスタンス削除時のスナップショット取得設定について

13章「データストア」でRDSを削除保護して構築します。

~~ここはサンプルコード通りに書いたのですが、apply実行時に以下のエラーが出ました。~~

※2020年2月12日に修正しましたが、skip_final_snapshotの値trueをfalseを誤って書いていました。@yubeさんご指摘ありがとうございます！
2020年2月19日に再修正しました。

RDSにはインスタンス削除時にスナップショットをとるか設定できる項目`skip_final_snapshot`があります。
サンプルコードではインスタンス作成時はこの値をfalseにし、削除するときはtrueにすると書いています。
しかし、私は誤ってfalseのままdestoryを実施してしまいました。（ログを残していなかったので、推測ではありますが）

インスタンス削除時にスナップショットをとる（skip_final_snapshot = false）ならば、
合わせてfinal_snapshot_identifierの設定が必要であり、この設定をしていないまま削除（destroy）を実行すると以下のエラーがでます。

```zsh
Error: DB Instance FinalSnapshotIdentifier is required when a final snapshot is required
```

ややこしいのは、skip_final_snapshot = falseとしてfinal_snapshot_identifierを設定していないままapplyしてもエラーはでません。

|設定項目 |apply実施結果 |destroy実施結果 |
|---|---|---|
|skip_final_snapshot=false、final_snapshot_identifier設定無し|成功|エラー|
|skip_final_snapshot=false、final_snapshot_identifier設定あり|成功|成功|

となります。

GitHubでも[issue](https://github.com/terraform-providers/terraform-provider-aws/issues/4910)があがっていたのと、[公式ドキュメント](https://www.terraform.io/docs/providers/aws/r/db_instance.html#skip_final_snapshot)でもskip_final_snapshotをfalseにするならば、final_snapshot_identifierは必須と書かれていました。

著者の野村さんが教えてくださったのですが、実装がドキュメントと少し違っているようです。

> そこでAWSプロバイダの実装を確認したトコロ、「final_snapshot_identifier」は、DBインスタンス削除時のみ参照される値であることがわかりました。
https://github.com/terraform-providers/terraform-provider-aws/blob/v2.46.0/aws/resource_aws_db_instance.go#L1395-L1423

また、野村さんから以下のようなコメントもいただきました。

> final_snapshot_identifierの必要性
ご指摘の通り、skip_final_snapshotをtrueにしてDBインスタンスを削除する場合は、final_snapshot_identifierの指定が必要です。
前述のとおり、DBインスタンス「作成」時にはなくても動きますが、「削除」時にエラーメッセージが出るようです。
正直このエラーの出方はわかりづらく、本来の用途でskip_final_snapshotを指定するなら、final_snapshot_identifierも指定したほうがよいと思います。

※` skip_final_snapshotをtrue` とありますが、ここはfalseの誤りだと思われます。

スナップショットをとるならば常にセットでパラメータを指定した方が良いということですね。ご意見も聞けてありがたかったです。

> ちなみに書籍のコラム「RDSの削除」では、final_snapshot_identifierを指定するのではなく、skip_final_snapshotをfalseにするご案内をしておりました。
DBインスタンスを削除してよいケースというのは基本的に検証時だけで、検証用インスタンスのスナップショットはいらないと考えていたのでこのような記述になっていました。

この部分を見落としていました。きっちりと書かれているのに大変失礼しました。にもかかわらず、丁寧に確認と回答をしていただいてとてもありがたかったです。@tmknom さんありがとうございました。

※追記修正 はここまで

あとこのIDは英数字とハイフンのみ使われた文字列ではないとだめらしいです。

```zsh
Error: only alphanumeric characters and hyphens allowed in "final_snapshot_identifier"
```

## ECSで実行するコンテナで常時起動していなくてもいいものは明示的に設定する

ECSで実行したいコンテナの定義はcontainer_definitions.jsonで定義できます。
これは9章のサンプルコードではwebサーバーとしてnginx1つしか定義していません。

14章「デプロイメントパイプライン」でAWS CodePipelineを使い、CI/CD環境を構築したあと、
サンプルコードはなかったのですが、この環境でデプロイしたコンテナもECS上で実行させたくなってもう1つ定義したときにはまりました。

追加したコンテナはよくサンプルで使われる[hello-worldイメージ](https://hub.docker.com/_/hello-world)のものです。
このコンテナは起動して標準出力で'hello world'と出力したら終了します。以下のように定義しました。

```json
[
    {
        "name": "example_nginx",
        "image": "nginx:latest",
        "essential": true,
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-region": "ap-northeast-1",
                "awslogs-stream-prefix": "nginx",
                "awslogs-group": "/ecs/example"
            }
        },
        "portMappings": [
            {
                "protocol": "tcp",
                "containerPort": 80
            }
        ]
    },
    ***** 以下が追加したコンテナ *****
    {
        "name": "example_container",
        "image": "[アカウント名].dkr.ecr.ap-northeast-1.amazonaws.com/example:latest",
        "essential": true,
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-region": "ap-northeast-1",
                "awslogs-stream-prefix": "example_container",
                "awslogs-group": "/ecs/example"
            }
        }
    }
]
```

これでデプロイを行うと、ECSで定義した上記2つのコンテナを実行するタスクは失敗となりました。
理由は、essenntialをtrueにしていたからでした。

> コンテナの essential パラメータが true とマークされている場合、そのコンテナが何らかの理由で失敗または停止すると、タスクに含まれる他のすべてのコンテナは停止されます。

詳細コンテナ定義パラメータ 環境, https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_environment, （検索年月日：2019年1月24日

この値のデフォルトはtrueであるため、明示的にesselntial: falseと指定しないと標準出力をしたらすぐ停止してしまうhello-worldコンテナがいると、
nignxが正常に起動していたけどタスクは失敗となっていたというわけでした。
明示的に指定すると、タスクは成功しました。基本的なポリシーとして実行するコンテナは起動しっぱなしにする前提だからこういう設定になるのでしょうね。お試しでやっていたので、その前提が抜けていました。

## destroyサブコマンド実施前にコードを変更していたらapplyしましょう（2020年2月19日追記）

[RDSインスタンス削除時のスナップショット取得設定について](#RDSインスタンス削除時のスナップショット取得設定について)」でコメントをいただき、
見直しているときに気づいたことです。

まずRDSを削除保護有効、かつインスタンス削除時のスナップショットは作成する設定でapplyしてインスタンスを作成しました。

```terraform
deletion_protection        = true # 削除保護
skip_final_snapshot        = false # インスタンス削除時にスナップショット作成をする
# final_snapshot_identifier  = "example-finale-snapshot-id" # スナップショットをとるなら必須
```

次にコードでRDSの削除保護を無効にして、インスタンス削除時にスナップショット作成をしないようにして（skip_final_snapshot=true）から**applyせずにdestory**してみました。

applyして明示的にコードの変更を反映されていなくとも、destroy実行時に設定を反映してから削除処理をするのか確かめるためです。
設定が反映されるのならば、インスタンスは削除され、反映されないのならば[RDSインスタンス削除時のスナップショット取得設定について](#RDSインスタンス削除時のスナップショット取得設定について)」で触れたとおり、エラーになります。

```zsh
vaivailx@MacBook-Pro-2 practice % cat -n  rds.tf | sed -n '57,59p'
    57    deletion_protection        = false # 削除保護
    58    skip_final_snapshot        = true # インスタンス削除時にスナップショット作成をする
    59    # final_snapshot_identifier  = "example-finale-snapshot-id" # スナップショットをとるなら必須
vaivailx@MacBook-Pro-2 practice % dterraform  destroy  -target=aws_db_instance.example --auto-approve

...

aws_db_instance.example: Destroying... [id=example]

Error: DB Instance FinalSnapshotIdentifier is required when a final snapshot is required
```

→結果はエラーでした。プロバイダやリソースによって異なるかもしれませんがRDSの場合、destroy時はコードと環境に差分があれど、削除処理しかしないようです。

書籍にもRDS削除時はコードを変更してapplyしてからdestroyするように書いていました。
おそらく、今回applyし忘れていたのでサンプルコードのとおりに書いたけどエラーになって混乱したのだと思います。

設定を変更したらまずplanしてapply。絶対！これ大事！

ということがよくわかりました。

## さいごに

写経をしてみてTerraformは使いこなすと便利そうなのはたしかでした。

* コード化しているため変更内容を管理しやすい
* コード化していると、あるモジュールはどんなサービスをどう使っているのかわかりやすい。
  * たとえばCI/CD環境はECRとAWS CodePipeline, Code Buildを使って構築しているとか。

ただ、エラーがでたときにTerraformの使い方とAWSの使い方のどちらが悪いのかがわかりにくいというのは明らかでした。どちらの知識も中途半端だと、かえってデバッグの手間が多くなりそうです。
その手間が少なくなるようにどんどんコードを書いていこうと思います!!