G-gen の杉村です。組織で定めた標準構成の Google Cloud プロジェクトを、Terraform を使って払い出すためのサンプル構成ファイルをご紹介します。

はじめに
標準構成のプロジェクトと Terraform
企業や官公庁などにおいて、複数の部署やチームが Google Cloud を利用する場合、情報システム部門等が Google Cloud プロジェクトの標準的な構成を定め、それを利用部門に対して払い出し、セキュアな利用を促すことは一般的に行われています。
Terraform などの Infrastructure as Code(IaC)ツールを用いることで、以下のような所定の構成を持つプロジェクトをミスなく、少ない工数で払い出すことができるようになります。
- 所定の請求先アカウントをプロジェクトに紐づけ
- 所定の IAM ロールを申請者に付与
- 決まった構成の VPC ネットワークとサブネットを作成
- 所定の「組織のポリシーの制約」をプロジェクトにアタッチ
当記事では、Google Cloud で IaC を実現するときのデファクトスタンダードである Terraform を用いた場合の、ファイル構成や構成ファイルのサンプルをご紹介します。
免責事項
当記事で紹介するプログラムのソースコードや構成ファイルは、ご自身の責任のもと、使用、引用、改変、再配布して構いません。
ただし、同ソースコードが原因で発生した不利益やトラブルについては、当社は一切の責任を負いません。
フォルダ構成
当記事のサンプルでは、以下のようなフォルダ構成を取ります。
environments/ └ prd ├ main.tf ├ backend.tf ├ variables.tf └ terraform.tfvars modules/ └ projects/ ├ apis.tf ├ iam.tf ├ projects.tf └ variables.tf
この構成では、ルートモジュール(terraform apply などを実行するディレクトリ)と、リソースを定義するモジュールを別々のディレクトリに配置しています。このような構成は Google Cloud の公式ドキュメントでも、ベストプラクティスとして記載されています。
上記のサンプルでは environments ディレクトリに prd というディレクトリだけを用意し、ここをルートモジュールとしています。必要に応じて複数の環境のディレクトリを作成します。prd、stg、dev といった区切り方や、あるいは部署ごと、サブシステムごとといった区切り方が考えられます。適切な区切り方は、運用体制などに応じて検討します。
リソースを定義するモジュールは、modules ディレクトリ以下に格納します。今回は projects というディレクトリを作成し、ここに標準構成のプロジェクトを定義します。これをルートモジュールから呼び出すことで、同じ構成のプロジェクトの複数の払い出しを実現します。
ルートモジュール
main.tf
ルートモジュールの main.tf は、以下のように記述します。
# 1個目のテナント module "tenant_test01" { source = "../../modules/projects" # テナント固有情報 tenant = { name = "my-test-project-01" folder_id = "123456789012" billing_account_id = var.common_billing_account } tenant_groups = { admin = [ "group:sys-admin@example.com", "user:john-doe@example.com" ] developer = [ "group:dev-team-01@example.com" ] viewer = [ "user:ichiro-suzuki@g-gen.co.jp" ] } } # 2個目のテナント module "tenant_test02" { source = "../../modules/projects" # テナント固有情報 tenant = { name = "my-test-project-02" folder_id = "123456789012" billing_account_id = var.common_billing_account } tenant_groups = { admin = [ "group:sys-admin@example.com", "user:john-doe@example.com" ] developer = [ "group:dev-team-02@example.com" ] viewer = [ "user:tom-brown@g-gen.co.jp" ] } }
今回のサンプルでは、1つの main.tf ファイルに複数のテナント(利用部署)のプロジェクトを記述しています。利用部署が増えるたびに、module "tenant_test01" { ... } の部分を増やしていきます。
プロジェクトが所属するフォルダは、テナントごとに指定できるようにしました。一方、プロジェクトに紐づける請求先アカウントは変数を使って共通の請求先アカウント ID を指定できるようにしました。変数への値の代入は、変数ファイル(terrafoirm.tfvars)で行います。
また、IAM ロールの付与も実現しています。あらかじめ「admin」「developer」「viewer」という権限セットをリソースモジュール側で定義しておきます。定義に応じた IAM ロールが、ここで指定した Google グループやアカウントに付与されます。
provider.tf
provider.tf では、プロバイダの情報と、state ファイルを格納するバックエンドバケットを定義します。
先述の Google のベストプラクティスのドキュメントでは、provider はリソースモジュール側で定義していましたが、ここでは統一された標準構成をすべてのテナントにデプロイする意図で、ルートモジュール側に持たせました。
# provider ・バックエンド定義 provider "google" { region = "asia-northeast1" request_timeout = "60s" } terraform { required_providers { google = { source = "hashicorp/google" version = "~> 7.3.0" } } required_version = ">= 1.5.7" backend "gcs" { bucket = "my-state-bucket" prefix = "standard-project/" } }
state ファイルを Cloud Storage バケットに格納することの意義などについては、以下の記事も参考にしてください。
variables.tf
このファイルでは、ルートモジュールの変数を宣言しています。今回は、組織が共通で使う請求先アカウントの ID を代入するための変数を宣言します。
# 共通の請求先アカウント variable "common_billing_account" { type = string }
terraform.tfvars
このファイルでは、組織が共通で使う請求先アカウントの ID を変数に代入します。これにより、同じ請求先アカウントを複数のプロジェクトに紐づけできます。
common_billing_account = "01234A-56789B-43210C"
Terraform においては、Google Cloud プロジェクトに紐づける請求先アカウントは名称ではなく英数字とハイフンで構成される ID で指定することに注意してください。
リソースモジュール
projects.tf
このファイルでは、Google Cloud プロジェクトのみを定義します。
# プロジェクト resource "google_project" "project" { # DELETE だと terraform で削除されるとプロジェクトが削除される。"ABANDON" だと state のみ削除 deletion_policy = "ABANDON" # プロジェクト名は6~30文字、英大文字不可 name = var.tenant.name project_id = var.tenant.name folder_id = var.tenant.folder_id billing_account = var.tenant.billing_account_id # false で default VPC ネットワークを作成しない auto_create_network = false }
誤用や事故を防ぐため、プロジェクト名とプロジェクト ID は同一の値としました。またここで、所属するフォルダ ID や紐づける請求先アカウントを指定しています。
deletion_policy はメタ引数(Google Cloud リソースとしての構成には影響はないが、Terraform が管理上用いる引数)です。値を DELETE にすると、Terraform リソースが削除されたときに、実際に Google Cloud プロジェクトも削除されます。ABANDON にすると、Terraform の state からは削除されますが、実際に Google Cloud プロジェクトは削除されずに残ります。
また、auto_create_network の値を false にすることで、本番環境では推奨されない構成であるデフォルトネットワークの作成を防いでいます。ただし、組織全体でデフォルトネットワークの作成を防ぐには、このようにリソース作成時に作成をスキップするだけでなく、組織のポリシーの制約である constraints/compute.skipDefaultNetworkCreation を用いることが推奨されます。
組織のポリシーについては以下の記事も参照してください。
apis.tf
このファイルでは、プロジェクトで有効化する API を記述します。
## プロジェクトで有効化するサービス API locals { apis = [ "compute.googleapis.com", "cloudfunctions.googleapis.com", "run.googleapis.com" ] } # API 有効化 resource "google_project_service" "enable_apis" { for_each = toset(local.apis) project = google_project.project.id service = each.value }
有効化する対象 API は、配列形式のローカル変数で定義しています。対象の API を増やしたい場合は、ここに追加します。
iam.tf
このファイルでは、IAM ロールの付与方法を定義します。ルートモジュールで「admin」「developer」「viewer」という権限セットが記述されていましたが、それぞれどのようなロールが付与されるかは、このファイルに定義されています。
######################### # IAM 定義 - 管理者グループ ######################### # ロール : 閲覧者 resource "google_project_iam_member" "admin_viewer" { for_each = toset(var.tenant_groups.admin) project = google_project.project.id role = "roles/viewer" member = each.value } # ロール : プライベート ログ閲覧者 resource "google_project_iam_member" "admin_private_log_viewer" { for_each = toset(var.tenant_groups.admin) project = google_project.project.id role = "roles/logging.privateLogViewer" member = each.value } # ロール : Compute 管理者 resource "google_project_iam_member" "admin_compute_admin" { for_each = toset(var.tenant_groups.admin) project = google_project.project.id role = "roles/compute.admin" member = each.value } # ロール : Cloud Run 管理者 resource "google_project_iam_member" "admin_cloud_run_admin" { for_each = toset(var.tenant_groups.admin) project = google_project.project.id role = "roles/run.admin" member = each.value } ######################### # IAM 定義 - 開発者グループ ######################### # ロール : 閲覧者 resource "google_project_iam_member" "developer_viewer" { for_each = toset(var.tenant_groups.developer) project = google_project.project.id role = "roles/viewer" member = each.value } # ロール : プライベート ログ閲覧者 resource "google_project_iam_member" "developer_private_log_viewer" { for_each = toset(var.tenant_groups.developer) project = google_project.project.id role = "roles/logging.privateLogViewer" member = each.value } # ロール : Compute インスタンス管理者(v1) resource "google_project_iam_member" "developer_compute_admin" { for_each = toset(var.tenant_groups.developer) project = google_project.project.id role = "roles/compute.instanceAdmin.v1" member = each.value } # ロール : Cloud Run デベロッパー resource "google_project_iam_member" "developer_cloud_run_admin" { for_each = toset(var.tenant_groups.developer) project = google_project.project.id role = "roles/run.developer" member = each.value } ######################### # IAM 定義 - 閲覧者グループ ######################### # ロール : 閲覧者 resource "google_project_iam_member" "viewer_viewer" { for_each = toset(var.tenant_groups.viewer) project = google_project.project.id role = "roles/viewer" member = each.value } # ロール : プライベート ログ閲覧者 resource "google_project_iam_member" "viewer_private_log_viewer" { for_each = toset(var.tenant_groups.viewer) project = google_project.project.id role = "roles/logging.privateLogViewer" member = each.value }
for_each = toset(var.tenant_groups.admin) member = each.value のように定義することで、ルートモジュールから渡されたすべての Google グループやアカウントに対して、IAM ロールが付与されるようにしています。
なお、ここでは google_project_iam_member という Terraform リソースを使用しています。その他の IAM ポリシー関係のリソースとの違いや注意点については、以下の記事を参照してください。
variable.tf
このファイルでは変数を宣言します。ルートモジュールから渡されるべき変数を定義しています。
# テナント情報 variable "tenant" { type = object({ name = string folder_id = string billing_account_id = string }) } # 権限を付与する Google アカウント・グループのリスト variable tenant_groups { type = object({ admin = list(string) developer = list(string) viewer = list(string) }) }
カスタマイズ
ここまで掲載したサンプル構成ファイルをカスタマイズすることで、組織内で標準化されたプロジェクトを複数のテナントに払い出すことができます。
カスタマイズすることで、例として、払い出したプロジェクトに標準構成の VPC ネットワークを作成してそれを Network Connectivity Center ハブに接続する等も可能です。
当記事に掲載した構成ファイルはあくまでサンプルですので、実際には組織のガイドライン、運用ルール、運用体制などに準じた形の構成ファイルを記述し、使用してください。
トラブルシューティング
terraform apply コマンドを実行した際に、以下のようなエラーが出力されることがあります。
Error: Provider produced inconsistent result after apply
When applying changes to module.tenannt_test01.google_project_iam_member.admin_cloud_run_admin["group:non-existing-group@example.com"], provider "provider[\"registry.terraform.io/hashicorp/google\"]" produced an unexpected new value: Root resource was present, but now absent.
This is a bug in the provider, which should be reported in the provider's own issue tracker.
エラーメッセージには「Terraform の Google Cloud provider のバグである」旨が記述されていますが、実際にはこのエラーは、存在しない Google グループを IAM メンバーに指定した際に発生しました。このエラーが出力された際は、IAM ロール付与対象のプリンシパルとして指定した Google アカウントやGoogle グループの存在有無を確認してください。
杉村 勇馬 (記事一覧)
執行役員 CTO
元警察官という経歴を持つ IT エンジニア。クラウド管理・運用やネットワークに知見。AWS 認定資格および Google Cloud 認定資格はすべて取得。X(旧 Twitter)では Google Cloud や Google Workspace のアップデート情報をつぶやいています。
Follow @y_sugi_it