はじめに
私の趣味の個人開発では、一部プロダクトにGCPのリソースが使われています。今回はこれらのリソース管理をTerraformで行えるようにした話を書きます。
動機
今までは、GCPのリソースをすべてWebコンソール上で手動で作成していました。しかし、この方法では人的ミスが発生しやすく、また、リソースの状態を管理することが難しいと感じました。そこで、Terraformを使ってリソース管理を行うことで、リソースの状態をコードで管理できるようにしました。
ディレクトリ構成
本番環境だけで運用していると、本番環境でまずいことを起こしてしまう可能性があるため、動作確認用のステージング環境を作成することにしました。そこで問題になったのが、ディレクトリ構成をどうするか?ということです。
企業のプロジェクトだとステージング環境と本番環境は共通化されていないことが多いと思います。
- https://engineering.mercari.com/blog/entry/20220121-securing-terraform-monorepo-ci/
- https://inside.pixiv.blog/2020/07/30/172828
- https://techlife.cookpad.com/entry/2020/02/28/120000
ですが、今回は「個人開発」故の以下の特性を考慮して、ステージング環境と本番環境を共通化することにしました。
- 全コードをセルフレビューしているため、ステージング環境のリソースを本番環境にも記述するときに生じるミスを可能な限り減らしておきたい
- せっかくステージング環境を作っているのに、本番環境に反映し忘れるということが起こりうる
- 複数人で管理していたらレビューで防げるミスも、個人開発だと気づけずに本番環境に反映してしまう可能性がある
- ステージング環境と本番環境で構成が異なることがない
- 企業のプロジェクトだと負荷対策やDevOpsなどで本番環境とステージング環境で構成が異なることがあるが、個人開発だとステージング環境と本番環境で構成が異なることはない
→ 本番環境とステージング環境を共通化してしまっても問題ない
- 企業のプロジェクトだと負荷対策やDevOpsなどで本番環境とステージング環境で構成が異なることがあるが、個人開発だとステージング環境と本番環境で構成が異なることはない
そこで、以下のディレクトリ構成を採用しました。基本的にGCPに記載のベストプラクティスに従っています。
├── modules
│ ├── service1
│ │ ├── main.tf
│ │ └── variables.tf
│ ├── service2
│ │ ├── main.tf
│ │ └── variables.tf
│ └── common_resource1
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── production
│ ├── backend.tf
│ ├── locals.tf
│ ├── main.tf
│ └── providers.tf
└── staging
├── backend.tf
├── locals.tf
├── main.tf
└── providers.tf
サービスごとのリソースを modules
以下に配置し、 production
と staging
にはそれらのリソースを呼び出すためのコードを配置しました。環境依存の設定は外から変数として渡すようにしています。
運用を初めて1ヶ月が経った現在、variableをたくさん使わないといけなくてやや面倒という気持ちもありますが、トータルではこの構成でよかったと思っています。
実装
基本的にはGCPのドキュメントに従っていきます。
- プロバイダーの設定
provider "google" {
project = local.project
default_labels = {
service = "home-cluster"
environment = local.env
}
}
-
tf plan
で変更点を確認- 実行には
gcloud
のログインが必要そう
- 実行には
-
tf apply
で変更を適用
GitHub Actionsを使ってリソース管理をするためには、tfstate
(terraform上のリソースの状態)をGCSに保存する必要があります。
参考:https://cloud.google.com/docs/terraform/resource-management/store-state?hl=ja
まずはtfstateのためのGCSバケットを作成します。
resource "google_storage_bucket" "terraform_remote_backend" {
name = "bucket-name"
location = "US"
force_destroy = false
public_access_prevention = "enforced"
uniform_bucket_level_access = true
}
次に、tfstateをGCSに保存するための設定を行います。
terraform {
backend "gcs" {
bucket = "stg-terraform-remote-backend.piny940.com"
}
}
最後に terraform init -migrate-state
を実行して、tfstateをGCSに移行します。
Workload Identity連携
Terraform関連の処理はGitHub Actionsを使って行おうとしていたため、そのためのサービスアカウントを作成する必要があります。
Terraformのためのサービスアカウントは、その特性上、強い権限を持つことになり、サービスアカウントキーの管理方法が課題になります。そこで、GCPのWorkload Identity連携の機能を活用することで、サービスアカウントキーを発行することなく、サービスアカウントを利用できるようにしました。
まずはworkload_identity_pool
を作成します。
resource "google_iam_workload_identity_pool" "default" {
workload_identity_pool_id = "pool"
}
次に、サービスアカウントを作成・ロール付与を行います。
ここでは最強の権限を与えていますが、本来は最小限の権限を与えるべきです。
resource "google_service_account" "terraform_github_actions" {
account_id = "terraform-github-actions"
display_name = "Terraform GitHub Actions"
create_ignore_already_exists = true
}
resource "google_project_iam_member" "terraform_github_actions_workload_identity_user" {
project = var.project
member = "serviceAccount:${google_service_account.terraform_github_actions.email}"
role = "roles/iam.workloadIdentityUser"
}
resource "google_project_iam_member" "terraform_github_actions_owner" {
project = var.project
member = "serviceAccount:${google_service_account.terraform_github_actions.email}"
role = "roles/owner"
}
githubをworkload_identity_pool_provider
として登録します。
resource "google_iam_workload_identity_pool_provider" "repo_github_actions" {
workload_identity_pool_id = google_iam_workload_identity_pool.default.workload_identity_pool_id
workload_identity_pool_provider_id = "repo-github-actions"
display_name = "Terrform GitHub Actions"
description = "for Terraform GitHub Actions"
attribute_condition = "assertion.repository == \"{user/repo}\""
attribute_mapping = {
"google.subject" = "assertion.sub"
}
oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}
}
最後に、サービスアカウントの権限借用の設定を行います。
subの値はGitHubのドキュメントに記載の通りです。
resource "google_service_account_iam_member" "terraform_github_actions_env_workload_identity_user" {
service_account_id = google_service_account.terraform_github_actions.id
role = "roles/iam.workloadIdentityUser"
member = "principal://iam.googleapis.com/projects/{プロジェクト番号}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.default.workload_identity_pool_id}/subject/{subの値}"
}
GitHub ActionsでのCI/CD
基本的な設定はドキュメントに記載のとおりです。
ですが、今回はステージング環境と本番環境のコードを共通化したことによって、ある問題が発生しました。
GitHub ActionsでのCI/CDは、PRをマージしたタイミングで terraform apply
をするようにしたいです。しかし、ステージング環境と本番環境のコードが共通化されているため、ステージング環境のリソースがapplyされたときには、同時に本番環境のリソースもapplyされてしまいます。
この問題を解決するために、GitHubの deployment
機能を活用して、以下のデプロイフローを採用することで解決しました。
- PRを作成。
terraform plan
が実行され、変更点を確認する。 -
environment: staging
を設定したJobでterraform apply
を実行する- Deployment protection rulesでownerの許可を必要にすることで、私が確認してからapplyすることができる
- PRがマージされたら、
environment: production
を設定したJobでterraform apply
を実行する-
production
環境のDeploymentはmainブランチでしか行えないようにしています。
-
これにより、ステージング環境と本番環境のリソースが同時にapplyされることを防ぐことができました。
まとめ
今回は、GCPのリソース管理をTerraformで行えるようにしました。
個人開発ゆえの特性を考慮して、独自の工夫を組み込むことができたのはかなりよかったと感じています。
これにてIaCを無事実現でき、かなり良いDXを得ることができました。今後はAWSでも同様の工夫を行って開発環境をより良くしていきたいなと思っています。