Terraform + GitHub Actions + ECSデプロイ自動化でハマったことまとめ
個人プロジェクトでTerraformとGitHub Actionsを初めて一緒に使うと、よくやりがちなミスがあります。
それが、remote stateを使わずに、GitHub Actions上だけで terraform apply を実行してしまうことです。
自分も今回、ECSのデプロイ環境を構築する中で、まさに同じ問題に遭遇しました。
最初の構成はかなりシンプルでした。
- GitHub ActionsでDockerイメージをビルド
- ECRへpush
-
terraform applyでECSへの反映
デプロイ自体は問題なく動いていました。
本当の問題は、その後インフラを整理しようとして terraform destroy を実行したときに始まりました。
最初は普通に動く
GitHub Actionsのworkflowは、おおよそ次のような構成でした。
- name: Terraform Apply
working-directory: ./infra
run: |
terraform init
terraform apply -auto-approve \
-var="container_image=$IMAGE_URI"
そして、Terraformのbackend設定は特にしていませんでした。
つまり、Terraform stateはデフォルトのローカル terraform.tfstate ファイルで管理されていたということです。
ここに大きな落とし穴がありました。
GitHub Actionsのrunnerは、実行されるたびに新しい一時的なVM環境で動作します。
つまり、実際には次のような流れになります。
workflow開始
→ terraform apply実行
→ terraform.tfstate作成
→ runner終了
→ terraform.tfstate削除
デプロイは成功します。
しかし、stateファイルはworkflowの実行が終わると一緒に消えてしまいます。
当時は単純に考えていました。
「AWS上にはリソースが作られているんだから、あとでdestroyすれば消せるだろう」
しかし、Terraformはそういう仕組みではありませんでした。
Terraform destroyはコードだけを見て削除するわけではない
よく誤解しがちな点ですが、terraform destroy は次のような処理ではありません。
コードに書かれているリソース名を探して削除する
Terraformはstateを基準にインフラを管理します。
例えば、次のようなコードがあるとします。
resource "aws_ecs_cluster" "main" {
name = "teamspace-cluster"
}
Terraformが実際に管理しているのは、次のような対応関係です。
aws_ecs_cluster.main
→ 実際のAWS resource ID
このマッピング情報は、すべてstateファイルの中に保存されます。
問題は、GitHub Actionsのrunnerが終了したタイミングで、そのstateも一緒に消えていたことでした。
destroy時に遭遇した問題
1. container_image variable の問題
最初に destroy を実行したとき、次のようなエラーが出ました。
No value for required variable
terraform destroy も内部的にはplanを作成するため、variableの値が必要になります。
そのため、最終的には次のようにdummy値を渡す必要がありました。
terraform destroy -var="container_image=dummy"
削除するだけなのにDockerイメージURIを要求されるのは、なかなか不思議な状況でした。
2. Cloudflare provider の認証問題
次に、Cloudflare providerでエラーが発生しました。
must provide exactly one of "api_key", "api_token"
GitHub Actions上ではsecret経由で問題なく動いていた値でしたが、ローカル環境にはその環境変数が存在していませんでした。
最終的に、provider設定を直接修正しました。
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
そして、destroy 実行時にvariableとして直接渡しました。
terraform destroy \
-var="cloudflare_api_token=..."
3. AWS provider のcredential問題
その次は、AWS providerの問題でした。
No valid credential sources found
少しややこしかったのは、AWS CLI自体は正常に動作していたことです。
aws sts get-caller-identity
このコマンドも問題なく実行できていました。
しかし、Terraform providerはまた別のcredential chainを使います。
Windows CMD、PowerShell、GitHub Actions、Terraform Cloud、AWS SDKの環境がそれぞれ微妙に違うため、credential周りがだんだん混乱していきました。
最終的にTerraformがEC2 metadata endpointまで見に行こうとしているのを見たときは、少し力が抜けました。
169.254.169.254
ローカルPCで実行しているのに、EC2 IAM Roleを探しに行っていたわけです。
結局、根本原因はstateだった
いろいろな問題に遭遇しましたが、結局のところ根本原因は一つでした。
Terraform stateがなければ、
terraform destroyはほとんど意味を持たない
Terraformはコードだけを見て、既存リソースを削除するわけではありません。
stateが存在しない場合、Terraformは次のように認識します。
管理中のリソース = 0
つまり、実際のAWS上には次のようなリソースが残っていても、
- ECS cluster
- ECR repository
- Cloudflare record
Terraformから見ると、それらは「知らないリソース」になってしまいます。
stateを失ったあとの選択肢
最終的に取れる選択肢は、大きく分けて2つでした。
1. importを行う
Terraform stateに、既存リソースを再登録する必要があります。
terraform import aws_ecs_cluster.main teamspace-cluster
これをすべてのリソースに対して繰り返す必要があります。
実際にやろうとすると、思った以上に手間のかかる作業でした。
2. コンソールから直接削除する
個人プロジェクト規模であれば、AWSコンソールやCloudflareの画面から直接削除したほうが早い場合もあります。
最終的に感じたこと
Terraformを使うなら、remote stateは事実上必須だと感じました。
これは個人プロジェクトでも同じです。
少なくとも、次のどちらかは使うべきだと思います。
Terraform Cloud
terraform {
cloud {
organization = "my-org"
workspaces {
name = "teamspace"
}
}
}
メリットは次のとおりです。
- stateを自動で管理できる
- remote executionに対応している
- variableを管理できる
- GitHub Actionsと連携しやすい
S3 Backend
terraform {
backend "s3" {
bucket = "terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
}
}
AWS環境内でstate管理を完結させることができます。
今回の経験から学んだこと
Terraformは単なる「インフラ作成ツール」というよりも、次のようなものに近いと感じました。
stateをもとにインフラを追跡するツール
そしてstateを失った瞬間、Terraformは既存のインフラをまったく認識できなくなります。
その状態で destroy を実行しても、
苦労するのは人間だけで、リソースはそのまま残ります。
今回の経験を通して、個人プロジェクトであってもremote stateを必ず構成すべき理由をはっきり実感しました。