はじめに
Terraformをチームで開発する際に、リポジトリ管理+ステートバケットを使って管理するのが良いとされているが、何も考えずにtfstateを競合させた場合に何が起きるかを確認した。
構成
もともと以下の構成をしているTerraformのリポジトリがあるとする。
.
├── 00_main.tf
└── 01_resource1.tf
terraform {
backend "s3" {
bucket = "terraform-test"
key = "test/terraform.tfstate"
region = "ap-northeast-1"
}
resource "aws_s3_bucket" "terraform_test_1" {
bucket = "terraform-test-1"
acl = "private"
}
このリポジトリで、メンバAとBが上記状態をチェックアウトした後に
- メンバAが以下のリソースをapply
resource "aws_s3_bucket" "terraform_test_2" {
bucket = "terraform-test-2"
acl = "private"
}
- その後、メンバBがリポジトリを更新しないで以下のリソースをapply
resource "aws_s3_bucket" "terraform_test_3" {
bucket = "terraform-test-3"
acl = "private"
}
としたらどうなるか。
実験結果
もしかしたらTerraformが良い感じにtfstateの更新日時とかで管理してくれるかと思ったがそんなことはなかった。
メンバAのterraform planでは当然、terraform_test_2
のバケットのaddだけができた。
Terraform will perform the following actions:
# aws_s3_bucket.terraform_test_2 will be created
+ resource "aws_s3_bucket" "terraform_test_2" {
+ acceleration_status = (known after apply)
+ acl = "private"
+ arn = (known after apply)
+ bucket = "terraform-test-2"
+ bucket_domain_name = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ force_destroy = false
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ region = (known after apply)
+ request_payer = (known after apply)
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)
+ versioning {
+ enabled = (known after apply)
+ mfa_delete = (known after apply)
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
しかし、メンバBのterraform planでは、以下のようになってしまった。
Terraform will perform the following actions:
# aws_s3_bucket.terraform_test_2 will be destroyed
- resource "aws_s3_bucket" "terraform_test_2" {
- acl = "private" -> null
- arn = "arn:aws:s3:::terraform-test-2" -> null
- bucket = "terraform-test-2" -> null
- bucket_domain_name = "terraform-test-2.s3.amazonaws.com" -> null
- bucket_regional_domain_name = "terraform-test-2.s3.ap-northeast-1.amazonaws.com" -> null
- force_destroy = false -> null
- hosted_zone_id = "Z2M4EHUR26P7ZW" -> null
- id = "terraform-test-2" -> null
- region = "ap-northeast-1" -> null
- request_payer = "BucketOwner" -> null
- versioning {
- enabled = false -> null
- mfa_delete = false -> null
}
}
# aws_s3_bucket.terraform_test_3 will be created
+ resource "aws_s3_bucket" "terraform_test_3" {
+ acceleration_status = (known after apply)
+ acl = "private"
+ arn = (known after apply)
+ bucket = "terraform-test-3"
+ bucket_domain_name = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ force_destroy = false
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ region = (known after apply)
+ request_payer = (known after apply)
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)
+ versioning {
+ enabled = (known after apply)
+ mfa_delete = (known after apply)
}
}
Plan: 1 to add, 0 to change, 1 to destroy.
うーむ、これは危険。ちゃんとterraform planの内容を確認しないと、他人の作ったリソースをあっさり消してしまう可能性があるということか……。
しかも、リソースにprevent_destroy = true
すれば消えないかと思いきや、あくまでも「prevent_destroy = trueが書かれたファイルを明示的にdestroyで消せない」だけであり、そもそも.tfファイルが存在しない場合は容赦なくdestoryの対象になってしまうようだ。
もちろん、メンバAが正しくgit push
をして、メンバBが適宜git pull
してくれればこの悲劇は回避されるが、結局、人依存の運用にしてしまうのはイケてないなぁ……。エンタープライズ版にすると、この問題は回避されるのだろうか。
結論
そうか、そもそも人力でterraform apply
するということ自体がナンセンスということか。
複数人でリポジトリ管理するのであれば、しっかりとブランチ戦略を作り、検証の済んだIaCをmasterブランチにプルリクして、然るべきメンバがレビュー承認をすることで初めて商用環境向けのIaCのデプロイパイプラインが起動して、その中で間違いがないかを確認するフローを作り上げるべきなんだ。
実践Terraform AWSにおけるシステム設計とベストプラクティスがすべてを物語っていた。
ということで、中途半端に人力でterraform applyをする環境を作ってはいけない。
せいぜい検証環境や開発環境までにしておけ、ということだ。