TerraformGoogle Cloud 実践教科書
演習ZIP ↓
LEARNING GUIDE / 0022章 · 図解 · 実行できるコード
コードで管理するインフラ / GOOGLE CLOUD

Google Cloudで学ぶTerraform
実践教科書

最初のplanから、Webアプリの運用まで。
つくる・変える・守る・復旧する力を、
ひとつずつ、自分の手に。

第1章から学び始める →
最初のインフラ構成
+network
+cloud_run
+cloud_sql
+復旧の手順
構成を理解して、変更を確かめる。
第 00 章設計目安 25分

学びの地図

作れる、変えられる、復旧できる。

この章の到達点
この教科書の到達点と、演習を進める順序を説明する。

インフラを「自分で管理できる」とは、サービスを起動できるだけではありません。変更前に影響を予測し、問題を検知し、データを守りながら元に戻せることです。この教科書では、その一連の仕事をGoogle Cloudで練習します。

題材は、Webアプリを支える小さな実用構成

アプリはCloud Run、データベースはCloud SQL for PostgreSQL、イメージはArtifact Registry、秘密はSecret Manager。最終段階で外部Application Load Balancer、HTTPS、Cloud Armor、監視を加えます。アプリは読み取り専用の状態確認用です。業務画面の実装より、インフラの振る舞いの確認に集中します。

HTTPSNEGprivate IP起動時に参照イメージを取得利用者ブラウザ / APIHTTPSの入口LB + Cloud ArmorCloud RunWebアプリArtifact Registryビルド済みイメージSecret Managerパスワードの保管Cloud SQLPostgreSQLTerraformで構成を管理。DBへの出口はDirect VPC egressとPSA。
最終構成:公開入口、アプリ、DBを分けて管理する↔ 狭い画面では図を左右にスクロールできます

本書でいう「中規模」は特定の月間PV数ではなく、開発と本番が分かれ、複数人が変更し、DBと機密情報を扱い、継続した運用責任がある規模です。単一リージョンから始め、可用性・性能・復旧時間に応じて拡張する判断を学びます。Cloud Runの適性を外れる常駐処理や特殊なネットワーク要件の有無は、GKEやCompute Engineを検討する境界です。

4段階の学習ルート

01 仕組みをつかむ

第1〜3章。ローカルファイルでplan、apply、state、HCLを体験。

02 Google Cloudへ

第4〜7章。認証、最初のリソース、共同管理、環境分離。

03 Webアプリを組む

第8〜13章。VPC、DB、秘密、コンテナ、HTTPS、監視。

04 運用を引き受ける

第14〜21章。容量計画、CI/CD、変更・取込・復旧、卒業課題。

準備と学習の進め方

想定する前提は、ファイルを編集できることと、ターミナルでコマンドを実行できることです。HTTP、DNS、CIDR、IAMは必要な章で説明します。実践の目安は24〜36時間+卒業課題。章の時間表示は読解と基本演習の目安で、サービス作成や証明書発行の待ち時間を含みません。

  1. 上部の「演習ZIP」からZIPを展開する。
  2. 展開したterraform-google-cloudを作業ルートにする。以降、特記しないコマンドはそのルートから実行する。
  3. コードを読む → 実行前の予測を書く → 実行結果との差を調べる、を繰り返す。
  4. 理解度テストと「完了」を使い、日を分けて学習する。進捗はこのブラウザ内に保存される。

版と検証の読み方

仕様確認日:2026年9月6日。Terraform 1.16.1、Google provider 8.1.0、local provider 2.5.3を教材の基準にしています。最新版を自動追従する教材ではありません。全ルートの静的検証とローカル演習を確認し、Google Cloud上でのapplyと課金を伴う試験は実施していません。組織ポリシー、権限、割当量、DNSは実行時に確認する受け入れ条件です。

この教科書で扱わない領域

Kubernetesの詳細、アプリ利用者のログイン実装、全面的なセキュリティ監査、法令対応、企業共通の組織・フォルダ設計は独立した学習領域です。最終章ではそれらを本番導入前に誰が確認するかまで設計します。「このサンプルをapplyしたら本番準備がすべて終わる」という教材ではありません。

理解度チェック
この教科書の到達点として最も適切なのは?
第 01 章概念目安 45分

Terraformの考え方

命令の列から、望ましい状態へ。

この章の到達点
構成・state・実リソースの三者関係を説明し、planの差分を読める。

「どう操作するか」より「何が存在すべきか」

手作業では「コンソールを開く、バケットを作る、公開を禁止する」と操作を並べます。Terraformでは、バケットの名前・場所・公開設定を宣言します。Terraformが現在の状況を読み取り、その宣言に近づけるための作成・変更・削除を計画します。これが宣言的なInfrastructure as Codeです。

Terraformは常駐監視サーバーではありません。コンソールで誰かが設定を変更しても、次のplan/applyまで自動で戻すわけではありません。また、applyはDBトランザクションのような全体一括ロールバックではなく、途中まで成功することがあります。

planが照合対応づけ / 読み取りapplyで変更構成 .tfどうしたいかstate何を管理しているか実リソースいま存在するもの
3つを見比べる:望ましい構成、管理の記録、実リソース↔ 狭い画面では図を左右にスクロールできます
構成(.tf)
Gitでレビューする設計。「これから、こうしたい」。
state(.tfstate)
Terraformのアドレスとクラウドの実体IDを対応づける記録。「何を自分が管理しているか」。
実リソース
Google Cloud側に実際に存在するもの。現在の設定やデータはAPIで確認する。

providerとresourceの役割

Terraform本体はHCLを解釈し、依存関係とstateを扱います。Google providerはGoogle CloudのAPIとのやり取りを担当します。google_storage_bucketはresourceの種類、practiceはそのコード内の名前です。この2つを結んだgoogle_storage_bucket.practiceがリソースアドレスになります。

HCL
resource "google_storage_bucket" "practice" {
  name     = "世界で一意なバケット名"
  location = "ASIA-NORTHEAST1"
}

この断片は文法の説明です。実行用の完全なコードは第5章にあります。コード内の名前を変えてもクラウド名は自動では変わりません。逆に、state上のアドレスが変わるだけで削除・新規作成と解釈される場合があります。後の章でmovedを使って対応づけを移します。

差分を手で予測してみる

変更を予測する、小さなplan演習操作して学ぶ / 01

教育用シミュレーション。Google CloudのAPIは呼ばず、課金も発生しません。実際の差分はprovider・属性・現在の状態によって変わります。

planは通常、実リソースの状態を読み、前回stateと構成を比較します。+は作成、~は更新、-は削除、-/+は置換です。変更可能な属性かどうかはproviderとAPIによって異なるため、名称だけで判断せず実際のplanを確認します。

理解度チェック
stateを消すと、Google Cloudの実リソースも消える?
この章の公式資料 · 2026-09-06確認Terraformの概要 ↗Terraform state ↗terraform plan ↗
第 02 章実習目安 45分

最初の20分:ローカル演習

init → plan → apply → もう一度plan。

この章の到達点
クラウドを使わず、作成・変更・再実行・削除を1周する。

ツールを準備する

macOSではHashiCorp公式Homebrew tapからインストールできます。教材と同じ1.16.1を使う場合は公式配布ページの該当バイナリを選び、SHA-256を照合してください。WindowsはWSL2、Linuxは公式手順を利用できます。以降のシェル例はbash/zshを想定します。

BASH
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform version

.terraform-versionは対応するバージョン管理ツール向けの目印です。Terraform自体がそのファイルを読んで版を切り替えるわけではありません。required_versionが適合しないとエラーになるので、先に版を確認します。

演習A:ファイルをTerraformで作る

完全なコードを見る · labs/00-local/main.tf

演習ZIP内の同じファイルを掲載しています。

labs/00-local/main.tf
terraform {
  required_version = ">= 1.16.1, < 1.17.0"
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "2.5.3"
    }
  }
}
variable "message" {
  type    = string
  default = "はじめての Terraform"
}
resource "local_file" "hello" {
  filename        = "${path.module}/hello.txt"
  content         = "${var.message}\n"
  file_permission = "0644"
}
output "path" {
  value = local_file.hello.filename
}
BASH
terraform -chdir=labs/00-local init
terraform -chdir=labs/00-local fmt
terraform -chdir=labs/00-local validate
terraform -chdir=labs/00-local plan -out=create.tfplan
terraform -chdir=labs/00-local show create.tfplan
terraform -chdir=labs/00-local apply create.tfplan
cat labs/00-local/hello.txt
terraform -chdir=labs/00-local plan

最初のplanに1 to add、apply後のファイルに本文、最後のplanにNo changesが出れば成功です。initはproviderの準備、fmtは整形、validateは設定検査です。validateの成功は、権限や課金設定が正しい証明ではありません。

演習B:変更して、手作業との差を観察する

  1. messageのdefaultを「Google Cloudをコードで管理する」に編集する。
  2. plan -out=change.tfplanを作り、ファイルが更新か置換かを読む。
  3. apply change.tfplanを実行してcatする。
  4. 今度はhello.txtだけをエディターで変え、再度planする。

local_fileは内容変更が置換になることがあります。「テキストが変わるから~だろう」という直感より、providerが出すplanを優先します。Terraform管理対象への手作業はdrift(構成と実体のずれ)の入口です。

完了条件と片付け

BASH
terraform -chdir=labs/00-local output
terraform -chdir=labs/00-local state list
terraform -chdir=labs/00-local destroy

destroyの一覧がlocal_fileだけであることを確認し、yesを入力します。hello.txtが消えても、設定ファイルは残ります。もう一度applyすれば再作成できることがIaCの利点です。次章の検証に使うのでlabs/00-local自体は残します。

よくあるつまずき

「No configuration files」は作業場所の違いを疑います。本書は-chdirで場所を固定します。「Unsupported Terraform Core version」は1.16系への切り替えを確認。providerの取得に失敗したらプロキシやネットワークを調べます。stateを消して直そうとしないでください。

理解度チェック
構成を変えた後、以前保存したcreate.tfplanをapplyすると?
第 03 章実習目安 90分

HCLを読んで書く

値・参照・繰り返しを、ひとつずつ。

この章の到達点
変数、型、参照、locals、for_each、output、依存関係を使える。

まず読めるようになる5つの部品

部品役目この教材の例
variable外から渡す入力project_id / region
localsコード内の計算結果tfbook-devという共通名
resource管理する実体Cloud Runサービス
data既存情報の読み取りプロジェクト番号
outputモジュールの外へ公開する結果サービスURL

同じディレクトリの*.tfはまとめて1つのモジュールとして評価されます。main.tfから順番に実行されるわけではありません。Terraformは参照関係から依存グラフを作り、依存しない処理は並行して進めます。

HCL
variable "environment" {
  type = string
  validation {
    condition     = contains(["dev", "prod"], var.environment)
    error_message = "dev または prod を指定してください。"
  }
}
locals {
  name = "tfbook-${var.environment}"
  labels = {
    environment = var.environment
    managed_by  = "terraform"
  }
}

型と式をconsoleで試す

stringは文字列、numberは数、boolは真偽値。list(string)は順序のある集合、set(string)は重複を除いた集合、map(string)は文字列キーの辞書です。異なる型の属性をまとめたいときはobject({name=string, ha=bool})を使います。nullは空文字とは異なり「未指定」を表す場面があります。

BASH
terraform -chdir=labs/00-local console
HCL
upper("dev")
length(["run", "sql"])
toset(["run", "sql", "run"])
{ for name in ["run", "sql"] : name => "api-${name}" }
true ? "REGIONAL" : "ZONAL"

期待値は順にDEV、2、重複なしの集合、キーrun/sqlを持つmap、REGIONALです。consoleはexitで終了します。

for_eachは「安定した名前」で管理する

HCL
resource "google_project_service" "apis" {
  for_each = toset([
    "run.googleapis.com",
    "sqladmin.googleapis.com"
  ])
  project            = var.project_id
  service            = each.value
  disable_on_destroy = false
}

apis["run.googleapis.com"]というアドレスになるため、追加しても既存要素の番号がずれません。countは同じものをN個作る用途や0/1の切り替えに適しますが、途中要素を削除すると添字のずれが変更を増やすことがあります。for_eachのキーはplan時に確定している必要があります。

参照すれば、作成順序が生まれる

サブネットのnetwork = google_compute_network.main.idは値の受け渡しであり、ネットワーク作成後にサブネットを作るという依存関係でもあります。depends_onは、API有効化やPrivate Services Accessのような参照だけで表せない依存に限定します。何でもdepends_onで直列にすると、不要な待ち時間や「apply後まで不明」が増えます。

入力ファイルの扱い

terraform.tfvarsと*.auto.tfvarsは自動で読み込まれます。複数経路から同じ値を渡すと優先順位で上書きされるため、通常値は各ルートのtfvarsに統一します。プロジェクトIDは秘密ではありませんが、教材の誤実行防止のため実値入りtfvarsをGitから除外しています。チームでは秘密なしの環境設定をレビュー対象にする設計も有効です。

理解度チェック
リソースの作成順を決める主な情報は?
第 04 章準備目安 60分

Google Cloudの準備と認証

誰が、どこへ、何をしてよいか。

この章の到達点
学習専用プロジェクトでADCを用意し、実行者とアプリの権限を区別する。

プロジェクト・課金・APIの3点セット

Google CloudのプロジェクトはリソースとIAMの主要な境界です。プロジェクトIDは人が選ぶ文字列、プロジェクト番号は自動付与の数値。Terraformの多くの引数はIDを使いますが、WIFのリソース名や予算フィルターでは番号も登場します。混同しないよう手元に記録します。

  1. Google Cloudコンソールで空の学習プロジェクトを選び、課金アカウントを紐づける。
  2. 開始前にCloud Billingで学習予算とメール通知を設定する。予算は自動停止装置ではない。
  3. 組織管理下なら、許可リージョン、外部公開、サービスアカウント、外部IPの制約を管理者に確認する。
  4. gcloud CLI、Git、Python 3、Docker(第10章で使用)を準備する。
BASH
export BOOK_PROJECT="自分の学習プロジェクトID"
export BOOK_REGION="asia-northeast1"
export BOOK_STATE="世界で一意なstateバケット名"
gcloud auth login
gcloud config set project "$BOOK_PROJECT"
gcloud auth application-default login
gcloud auth application-default set-quota-project "$BOOK_PROJECT"
gcloud projects describe "$BOOK_PROJECT" --format='value(projectNumber)'
gcloud services enable serviceusage.googleapis.com cloudresourcemanager.googleapis.com storage.googleapis.com --project="$BOOK_PROJECT"

これらの環境変数はターミナルを開き直すと消えます。秘密ではない値だけを自分のメモに控えておきます。Cloud Shellでは通常ADCの追加ログインは不要です。

gcloud loginとADCは別の認証経路

あなたgcloud auth logingcloudCLI用の認証あなたのADCapplication-defaultTerraformprovider / backendGoogle Cloud APIIAMで操作を許可アプリはさらに別のruntime SAを利用する。個人のADCを埋め込まない。
コマンド・Terraform・アプリは異なる主体で動く↔ 狭い画面では図を左右にスクロールできます

gcloud auth loginは主にgcloud用、gcloud auth application-default loginはTerraformやアプリSDKが使うADC用です。gcloud config set projectだけでTerraformのprojectが自動的に正しくなると考えず、providerにも明示します。GOOGLE_APPLICATION_CREDENTIALSに古い認証ファイルが設定されていると、ADCの探索でそちらが優先される場合があります。

必要な権限を段階ごとに付ける

実行対象代表的なロール付与先・補足
初期APIService Usage Admin学習プロジェクト
バケットStorage Admin作成時。state操作はバケットのObject Adminへ絞る
基盤Compute Network Admin / Service Networking Admin / Cloud SQL AdminVPC・PSA・DBの作成
秘密とSASecret Manager Admin / Service Account Admin / Project IAM Admin初期構築に必要。日常CIと分離
アプリCloud Run Admin / Artifact Registry Admin初回構築。実行用SAへのService Account Userも必要
外部入口Compute Load Balancer Admin / Compute Security AdminIP・LB・Cloud Armor。組織側の制約も確認
監視・予算・WIFMonitoring Admin / Billing Account Costs Manager / Workload Identity Pool Admin請求権限は課金アカウント。API有効化権限とは別

この表は機能ごとの権限設計の出発点です。組織のカスタムロールやDenyポリシーによって不足する場合があります。403なら失敗したAPIのpermissionを特定し、最小のスコープで追加します。常用CIにOwner/Editorを付与して解決しないこと。実行用SAのToken Creatorを持つことは、そのSAの権限を利用できることでもあります。

鍵ファイルを作らない経路

最初は個人ADC、チームではサービスアカウントのimpersonation、GitHub ActionsではWIFを使います。サービスアカウントのJSON鍵をダウンロードしてGitやCIの変数へ保存する経路を標準にしません。providerとGCS backendは別に認証するため、impersonationを採用する際は両方の権限を確認します。

理解度チェック
gcloudでは操作できるのにTerraformが認証エラー。最初に確認することは?
第 05 章有料実習目安 60分

最初のGoogle Cloudリソース

非公開バケットを作り、差分で育てる。

この章の到達点
GCSバケットを作成し、planで公開設定と変更内容を確認する。

なぜバケットから始めるのか

作成に必要な設定が少なく、VMやDBより早くフィードバックを得られるからです。ただし保存量・操作・ネットワーク通信などに課金があり、無料を保証する演習ではありません。今回はオブジェクトを入れない空のバケットで操作します。

完全なコードを見る · labs/01-bucket/main.tf

演習ZIP内の同じファイルを掲載しています。

labs/01-bucket/main.tf
variable "project_id" { type = string }
variable "bucket_name" { type = string }
variable "region" {
  type    = string
  default = "asia-northeast1"
}
provider "google" {
  project = var.project_id
  region  = var.region
}
resource "google_storage_bucket" "practice" {
  name                        = var.bucket_name
  location                    = var.region
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
  force_destroy               = false
  labels = {
    environment = "learning"
    managed_by  = "terraform"
  }
}
output "bucket" { value = google_storage_bucket.practice.name }

uniform_bucket_level_accessは個別オブジェクトACLを無効にしてIAMへ統一します。public_access_prevention = "enforced"は公開を防ぎます。force_destroy = falseは中身があると削除を失敗させる設定です。バケット名は世界で一意なので、プロジェクトIDと任意の短い接尾辞を組み合わせます。

演習:実値を設定して適用する

BASH
cp labs/01-bucket/terraform.tfvars.example labs/01-bucket/terraform.tfvars
# エディターで project_id と bucket_name を自分の値に変更
terraform -chdir=labs/01-bucket init
terraform -chdir=labs/01-bucket validate
terraform -chdir=labs/01-bucket plan -out=bucket.tfplan
terraform -chdir=labs/01-bucket show bucket.tfplan
terraform -chdir=labs/01-bucket apply bucket.tfplan
terraform -chdir=labs/01-bucket output -raw bucket

apply前の確認は「正しいproject」「意図したリージョン」「公開防止」「削除0」の4点です。outputに出た名前をGoogle Cloudコンソールで開き、公開アクセスが禁止されていることを確認します。

BASH
BOOK_BUCKET=$(terraform -chdir=labs/01-bucket output -raw bucket)
gcloud storage buckets describe "gs://$BOOK_BUCKET" --project="$BOOK_PROJECT"
terraform -chdir=labs/01-bucket plan

変更は1つずつ

main.tfのlabelsにpurpose = "terraform-book"を追加してplanします。期待するのはバケットの更新です。別のバケットを作る計画になったら、名前・作業場所・stateの接続先を見直します。変更後は再度planしてNo changesを確かめます。

片付けまでを1回の演習にする

BASH
terraform -chdir=labs/01-bucket plan -destroy -out=cleanup.tfplan
terraform -chdir=labs/01-bucket show cleanup.tfplan
terraform -chdir=labs/01-bucket apply cleanup.tfplan

対象はこのバケット1つだけです。ソフトデリートが有効なら削除後も保持期間中の費用が残ることがあります。force_destroyで強制的に消す習慣を付けず、空かどうかを調べるところから始めます。次章のstateバケットは、この練習用バケットとは別に作ります。

理解度チェック
公開されないことを確認するために見るべきものは?
第 06 章有料実習目安 90分

stateをチームで管理する

共有するのは設計。stateにはアクセス制御を。

この章の到達点
stateバケットを先に作り、ローカルstateをGCSへ移す。ロックと復元の役割を理解する。

stateをGitに置かない

stateには実リソースのIDや属性、場合によっては秘密の値も含まれます。Gitの差分共有ではなく、暗号化・IAM・履歴・ロックを持つbackendで管理します。本書はGCSを利用します。GCS backendはstate lockingをサポートし、同じstateへの同時書き込みを防ぎます。

作業者 / CI同じ変更経路を使うGCS backendstate・ロック・履歴Google Cloud実リソースstateの復元とDBデータの復元は別の仕事。
共有stateはアクセス制御とロックを持つ保存先へ↔ 狭い画面では図を左右にスクロールできます

bootstrap:保存先を誰が作るのか

GCS backendは、すでに存在するバケットを要求します。そのため最初だけローカルstateでバケットを作り、できた後でstateを移します。この「自分の土台を先に用意する」処理をbootstrapと呼びます。

完全なコードを見る · bootstrap/state/main.tf

演習ZIP内の同じファイルを掲載しています。

bootstrap/state/main.tf
variable "project_id" { type = string }
variable "bucket_name" { type = string }
provider "google" { project = var.project_id }
resource "google_storage_bucket" "state" {
  name                        = var.bucket_name
  location                    = "ASIA-NORTHEAST1"
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
  force_destroy               = false
  versioning { enabled = true }
  lifecycle { prevent_destroy = true }
}
output "bucket_name" { value = google_storage_bucket.state.name }
BASH
cp bootstrap/state/terraform.tfvars.example bootstrap/state/terraform.tfvars
# project_id と bucket_name を編集
terraform -chdir=bootstrap/state init
terraform -chdir=bootstrap/state plan -out=bootstrap.tfplan
terraform -chdir=bootstrap/state apply bootstrap.tfplan
cp bootstrap/state/backend.tf.example bootstrap/state/backend.tf
cp bootstrap/state/backend.hcl.example bootstrap/state/backend.hcl
# backend.hcl の bucket を作成済みバケット名に変更
terraform -chdir=bootstrap/state init -migrate-state -backend-config=backend.hcl
terraform -chdir=bootstrap/state state list
terraform -chdir=bootstrap/state plan

移行確認にyesを答え、bootstrap/state/default.tfstateがGCSに存在することと、No changesを確認します。旧ローカルstateや計画ファイルも機密として扱い、移行完了まで保管し、完了後は不要な複製を削除します。移行途中にバケットを削除しないでください。

ロック・バージョニング・保護は別物

ロック
同時更新による衝突を防ぐ。planから承認までの長い時間をずっと排他にするわけではない。
Object Versioning
上書き前のstateを残す。消したDB内のデータを復元する機能ではない。
prevent_destroy
構成が残っている間のTerraform経由の削除を止める。コンソール操作に対する防御ではない。

-lock-timeout=5mはロックが空くまで待ちます。force-unlockは実行者がもう動いていないことを確認した障害対応に限ります。「ロックが邪魔だから解除する」と並行applyの防御がなくなります。

本書の分割単位

同じdev専用バケット内にdev/foundation、dev/runtime、dev/edge、dev/operations、dev/identityを作ります。prefixは名前空間であり、そのままIAMの境界になるわけではありません。本番は別プロジェクト・別stateバケット・別実行SAで分けます。

理解度チェック
GCSのprefixをdev/prodで変えるだけで十分な権限分離になる?
第 07 章設計目安 75分

モジュールと環境分離

再利用する部品と、事故を閉じ込める境界。

この章の到達点
rootと子モジュールを分け、dev/prodを取り違えない構造を作る。

モジュールは「入力と出力があるディレクトリ」

root moduleはTerraformを実行するディレクトリです。子モジュールはそこからmoduleで呼び出す再利用単位です。再利用はコピーを減らし、state分割は変更の影響を小さくします。両者の目的は違うので、「すべてを1モジュール」「resourceごとに1state」の両極端を避けます。

dev root開発project / dev state共通modulesnetwork / runtime 等prod root本番project / prod stateモジュールはコードの共通化。rootとbackendが管理の境界。
同じ部品を使い、環境の権限とstateは分ける↔ 狭い画面では図を左右にスクロールできます
TEXT
terraform-google-cloud/
  bootstrap/state/             # state保存先を準備
  labs/00-local/               # 基本操作
  labs/01-bucket/              # 最初のGCP演習
  modules/
    network/ platform/ runtime/ edge/ operations/
  environments/
    dev/
      foundation/ runtime/ edge/ operations/ identity/
    prod/
      foundation/ runtime/ edge/ operations/
  app/                         # 状態確認用Webアプリ
  ci/                          # 有効化前のWorkflow
  helpers/                     # 検証と秘密の一時入力

ライフサイクルで分ける

foundationはVPC・DB・SA・イメージ置き場・秘密。runtimeは頻繁に更新するCloud Run。edgeはドメインに結びつく公開入口。operationsは監視と予算。WIFとCI用IAMはidentityに分け、通常のアプリ更新から切り離します。

複数stateをまたぐ順序はTerraformが自動で解決しません。foundation → runtime → edge → operationsの順に実行します。子モジュールの中に別stateがあるわけでもありません。1回のroot実行が、そのroot配下の全子モジュールを同じstateで管理します。

演習:設定ファイルを用意する

BASH
for stage in foundation runtime edge operations identity; do
  cp "environments/dev/$stage/terraform.tfvars.example" "environments/dev/$stage/terraform.tfvars"
  cp "environments/dev/$stage/backend.hcl.example" "environments/dev/$stage/backend.hcl"
done

project_id、state_bucket、backendのbucketを自分の値へ変更します。prefixは既定のdev/各段階を保ちます。image、ドメイン、通知先、GitHub IDは該当章で埋めれば構いません。YOUR_...が残るルートをapplyしないでください。

CLI workspaceを本番の境界にしない

workspaceは同じ構成に複数のstateを持たせる機能ですが、選択忘れが見えにくく、権限・backend・アカウントの違いまで解決しません。本書は各ディレクトリのdefault workspaceだけを使います。Google Cloudのroot moduleガイドも環境ごとのディレクトリを勧めています。

共通化しすぎない

モジュールの入力を100個作ると、利用者は結局サービスの全設定を理解する必要があります。教材ではregion、容量、HA、保護フラグなど、実際に変える項目を入力にします。サービス間の契約としてoutputを定義し、リソース内部の細部をすべて公開しないこと。本書のplatform出力は学習しやすさを優先した集約ですが、実務では必要な値だけに絞れます。

理解度チェック
子モジュールを作ると、自動的にstateも別になる?
第 08 章有料実習目安 100分

ネットワークと基盤

入口と出口、VPCとIAMを分けて考える。

この章の到達点
Direct VPC egressとPrivate Services Accessで、Cloud Runからprivate IPのDBへの経路を作る。

通信を4つに分ける

「アプリがネットワークにつながらない」では調べる場所が広すぎます。利用者→Cloud Runの入口、Cloud Run→DBの出口、アプリ→Google APIの通信、Terraform→管理APIの通信は、それぞれ別の経路と認証を使います。

出口接続Cloud RunDirect VPC egressVPC subnet10.20.0.0/24PSAの接続予約:10.30.0.0/16Cloud SQLサービス側のprivate IPHTTPのingressは別設定。Connectorだけでは経路を作れない。
Cloud Runの出口:自分のVPCからサービス側ネットワークへ↔ 狭い画面では図を左右にスクロールできます

VPCは仮想的なネットワーク、subnetはリージョンごとにIPアドレスを割り当てる範囲です。10.20.0.0/24は256個のアドレス空間を表しますが、すべてをアプリに使えるわけではありません。Google Cloudの予約アドレスとCloud RunのIP確保を考慮します。Direct VPC egressはrevision更新時も余分なIPを必要とするため、現在のインスタンス数だけでサブネットを小さくしません。

Direct VPC egress

Cloud RunからVPCへ出ていく通信の機能です。本書では別のServerless VPC Access connectorを作らずに利用します。これはCloud RunのHTTP入口をVPC内へ移す設定ではありません。入口の許可はingressとIAMで別に制御します。

PRIVATE_RANGES_ONLYは私設アドレス向けをVPCへ流し、それ以外は通常の出口を使います。ALL_TRAFFICへ変えるなら、外部通信のためのCloud NATなどを設計する必要があります。NATを無条件に足すと常時費用が増えるので、固定送信元IPなどの要件があるときに選びます。

Private Services Access(PSA)

Cloud SQLのprivate IPは、自分のCloud Runサブネットに直接割り当てるものではありません。Google管理のサービス側ネットワークと接続するために、10.30.0.0/16を予約し、Service Networkingの接続を作ります。自社VPN・他VPC・将来のサブネットと重複しないことを、apply前に確認します。

完全なコードを見る · modules/network/main.tf

演習ZIP内の同じファイルを掲載しています。

modules/network/main.tf
variable "project_id" { type = string }
variable "name" { type = string }
variable "region" { type = string }
variable "subnet_cidr" { type = string }
variable "psa_address" { type = string }
resource "google_compute_network" "main" {
  project                 = var.project_id
  name                    = var.name
  auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "run" {
  project                  = var.project_id
  name                     = "${var.name}-run"
  region                   = var.region
  network                  = google_compute_network.main.id
  ip_cidr_range            = var.subnet_cidr
  private_ip_google_access = true
}
resource "google_compute_global_address" "psa" {
  project       = var.project_id
  name          = "${var.name}-psa"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  address       = var.psa_address
  prefix_length = 16
  network       = google_compute_network.main.id
}
resource "google_service_networking_connection" "sql" {
  network                 = google_compute_network.main.id
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.psa.name]
}
output "network_id" { value = google_compute_network.main.id }
output "subnet_id" { value = google_compute_subnetwork.run.id }

まず基盤の計画を読む

foundationにはネットワークだけでなく次章のDBと秘密も含まれます。ここではコードの関係を読み、実際のapplyは第9章の説明後にまとめて行います。通常の演習のために-targetで途中だけ切り出しません。

BASH
terraform -chdir=environments/dev/foundation init -backend-config=backend.hcl
terraform -chdir=environments/dev/foundation validate

ローカルのPCはこのVPCに入っていないため、private IPのDBへ直接接続できません。Cloud SQL Auth ProxyやLanguage Connectorはネットワーク経路そのものを作る機能ではありません。ローカルDB接続が必要ならVPN、VPC内の作業環境、認可した踏み台などの経路を用意します。

理解度チェック
Cloud SQL Connectorを導入すれば、PCからprivate IPのDBへ必ず接続できる?
第 09 章有料実習目安 120分

IAM・DB・秘密情報

秘密をstateへ残さず、必要な権限だけを渡す。

この章の到達点
基盤を作成し、write-only引数を使った秘密管理とDB権限の境界を確認する。

3種類の「権限」を混同しない

Terraform実行者のCloud SQL AdminはDBインスタンスを構成する権限です。アプリSAのCloud SQL Clientは接続のためのGoogle Cloud権限です。PostgreSQLのユーザー・ロールはSQLを実行する権限です。Cloud SQL Clientを付けても、任意のテーブルをSELECT/UPDATEできるとは限りません。

教材のアプリSAはDB接続と1つのSecret参照だけを許可します。DBユーザーapp_readerはPostgreSQLのpg_read_all_dataを明示し、管理者ロールを使いません。これは専用演習DB全体を読める権限です。業務アプリでは対象schema・tableの必要操作だけを許可するカスタムロールへ絞り、DDLを行うmigrationユーザーと分離します。

一時入力ephemeral変数SQLユーザーpassword_woSecretの版secret_data_woCloud RunSecretのIDと版を参照stateには識別子と版番号。write-onlyの秘密値は保存しない。
同じ一時パスワードをSQLとSecretへ渡す↔ 狭い画面では図を左右にスクロールできます

sensitiveとephemeralは違う

sensitive = trueは主にCLI表示を隠します。通常のpasswordに渡せばstateに残り得ます。本書ではTerraform 1.16.1とGoogle provider 8.1.0の組み合わせで、入力変数にephemeral = true、リソースにpassword_woとsecret_data_woを使います。これらはplan/stateへ値を保存しないwrite-only引数です。

HCL
variable "db_password" {
  type      = string
  sensitive = true
  ephemeral = true
}
# SQLユーザーとSecretの両方に、同じ一時値を渡す。
# password_wo         = var.db_password
# secret_data_wo      = var.db_password

この機能は対応provider/属性に限られます。古い記事のpassword = var.db_passwordへ置き換えると秘密の保存条件が変わります。またwrite-onlyは値の比較ができないので、変更時はpassword_versionを増やします。秘密そのものはパスワードマネージャーで保管し、plan時とapply時に同じ値を入力します。

完全なコードを見る · modules/platform/main.tf

演習ZIP内の同じファイルを掲載しています。

modules/platform/main.tf
resource "google_service_account" "runtime" {
  project      = var.project_id
  account_id   = "${var.name}-runtime"
  display_name = "Webアプリ実行用"
}
resource "google_artifact_registry_repository" "app" {
  project       = var.project_id
  location      = var.region
  repository_id = var.name
  format        = "DOCKER"
  description   = "ハンズオンのアプリイメージ"
}
resource "google_sql_database_instance" "main" {
  project             = var.project_id
  name                = "${var.name}-db"
  region              = var.region
  database_version    = "POSTGRES_16"
  deletion_protection = var.protect_data
  settings {
    edition                     = "ENTERPRISE"
    tier                        = var.sql_tier
    availability_type           = var.ha ? "REGIONAL" : "ZONAL"
    disk_size                   = 10
    disk_autoresize             = true
    disk_autoresize_limit       = 100
    deletion_protection_enabled = var.protect_data
    ip_configuration {
      ipv4_enabled    = false
      private_network = var.network_id
      ssl_mode        = "ENCRYPTED_ONLY"
    }
    backup_configuration {
      enabled                        = true
      start_time                     = "18:00"
      point_in_time_recovery_enabled = true
      transaction_log_retention_days = 7
      backup_retention_settings {
        retained_backups = 7
        retention_unit   = "COUNT"
      }
    }
    maintenance_window {
      day          = 7
      hour         = 19
      update_track = "stable"
    }
    user_labels = {
      environment = var.environment
      managed_by  = "terraform"
    }
  }
}
resource "google_sql_database" "app" {
  project  = var.project_id
  name     = "app"
  instance = google_sql_database_instance.main.name
}
resource "google_sql_user" "app" {
  project             = var.project_id
  name                = "app_reader"
  instance            = google_sql_database_instance.main.name
  password_wo         = var.db_password
  password_wo_version = var.password_version
  # 専用の演習DBの読み取り用。実務では対象schemaだけのカスタムロールへ絞る。
  database_roles = ["pg_read_all_data"]
}
resource "google_project_iam_member" "sql_client" {
  project = var.project_id
  role    = "roles/cloudsql.client"
  member  = "serviceAccount:${google_service_account.runtime.email}"
}
resource "google_secret_manager_secret" "db" {
  project   = var.project_id
  secret_id = "${var.name}-db-password"
  replication {
    auto {}
  }
}
resource "google_secret_manager_secret_version" "db" {
  secret                 = google_secret_manager_secret.db.id
  secret_data_wo         = var.db_password
  secret_data_wo_version = tostring(var.password_version)
  # 切り戻しに備えて旧版を残す。廃止後の無効化は別途実施する。
  deletion_policy = "ABANDON"
  depends_on      = [google_sql_user.app]
}
resource "google_secret_manager_secret_iam_member" "runtime" {
  project   = var.project_id
  secret_id = google_secret_manager_secret.db.secret_id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.runtime.email}"
}

演習:基盤をapplyする

  1. 第7章のfoundationのtfvarsを埋め、devはdb-f1-micro、ha=falseを確認。
  2. 20文字以上のランダムパスワードを生成し、パスワードマネージャーへ保存する。
  3. 以下のplanを作る。入力した秘密はヘルパーがTerraform子プロセスの環境変数にだけ渡す。
  4. DBのpublic IP無効、PITR有効、保護フラグ、IAMの付与先を確認してapplyする。
BASH
python3 helpers/with-db-password.py environments/dev/foundation plan -lock-timeout=5m -out=foundation.tfplan
terraform -chdir=environments/dev/foundation show foundation.tfplan
python3 helpers/with-db-password.py environments/dev/foundation apply foundation.tfplan
terraform -chdir=environments/dev/foundation output
python3 helpers/with-db-password.py environments/dev/foundation plan

Cloud SQLやPSAの作成には時間がかかります。出力にはVPC、subnet、イメージ置き場、DB接続名、SecretのIDと版番号が出ます。秘密の値がoutputへ出ないことを確認します。Secret Managerコンソールで版が有効、Cloud SQLでprivate IPのみ、バックアップ有効、runtime SAの限定ロールを確認できたら成功です。

パスワード更新には順序がある

PostgreSQLの単一ユーザーのパスワードを変えると、旧パスワードを使う既存revisionは再接続に失敗し得ます。教材の単一ユーザー方式は無停止ローテーションを保証しません。devで新しい秘密とversionを同時に更新し、foundation適用後にruntimeを再適用する練習をします。本番では新旧ユーザーを並行運用して切り替える方法や、IAM DB認証を検討します。

理解度チェック
sensitive=trueだけでDBパスワードをstateに保存しないようにできる?
第 10 章有料実習目安 120分

コンテナをCloud Runへ

同じイメージを、同じ設定で再現する。

この章の到達点
アプリをArtifact Registryへ保存し、digestを固定してCloud Runを構成する。

Terraformはビルドツールではない

TerraformはCloud Runの構成と参照するイメージを管理します。Dockerのビルドやアプリの単体テストは別の手順です。local-execにビルドとデプロイを詰め込むと、差分の意味・失敗時の再開位置・再現性が分かりにくくなります。

タグlab1は後で別のイメージを指せます。digestのsha256:...は内容の識別子なので、環境間で同じものを渡せます。本書のruntime moduleはdigestなしのimageを検証エラーにします。

完全なコードを見る · app/Dockerfile

演習ZIP内の同じファイルを掲載しています。

app/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py ./
RUN useradd --uid 10001 --create-home appuser
USER 10001
ENV PYTHONUNBUFFERED=1
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "1", "--threads", "8", "--timeout", "30", "--access-logfile", "-", "main:app"]

演習:ローカルでビルドし、登録する

Dockerを起動し、docker versionで動作を確認してください。Apple SiliconでもCloud Runが利用できるlinux/amd64向けをビルドします。ベースイメージのタグは教材の可読性のために使っています。本番はベースイメージも検証したdigestに固定し、脆弱性更新の手順を作ります。

BASH
export BOOK_IMAGE="$BOOK_REGION-docker.pkg.dev/$BOOK_PROJECT/tfbook-dev/web"
gcloud auth configure-docker "$BOOK_REGION-docker.pkg.dev"
docker buildx build --platform linux/amd64 --provenance=false --push -t "$BOOK_IMAGE:lab1" app
BOOK_DIGEST=$(gcloud artifacts docker images describe "$BOOK_IMAGE:lab1" --project="$BOOK_PROJECT" --format='value(image_summary.digest)')
printf '%s@%s\n' "$BOOK_IMAGE" "$BOOK_DIGEST"

表示された.../web@sha256:64桁をruntimeのterraform.tfvarsのimageへ貼り付けます。Terraformの実行SAがアプリSAを割り当てる権限、Cloud Runのサービスエージェントがイメージを読む権限も必要です。同じプロジェクトを前提にしており、別プロジェクトのRegistryを使う場合は追加のアクセス設計が必要です。

runtimeを構成する

完全なコードを見る · modules/runtime/main.tf

演習ZIP内の同じファイルを掲載しています。

modules/runtime/main.tf
variable "project_id" { type = string }
variable "region" { type = string }
variable "name" { type = string }
variable "runtime_sa" { type = string }
variable "network_id" { type = string }
variable "subnet_id" { type = string }
variable "sql_connection_name" { type = string }
variable "db_user" { type = string }
variable "db_name" { type = string }
variable "secret_id" { type = string }
variable "secret_version" { type = string }
variable "image" {
  type = string
  validation {
    condition     = can(regex("@sha256:[0-9a-f]{64}$", var.image))
    error_message = "image は @sha256:... で固定してください。タグだけでは再現できません。"
  }
}
variable "min_instances" { type = number }
variable "max_instances" { type = number }
variable "edge_only" { type = bool }
variable "protect_service" { type = bool }
resource "google_cloud_run_v2_service" "app" {
  project             = var.project_id
  name                = var.name
  location            = var.region
  deletion_protection = var.protect_service
  ingress             = var.edge_only ? "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" : "INGRESS_TRAFFIC_ALL"
  # edge_only=false: IAM認証。true: LBで公開し、直接の外部入口を閉じる。
  invoker_iam_disabled = var.edge_only
  template {
    service_account                  = var.runtime_sa
    execution_environment            = "EXECUTION_ENVIRONMENT_GEN2"
    max_instance_request_concurrency = 20
    timeout                          = "30s"
    scaling {
      min_instance_count = var.min_instances
      max_instance_count = var.max_instances
    }
    vpc_access {
      egress = "PRIVATE_RANGES_ONLY"
      network_interfaces {
        network    = var.network_id
        subnetwork = var.subnet_id
      }
    }
    containers {
      image = var.image
      ports { container_port = 8080 }
      resources {
        limits = {
          cpu    = "1"
          memory = "512Mi"
        }
        cpu_idle          = true
        startup_cpu_boost = true
      }
      env {
        name  = "INSTANCE_CONNECTION_NAME"
        value = var.sql_connection_name
      }
      env {
        name  = "DB_USER"
        value = var.db_user
      }
      env {
        name  = "DB_NAME"
        value = var.db_name
      }
      env {
        name = "DB_PASSWORD"
        value_source {
          secret_key_ref {
            secret  = var.secret_id
            version = var.secret_version
          }
        }
      }
      startup_probe {
        initial_delay_seconds = 10
        timeout_seconds       = 3
        period_seconds        = 10
        failure_threshold     = 20
        http_get { path = "/healthz" }
      }
      liveness_probe {
        initial_delay_seconds = 10
        timeout_seconds       = 2
        period_seconds        = 30
        http_get { path = "/healthz" }
      }
    }
  }
  traffic {
    type    = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
    percent = 100
  }
  lifecycle {
    precondition {
      condition     = var.min_instances >= 0 && var.min_instances <= var.max_instances && var.max_instances >= 1
      error_message = "0 <= min_instances <= max_instances の範囲で指定してください。"
    }
  }
}
output "service_name" { value = google_cloud_run_v2_service.app.name }
output "url" { value = google_cloud_run_v2_service.app.uri }
BASH
terraform -chdir=environments/dev/runtime init -backend-config=backend.hcl
terraform -chdir=environments/dev/runtime validate
terraform -chdir=environments/dev/runtime plan -out=runtime.tfplan
terraform -chdir=environments/dev/runtime show runtime.tfplan
terraform -chdir=environments/dev/runtime apply runtime.tfplan
BOOK_RUN_URL=$(terraform -chdir=environments/dev/runtime output -raw url)
gcloud run services proxy tfbook-dev --region="$BOOK_REGION" --project="$BOOK_PROJECT" --port=8085

proxyは起動したままにして、ブラウザでhttp://localhost:8085を開きます。別のターミナルでcurl -i http://localhost:8085/healthzを実行すると200が返るのが期待値です。プロキシの利用者にはrun.routes.invoke(例:Cloud Run Invoker)が必要です。終了はCtrl+Cです。

この時点はIAM認証つき

初期値のedge_only=falseは、外部HTTP入口を受け入れますがInvokerのIAM検査は有効です。匿名でrun.app URLへアクセスしたら成功しないことを確認します。第12章では認証済みテストを終えてからLB経由の公開へ切り替えます。

起動と生存を確認するprobe

startup_probeは起動待ち、liveness_probeはプロセスが応答し続けるかの確認です。教材の/healthzはDBを呼びません。DBが一時停止しただけでアプリを大量再起動すると、DB復旧時の接続殺到を悪化させるためです。外部依存を含む確認は次章の/readyzで分けます。

理解度チェック
同じアプリを検証環境と本番へ確実に渡すために固定するものは?
第 11 章有料実習目安 90分

アプリとCloud SQLをつなぐ

到達・接続認証・SQL権限を順に確かめる。

この章の到達点
private IP経由でSELECTを実行し、接続失敗を層に分けて診断する。

接続名はIPアドレスではない

PROJECT:REGION:INSTANCEという接続名をPython Connectorへ渡します。ConnectorはAPIから接続情報を取得し、暗号化した接続を作ります。本書はIPTypes.PRIVATEを指定し、Direct VPC egressでDBのprivate IPへ届く構成です。接続にはGoogle CloudのSA、DBのユーザーとパスワード、ネットワーク経路がすべて必要です。

完全なコードを見る · app/main.py

演習ZIP内の同じファイルを掲載しています。

app/main.py
"""Cloud SQLへの実接続を確認する、読み取り専用の教材アプリ。"""
import atexit
import logging
import os
import threading
from flask import Flask, jsonify
from sqlalchemy import create_engine, text

app = Flask(__name__)
engine = None
connector = None
engine_lock = threading.Lock()

def get_engine():
    global engine, connector
    if engine is None:
        with engine_lock:
            if engine is None:
                from google.cloud.sql.connector import Connector, IPTypes
                connector = Connector(refresh_strategy="lazy")
                def connect():
                    return connector.connect(
                        os.environ["INSTANCE_CONNECTION_NAME"], "pg8000",
                        user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"],
                        db=os.environ["DB_NAME"], ip_type=IPTypes.PRIVATE,
                    )
                engine = create_engine(
                    "postgresql+pg8000://", creator=connect,
                    pool_size=2, max_overflow=0, pool_timeout=5,
                    pool_recycle=1800, pool_pre_ping=True,
                )
    return engine

@atexit.register
def shutdown():
    if engine is not None:
        engine.dispose()
    if connector is not None:
        connector.close()

@app.get("/")
def index():
    return """<!doctype html><html lang="ja"><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Terraform ハンズオン</title>
<style>body{font:17px/1.8 sans-serif;max-width:45rem;margin:8vh auto;padding:24px;color:#131F33}a{color:#004AAD}code{background:#ECF0F5;padding:4px}</style>
<h1>コードから、Webアプリへ。</h1><p>Cloud Runがリクエストに応答しています。</p>
<p><a href="/readyz">Cloud SQLへの接続を確認する</a></p>
<p><code>/healthz</code> はプロセス、<code>/readyz</code> はDBを含む状態を確認します。</p></html>"""

@app.get("/healthz")
def health():
    return jsonify(status="ok")

@app.get("/readyz")
def ready():
    try:
        with get_engine().connect() as connection:
            connection.execute(text("SELECT 1")).scalar_one()
        return jsonify(status="ok", database="connected")
    except Exception:
        # 接続文字列・パスワード・SQL詳細は返さない。
        logging.error("database readiness failed")
        return jsonify(status="unavailable", database="unreachable"), 503

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "8080")))

接続プールを読む

リクエストごとにTCP接続を作り直すと、遅延とDB負荷が増えます。SQLAlchemyのpool_size=2、max_overflow=0で再利用数を制限します。gunicornは1プロセスなので、1コンテナの基本上限は2接続です。workersを増やせばプロセスごとにプールができます。

pool_pre_pingは古い接続が生きているかを確認し、pool_timeoutはプール待ちを制限します。Connectorのrefresh_strategy="lazy"は、必要時に接続情報を更新する選択です。CPUがリクエスト中に割り当てられる実行形態との相性を考えます。

演習:DBを含む成功を確認する

BASH
# 第10章と同じ認証プロキシを起動
gcloud run services proxy tfbook-dev --region="$BOOK_REGION" --project="$BOOK_PROJECT" --port=8085
BASH
# 別ターミナル
curl -i http://localhost:8085/healthz
curl -i http://localhost:8085/readyz

/healthzは{"status":"ok"}、/readyzは{"status":"ok","database":"connected"}と200が期待値です。画面が開くだけでDB接続成功とは判断しません。/readyzは毎回SELECT 1を行います。

失敗を3層へ切り分ける

症状疑う層確認
起動前にSecretアクセス拒否Cloud Run起動時の権限専用SA、Secret単位のAccessor、存在する有効な版
ConnectorのAPIで403Google Cloudの認可SQL Admin APIとCloud SQL Client
接続タイムアウトネットワークprivate IP、VPC、PSA、subnet、egressルール
password authentication failedDB認証SQLユーザー、password_version、参照Secret版
permission denied for tableDB内権限schemaのUSAGE、tableのSELECT等
too many connections容量全revision×process×poolとDB上限

教材アプリは秘密をログへ漏らさないよう、外部には一般的な503しか返しません。実務で詳細ログを追加するときも、パスワードや接続URLを出さず、エラー分類とrequest IDを記録します。

実データを持つアプリに拡張するとき

テーブル作成やスキーマ変更はアプリのmigrationとして管理します。TerraformはSQLインスタンス・DB・ユーザーのライフサイクルを扱い、データの全行を構成として同期する用途には使いません。migrationジョブ用SAとDBロールを分け、通常runtimeにDDL権限を与えない設計へ進みます。

理解度チェック
/healthzは200だが/readyzは503。判断として正しいのは?
第 12 章発展・有料目安 120分

HTTPS・DNS・公開入口

入口をひとつに集めて、守りやすくする。

この章の到達点
独自ドメインとTLSを構成し、LBを迂回する外部アクセスが成功しないことを検証する。

開始条件と費用

この章は自分でDNSを変更できるドメインがある場合に実施します。LB・外部IPv4・Cloud Armorなどの料金が追加されます。ドメインがなくても第11章まででTerraformとDB接続の学習は可能です。Cloud Runの標準URLを利用する小さなサービスでは、LBを追加しない判断も成立します。

DNS / TLS名前・証明書・公開IPLB + Cloud ArmorURL map / Serverless NEGCloud Run内部とLBのみ受け入れHTTPS成功と、外部からのrun.app直接アクセス不成功を確認。
公開入口を揃える:DNSと証明書、LBとingress↔ 狭い画面では図を左右にスクロールできます

DNSは名前をIPへ対応づけます。証明書は、その名前のサーバーと暗号化通信するために使います。LBはURL mapに従ってbackendを選び、Serverless NEGがCloud Runを参照します。この構成のNEGは通常のVM health checkの対象ではありません。

演習A:公開入口を作る

edgeのtfvarsに所有するapp.自分のドメインを設定します。証明書リソースは作成できても、DNSが正しく向くまでは利用可能になりません。

完全なコードを見る · modules/edge/main.tf

演習ZIP内の同じファイルを掲載しています。

modules/edge/main.tf
variable "project_id" { type = string }
variable "region" { type = string }
variable "name" { type = string }
variable "service_name" { type = string }
variable "domain" { type = string }
resource "google_compute_global_address" "web" {
  project = var.project_id
  name    = "${var.name}-web-ip"
}
resource "google_compute_region_network_endpoint_group" "run" {
  project               = var.project_id
  name                  = "${var.name}-neg"
  region                = var.region
  network_endpoint_type = "SERVERLESS"
  cloud_run { service = var.service_name }
}
resource "google_compute_security_policy" "web" {
  project = var.project_id
  name    = "${var.name}-armor"
  rule {
    action   = "throttle"
    priority = 1000
    match {
      versioned_expr = "SRC_IPS_V1"
      config { src_ip_ranges = ["*"] }
    }
    rate_limit_options {
      conform_action = "allow"
      exceed_action  = "deny(429)"
      enforce_on_key = "IP"
      rate_limit_threshold {
        count        = 120
        interval_sec = 60
      }
    }
  }
  rule {
    action   = "allow"
    priority = 2147483647
    match {
      versioned_expr = "SRC_IPS_V1"
      config { src_ip_ranges = ["*"] }
    }
  }
}
resource "google_compute_backend_service" "web" {
  project               = var.project_id
  name                  = "${var.name}-backend"
  protocol              = "HTTP"
  load_balancing_scheme = "EXTERNAL_MANAGED"
  security_policy       = google_compute_security_policy.web.id
  # Serverless NEG に通常の health_checks は付けない。
  backend { group = google_compute_region_network_endpoint_group.run.id }
  log_config {
    enable      = true
    sample_rate = 1
  }
}
resource "google_compute_url_map" "web" {
  project         = var.project_id
  name            = "${var.name}-https"
  default_service = google_compute_backend_service.web.id
}
resource "google_compute_managed_ssl_certificate" "web" {
  project = var.project_id
  name    = "${var.name}-cert"
  managed { domains = [var.domain] }
}
resource "google_compute_target_https_proxy" "web" {
  project          = var.project_id
  name             = "${var.name}-https"
  url_map          = google_compute_url_map.web.id
  ssl_certificates = [google_compute_managed_ssl_certificate.web.id]
}
resource "google_compute_global_forwarding_rule" "https" {
  project               = var.project_id
  name                  = "${var.name}-https"
  ip_address            = google_compute_global_address.web.address
  target                = google_compute_target_https_proxy.web.id
  port_range            = "443"
  load_balancing_scheme = "EXTERNAL_MANAGED"
}
resource "google_compute_url_map" "redirect" {
  project = var.project_id
  name    = "${var.name}-redirect"
  default_url_redirect {
    https_redirect         = true
    strip_query            = false
    redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
  }
}
resource "google_compute_target_http_proxy" "redirect" {
  project = var.project_id
  name    = "${var.name}-redirect"
  url_map = google_compute_url_map.redirect.id
}
resource "google_compute_global_forwarding_rule" "http" {
  project               = var.project_id
  name                  = "${var.name}-http"
  ip_address            = google_compute_global_address.web.address
  target                = google_compute_target_http_proxy.redirect.id
  port_range            = "80"
  load_balancing_scheme = "EXTERNAL_MANAGED"
}
output "ip" { value = google_compute_global_address.web.address }
output "url" { value = "https://${var.domain}" }
BASH
terraform -chdir=environments/dev/edge init -backend-config=backend.hcl
terraform -chdir=environments/dev/edge plan -out=edge.tfplan
terraform -chdir=environments/dev/edge apply edge.tfplan
terraform -chdir=environments/dev/edge output -raw ip

DNS管理画面で対象のホスト名のAレコードをoutputのIPv4へ設定します。同じ名前の古いAAAAレコードや別サービス向けレコードが残っていないか確認します。既存サイトの名前を流用せず、学習用サブドメインを使います。

Google Cloud DNSで管理したい場合

同梱のexamples/dnsは学習用の子ゾーンを新規作成する完全な例です。親ドメイン側でその子ゾーンのNSを委任し、ゾーン内にappのAレコードを作ります。既存の親ゾーン全体を教材へimportする必要はありません。作っただけでは委任されない点に注意してください。

完全なコードを見る · examples/dns/main.tf

演習ZIP内の同じファイルを掲載しています。

examples/dns/main.tf
# 第12章の任意演習。新しい学習用子ゾーンだけを管理する。
terraform {
  required_version = ">= 1.16.1, < 1.17.0"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "8.1.0"
    }
  }
}
variable "project_id" { type = string }
variable "zone_dns_name" {
  type = string
  validation {
    condition     = endswith(var.zone_dns_name, ".")
    error_message = "DNSゾーン名は末尾にドットを付けてください。"
  }
}
variable "lb_ip" { type = string }
provider "google" { project = var.project_id }
resource "google_project_service" "dns" {
  service            = "dns.googleapis.com"
  disable_on_destroy = false
}
resource "google_dns_managed_zone" "learning" {
  name       = "tfbook-learning"
  dns_name   = var.zone_dns_name
  depends_on = [google_project_service.dns]
}
resource "google_dns_record_set" "app" {
  managed_zone = google_dns_managed_zone.learning.name
  name         = "app.${var.zone_dns_name}"
  type         = "A"
  ttl          = 300
  rrdatas      = [var.lb_ip]
}
output "name_servers" { value = google_dns_managed_zone.learning.name_servers }
output "app_fqdn" { value = google_dns_record_set.app.name }

演習B:Cloud RunをLB向けに切り替える

runtimeのedge_only = trueに変更し、plan/applyします。INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCERに制限し、Invoker IAM検査を無効化します。これで公開WebアプリとしてLB経由の匿名アクセスを受けます。アプリのエンドユーザー認証を追加する場合は、アプリ側で別途実装が必要です。

BASH
terraform -chdir=environments/dev/runtime plan -out=edge-mode.tfplan
terraform -chdir=environments/dev/runtime apply edge-mode.tfplan
export BOOK_DOMAIN="app.自分のドメイン"
dig +short A "$BOOK_DOMAIN"
gcloud compute ssl-certificates describe tfbook-dev-cert --global --project="$BOOK_PROJECT" --format='yaml(managed.status,managed.domainStatus)'

証明書がACTIVEになるまで待ちます。DNS伝播と発行には時間がかかります。Terraform applyの成功だけでHTTPS完了とは言えません。組織ポリシーがInvoker検査の無効化を禁止している場合は、この公開方式を管理者と調整します。禁止を別方式で回避する前提にはしません。

演習C:入口の検証

BASH
curl -I "http://$BOOK_DOMAIN"
curl -i "https://$BOOK_DOMAIN/readyz"
BOOK_RUN_URL=$(terraform -chdir=environments/dev/runtime output -raw url)
curl -i "$BOOK_RUN_URL/healthz"

HTTPはHTTPSへのリダイレクト、HTTPSの/readyzは200、run.appへの外部直接アクセスは成功しないことが期待値です。内部と分類される通信は依然として許可されます。要件に応じてdefault URL自体の無効化も検討できますが、その影響を確認してから導入します。

Cloud Armorの役割と限界

教材は1送信元IPあたり毎分120件のthrottle例です。厳密な課金上限でも、利用者単位の公平な制限でもありません。企業ネットワークや携帯回線では多数の利用者が同じIPを共有します。WAFルールを追加するときはpreviewで誤検知を測り、アプリの認可・入力検証も併用します。

理解度チェック
LBとCloud Armorを置いたのにrun.appの外部URLも匿名で成功する。何が問題?
第 13 章発展・有料目安 100分

監視とコスト管理

動いていることと、運用できること。

この章の到達点
監視通知の経路を作り、予算とコスト上限の違いを説明する。

何を知りたいかから監視を選ぶ

ログは何が起きたかを調べる記録、メトリクスは時間と量の傾向、トレースはリクエスト内のどこが遅いかを追う手段です。全ログを毎日人が読むのではなく、利用者への影響がある状態を通知し、通知から必要なログへ進めるようにします。

最初に見るのはリクエスト数、エラー率、遅延、飽和です。教材のoperationsはHTTPSの/readyzを複数地点から確認し、2地点以上で5分継続して失敗した場合の通知を構成します。1つのprobeだけでは全機能の正常性は保証できません。業務アプリでは主要な利用フローも合成監視します。

外形監視HTTPS /readyzを確認アラート複数地点・継続時間担当者ログ確認 → 復旧通知先の設定だけでなく、実際の受信と回復を試す。
監視は、担当者の判断と復旧までつないで完成する↔ 狭い画面では図を左右にスクロールできます

演習:通知先まで接続する

第12章のHTTPSがACTIVEで、/readyzが200になってから開始します。operationsのtfvarsにdomain、email、billing_account、monthly_budget、currency_codeを設定します。金額の5000は教材用の予算入力例で、実際の月額見積もりではありません。通貨は課金アカウントに合わせます。

BASH
gcloud services enable billingbudgets.googleapis.com cloudbilling.googleapis.com --project="$BOOK_PROJECT"
terraform -chdir=environments/dev/operations init -backend-config=backend.hcl
terraform -chdir=environments/dev/operations plan -out=operations.tfplan
terraform -chdir=environments/dev/operations apply operations.tfplan
完全なコードを見る · modules/operations/main.tf

演習ZIP内の同じファイルを掲載しています。

modules/operations/main.tf
variable "project_id" { type = string }
variable "name" { type = string }
variable "domain" { type = string }
variable "email" { type = string }
output "email_channel" { value = google_monitoring_notification_channel.email.name }
resource "google_monitoring_notification_channel" "email" {
  project      = var.project_id
  display_name = "運用通知"
  type         = "email"
  labels       = { email_address = var.email }
}
resource "google_monitoring_uptime_check_config" "web" {
  project      = var.project_id
  display_name = "${var.name} / DBを含む到達確認"
  timeout      = "10s"
  period       = "60s"
  http_check {
    path         = "/readyz"
    port         = 443
    use_ssl      = true
    validate_ssl = true
  }
  monitored_resource {
    type = "uptime_url"
    labels = {
      project_id = var.project_id
      host       = var.domain
    }
  }
}
resource "google_monitoring_alert_policy" "availability" {
  project               = var.project_id
  display_name          = "${var.name} 到達確認の失敗"
  combiner              = "OR"
  notification_channels = [google_monitoring_notification_channel.email.name]
  conditions {
    display_name = "2地点以上で5分継続"
    condition_threshold {
      filter          = "metric.type=\"monitoring.googleapis.com/uptime_check/check_passed\" AND resource.type=\"uptime_url\" AND metric.label.check_id=\"${google_monitoring_uptime_check_config.web.uptime_check_id}\""
      duration        = "300s"
      comparison      = "COMPARISON_GT"
      threshold_value = 1
      aggregations {
        alignment_period     = "60s"
        per_series_aligner   = "ALIGN_NEXT_OLDER"
        cross_series_reducer = "REDUCE_COUNT_FALSE"
        group_by_fields      = ["resource.label.host"]
      }
      trigger { count = 1 }
    }
  }
  documentation {
    mime_type = "text/markdown"
    content   = "外形監視が失敗。DNS/TLS、LB、Cloud Runログ、DB接続を順に確認する。教科書第19章を参照。"
  }
}

メール通知チャンネルに確認が必要と表示されたら完了させます。通知チャネルのテスト送信とCloud Monitoring上の到達確認の成功を確認してください。予算の既定IAM受信者と指定通知先が、自分の想定と合っていることも確認します。

予算は自動で支払いを止めない

Cloud Billingのbudgetは通知の仕組みです。利用量反映と通知には遅延があり、閾値ぴったりで支出を停止しません。課金APIを止める自動化を安易に入れると、復旧が難しい停止につながります。予算通知、サービス側の上限、定期的な棚卸し、演習終了時の削除を組み合わせます。

稼働時間と固定費を見積もる操作して学ぶ / 02

初期値は計算の練習用の架空単価です。実料金・無料枠・為替・税・割引・各SKUの課金条件を反映していません。

この計算は自分で入力した仮単価による概算です。Google Cloudの請求額を再現するものではありません。DB・LB・IPv4・Armor・保存容量・ログ・イメージ・バックアップ・通信・DNS・税・割引を別々に確認し、料金計算ツールで正式に見積もります。

読み取り専用の診断コマンド

BASH
gcloud logging read 'resource.type="cloud_run_revision" AND resource.labels.service_name="tfbook-dev" AND severity>=ERROR' --project="$BOOK_PROJECT" --limit=20 --format=json
gcloud run services describe tfbook-dev --region="$BOOK_REGION" --project="$BOOK_PROJECT"
gcloud sql instances describe tfbook-dev-db --project="$BOOK_PROJECT"

ログを共有する前に個人情報やトークンの有無を確認します。日常ダッシュボードにSQLのCPU・接続数・ディスク使用量を追加し、「どの値で誰が何をするか」を決めます。通知先を設定しただけでは運用体制は完成しません。

理解度チェック
月額予算を5,000円に設定すると?
第 14 章設計目安 100分

容量と可用性の設計

中規模への道は、台数より先に限界を知る。

この章の到達点
Cloud Runの並列度とDB接続予算を結びつけ、HA・バックアップ・DRを区別する。

Cloud Runだけ増やすと、DBが先に詰まる

アプリの同時リクエスト数とDB接続数は同じではありません。短いCPU処理もあれば、DBからの応答を待つ処理もあります。HTTPのconcurrency、gunicornのworker/thread、接続プール、DBの最大接続数を別々に設計します。無制限の接続プールを使ってスケールさせると、小さなDBが先に上限へ達します。

DB接続の予算を確かめる操作して学ぶ / 03

DB上限100は計算用の仮値です。実DBで確認した値に置き換えます。Cloud Runの一時的な上限超過も別途考慮します。

概算は最大コンテナ数 × 同時稼働revision数 × worker数 × (pool_size + max_overflow)に、管理・migration・他サービスの接続を足します。本書のdevなら基本は3×1×1×2=6、更新中に2revisionあれば12接続が目安です。Cloud Runのmax_instancesは常に絶対的なハード上限ではなく、一時的に超える場合もあるため余裕を残します。

性能を測る順序

  1. 業務目標を決める。例:「通常時200リクエスト/秒、p95が500ms以内」。これは教材の仮要件で、達成を保証する設定値ではない。
  2. dev/stagingで負荷試験し、CPU、遅延、DB接続待ち、SQL時間を測る。
  3. 最も詰まる箇所を1つずつ改善する。SQL/indexの改善、キャッシュ、非同期化、DB容量変更も候補。
  4. ピーク・デプロイ時・障害からの再接続を含めて再測定する。

HA、バックアップ、DRは代わりにならない

ゾーン障害HA / フェイルオーバー誤更新・誤削除バックアップ / PITRリージョン障害別リージョンのDRどれかひとつで、残りの故障も解決するわけではない。
3つの故障に、3つの備え↔ 狭い画面では図を左右にスクロールできます

Cloud SQLのREGIONAL構成は同一リージョン内のゾーン障害への可用性を高めます。誤って削除した行はレプリカにも反映されるので、データ復旧にはバックアップやPITRが必要です。リージョン障害への備えは別リージョンの復旧手段、ネットワーク、アプリ、DNS切り替えまで含むDR設計です。

RPO
許容できるデータ損失の時間。例:5分前まで戻れればよい。
RTO
サービスを戻すまでの許容時間。例:60分以内に利用を再開する。
SLO
利用者へ提供する信頼性の目標。例:一定期間の正常リクエスト比率。

教材のdevとprod例の差

項目devprod例
プロジェクト・state学習専用別project / 別bucket / 別SA
SQLZONAL / db-f1-microREGIONAL / db-custom-2-7680
Cloud Run min/max0 / 32 / 10
削除保護有効有効+運用承認
バックアップPITR有効・保持を確認要件に応じた保持と復元試験

prodの容量は学習用の出発点です。「中規模ならこのマシンで足りる」とは判断できません。DBの接続上限は構成によって変わるので、稼働DBの設定を確認します。ゼロスケールのコスト削減と、コールドスタートやSLOを両立できるかも測って決めます。

理解度チェック
DBをREGIONALにしたら、誤削除したデータの復旧も不要になる?
第 15 章発展実習目安 120分

GitHub ActionsとWIF

長期鍵を配らず、変更経路を限定する。

この章の到達点
短期認証を使うCIを設定し、planとapplyの承認境界を説明する。

CIに秘密鍵を置く前に、信頼関係を設計する

Workload Identity Federation(WIF)は、GitHubが発行したOIDCトークンの属性をGoogle Cloudが検証し、短期の認証へ交換する仕組みです。「この組織ID・repository ID・mainブランチ・dev環境の実行だけ」を信頼します。名前の再取得リスクを抑えるため、所有者とrepositoryは数値IDで制限します。

GitHub ActionsOIDCトークンWIF所有者ID・repo ID・refCI用SA許可されたruntime更新長期鍵の配布ではなく、条件を絞った信頼関係を作る。
WIF:信頼する実行条件を検証して短期認証へ↔ 狭い画面では図を左右にスクロールできます

認証できることと、操作できることは別です。外部の主体には特定SAへのroles/iam.workloadIdentityUser、そのSAには既存dev/runtimeの更新権限を付けます。教材CIはWIF自体、DB、IAM、予算を変更できる構成にしません。

演習A:WIFとCI用SAを作る

GitHub上にこの演習一式を保存する自分のrepositoryを用意します。environments/dev/identityのproject、state bucket、GitHubの数値IDを編集します。IDはGitHub APIで確認できます。ここで使うOWNER/REPOは自分が管理するものです。

BASH
gh api repos/OWNER/REPO --jq '{repository_id: .id, owner_id: .owner.id}'
terraform -chdir=environments/dev/identity init -backend-config=backend.hcl
terraform -chdir=environments/dev/identity plan -out=identity.tfplan
terraform -chdir=environments/dev/identity apply identity.tfplan
terraform -chdir=environments/dev/identity output
完全なコードを見る · environments/dev/identity/main.tf

演習ZIP内の同じファイルを掲載しています。

environments/dev/identity/main.tf
variable "project_id" { type = string }
variable "state_bucket" { type = string }
variable "github_owner_id" { type = string }
variable "github_repository_id" { type = string }
provider "google" { project = var.project_id }
data "google_project" "current" { project_id = var.project_id }
resource "google_service_account" "ci" {
  account_id   = "tfbook-dev-ci"
  display_name = "開発Cloud Run更新用CI"
}
resource "google_iam_workload_identity_pool" "github" {
  workload_identity_pool_id = "tfbook-dev-github"
}
resource "google_iam_workload_identity_pool_provider" "github" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.github.workload_identity_pool_id
  workload_identity_pool_provider_id = "github"
  attribute_mapping = {
    "google.subject"          = "assertion.sub"
    "attribute.repository_id" = "assertion.repository_id"
  }
  attribute_condition = "assertion.repository_owner_id == '${var.github_owner_id}' && assertion.repository_id == '${var.github_repository_id}' && assertion.ref == 'refs/heads/main' && assertion.sub.endsWith(':environment:dev')"
  oidc { issuer_uri = "https://token.actions.githubusercontent.com" }
}
resource "google_service_account_iam_member" "federation" {
  service_account_id = google_service_account.ci.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github.name}/attribute.repository_id/${var.github_repository_id}"
}
resource "google_storage_bucket_iam_member" "state" {
  bucket = var.state_bucket
  role   = "roles/storage.objectAdmin"
  member = "serviceAccount:${google_service_account.ci.email}"
}
resource "google_project_iam_member" "run" {
  project = var.project_id
  role    = "roles/run.developer"
  member  = "serviceAccount:${google_service_account.ci.email}"
}
resource "google_service_account_iam_member" "act_as" {
  service_account_id = "projects/${var.project_id}/serviceAccounts/tfbook-dev-runtime@${var.project_id}.iam.gserviceaccount.com"
  role               = "roles/iam.serviceAccountUser"
  member             = "serviceAccount:${google_service_account.ci.email}"
}
resource "google_project_iam_member" "network" {
  project = var.project_id
  role    = "roles/compute.networkUser"
  member  = "serviceAccount:${google_service_account.ci.email}"
}
resource "google_project_iam_member" "service_usage" {
  project = var.project_id
  role    = "roles/serviceusage.serviceUsageConsumer"
  member  = "serviceAccount:${google_service_account.ci.email}"
}
resource "google_artifact_registry_repository_iam_member" "reader" {
  project    = var.project_id
  location   = "asia-northeast1"
  repository = "tfbook-dev"
  role       = "roles/artifactregistry.reader"
  member     = "serviceAccount:${google_service_account.ci.email}"
}
output "provider_name" { value = google_iam_workload_identity_pool_provider.github.name }
output "service_account" { value = google_service_account.ci.email }

演習B:Workflowを有効にする

  1. GitHub Environment devを作り、承認者とmainブランチ制限を設定する。契約プランで利用できる保護機能を確認する。
  2. ci/README.mdに従い、WIF_PROVIDER、TF_SERVICE_ACCOUNT、PROJECT_ID、STATE_BUCKET、IMAGE_DIGEST、EDGE_ONLYをEnvironment variablesに設定する。imageのdigestと公開モードは現状に合わせる。
  3. ci/terraform-runtime.ymlを.github/workflows/へコピーし、内容をレビューしてmainに置く。
  4. 手動実行し、認証、plan、apply、No changesを確認する。
完全なコードを見る · ci/terraform-runtime.yml

演習ZIP内の同じファイルを掲載しています。

ci/terraform-runtime.yml
# 第15章の設定後、.github/workflows/terraform-runtime.yml へコピーする。
name: 開発環境のTerraform適用
on:
  workflow_dispatch:
permissions:
  contents: read
  id-token: write
concurrency:
  group: terraform-dev-runtime
  cancel-in-progress: false
jobs:
  apply:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: dev
    defaults:
      run:
        working-directory: environments/dev/runtime
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.16.1
          terraform_wrapper: false
      - uses: google-github-actions/auth@v3
        with:
          workload_identity_provider: ${{ vars.WIF_PROVIDER }}
          service_account: ${{ vars.TF_SERVICE_ACCOUNT }}
      - name: 設定を生成
        env:
          PROJECT_ID: ${{ vars.PROJECT_ID }}
          STATE_BUCKET: ${{ vars.STATE_BUCKET }}
          IMAGE: ${{ vars.IMAGE_DIGEST }}
          EDGE_ONLY: ${{ vars.EDGE_ONLY }}
        run: |
          python3 - <<'PY'
          import json, os
          assert os.environ['EDGE_ONLY'] in ('true', 'false')
          values = dict(project_id=os.environ['PROJECT_ID'], state_bucket=os.environ['STATE_BUCKET'], environment='dev', region='asia-northeast1', image=os.environ['IMAGE'], min_instances=0, max_instances=3, edge_only=os.environ['EDGE_ONLY']=='true', protect_service=True)
          with open('ci.auto.tfvars.json', 'w') as f:
              json.dump(values, f)
          PY
          terraform init -input=false -backend-config="bucket=$STATE_BUCKET" -backend-config="prefix=dev/runtime"
      - run: terraform fmt -check -recursive ../../../modules
      - run: terraform validate
      - run: terraform plan -input=false -lock-timeout=5m -out=review.tfplan
      # 同じジョブ内の保存済みplanだけを適用。planを公開artifactにしない。
      - run: terraform apply -input=false review.tfplan

このサンプルの承認は「開始前」

Environment承認はジョブの開始前に行われます。サンプルはその後、同じジョブ内で保存済みplanをapplyします。したがって生成されたplanそのものを後から人が承認する仕組みではありません。まず開発環境で短期認証と同一plan適用を練習するための最小構成です。

本番ではplanジョブ → 限定公開のplan保管 → plan内容を人が確認 → 別の承認付きapplyジョブへ分けます。commit SHA、lockfile、入力、backendを記録し、承認対象と適用対象が同じことを保証します。stateが変わって計画がstaleになったら、再plan・再承認します。concurrencyとstateのロックを併用し、古いapplyを自動キャンセルしません。

信頼しないコードには認証を渡さない

外部PRや任意ブランチにWIFを開放しないこと。Terraformの設定やprovider、外部プログラムはCI権限で実行され得ます。PRの静的検査は無認証で実行し、実環境のplanは信頼済みの経路に限定します。state/planを公開artifactへアップロードしないこと。Actionは本番採用前に検証したcommit SHAへ固定し、更新を管理します。

理解度チェック
Environmentでジョブ開始を承認しただけで、そのジョブが後で生成するplanをレビューしたことになる?
第 16 章実習目安 90分

変更を安全に届ける

変更する前に、置換と切り戻しを読む。

この章の到達点
変更を小さく分け、planの危険な差分とアプリ・データの復旧方法を区別する。

いつもの変更手順

  1. 変更目的、利用者への影響、成功条件、切り戻しを短く書く。
  2. 小さな差分で編集し、fmt・validate・テストを実行する。
  3. 正しいproject/backend/commitからplanを作る。
  4. 削除、置換、公開範囲、IAM、容量、課金の変化を確認する。
  5. 確認したplanをapplyし、実エンドポイント・監視・No changesを確認する。

Terraformの成功はアプリ機能の成功と同義ではありません。DBへ到達できるか、エラー率が増えていないかまで確認します。変更前の画面ではなく、稼働中のrevisionとdigestを記録することが切り戻しを助けます。

コードと入力commit・lockfile・digest保存済みplan差分確認と承認apply後の検証HTTP・DB・監視・差分設定変更後は新しいplanを作る。旧planを使い回さない。
変更の証拠をつなぐ↔ 狭い画面では図を左右にスクロールできます

演習:Cloud Runの最大数を3→4→3へ

dev/runtimeのtfvarsでmax_instancesだけを4に変えます。SQLやネットワークが削除されないことを読み取ります。適用後にNo changesを確認し、3へ戻す変更を新たにplan/applyします。小さな設定変更で一連の運用を練習します。

BASH
terraform -chdir=environments/dev/runtime plan -out=scale.tfplan
terraform -chdir=environments/dev/runtime show scale.tfplan
terraform -chdir=environments/dev/runtime apply scale.tfplan
terraform -chdir=environments/dev/runtime plan

lifecycleを万能な保護にしない

prevent_destroy
削除を計画したとき止める。ただしresourceブロックごと構成から消すと保護も消える。API側の保護と運用ルールも使う。
create_before_destroy
先に新しいものを作るが、同じ名前が使えない資源や容量制約では成立しない。DBが自動的に無停止移行される機能ではない。
ignore_changes
明確に別システムが所有する属性に限る。安易にallを指定すると重要なdriftまで見失う。

アプリのデプロイ所有者を決める

本書ではimageとtrafficをTerraformが所有します。別CIからgcloud run deployで勝手に変えると、次のapplyが戻すことがあります。アプリCIがimage/trafficを所有する運用も可能ですが、その場合はTerraform側の対象を限定し、責任境界を文書化します。

段階的リリースへの発展例

教材はLATESTへ100%です。カナリアを行うなら、実在する新旧revision名を入力として渡し、以下のようなtrafficへ置き換えます。合計100%にし、エラー率・遅延を測ってから比率を増やします。revision名はgcloudで一覧を確認してから使用してください。

HCL
# 既存の traffic ブロックを置き換える設計例。
# var.stable_revision / var.candidate_revision はstring入力として追加する。
traffic {
  type     = "TRAFFIC_TARGET_ALLOCATION_TYPE_REVISION"
  revision = var.stable_revision
  percent  = 95
}
traffic {
  type     = "TRAFFIC_TARGET_ALLOCATION_TYPE_REVISION"
  revision = var.candidate_revision
  percent  = 5
}

アプリの戻し方とデータの戻し方は違う

旧digestへ戻しても、新アプリが削除したDB列や更新済みデータは戻りません。DB変更は、先に互換的な追加、両版対応、データ移行、旧コード停止、最後に不要列削除というexpand/contractで段階化します。DBエンジンのメジャーアップグレードも、providerの更新とは分けて計画します。

理解度チェック
DBを置換するplanが出たとき、最初の行動は?
第 17 章有料実習目安 100分

既存資産・import・drift

手で作った環境を、壊さずコードへ迎える。

この章の到達点
importで対応づけを取り込み、movedでアドレスを変更し、driftを扱える。

importはコピーでもバックアップでもない

importは、すでにある実体をTerraformのアドレスに対応づける操作です。設定コードがその実体を適切に表しているかは別に確認します。1つの実体を複数stateから管理しないことが重要です。削除・公開変更が出たまま「importだから安全」とapplyしないでください。

演習A:手動バケットを取り込む

  1. 専用学習projectのコンソールで、空のバケットを新規作成。東京、Uniform access、公開防止を設定する。
  2. examples/importのtfvars例をコピーし、project_idとbucket_nameを設定する。
  3. 以下のimport.tfを同じディレクトリに作成する。bucket_nameはtfvars経由で参照する。
  4. planで「import」と追加変更を分けて読み、想定外の削除・置換がなければapplyする。
HCL
import {
  to = google_storage_bucket.adopted
  id = var.bucket_name
}
完全なコードを見る · examples/import/main.tf

演習ZIP内の同じファイルを掲載しています。

examples/import/main.tf
terraform {
  required_version = ">= 1.16.1, < 1.17.0"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "8.1.0"
    }
  }
}
variable "project_id" { type = string }
variable "bucket_name" { type = string }
provider "google" { project = var.project_id }
resource "google_storage_bucket" "adopted" {
  name                        = var.bucket_name
  location                    = "ASIA-NORTHEAST1"
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
  force_destroy               = false
}
BASH
terraform -chdir=examples/import init
terraform -chdir=examples/import plan -out=import.tfplan
terraform -chdir=examples/import show import.tfplan
terraform -chdir=examples/import apply import.tfplan
terraform -chdir=examples/import plan

期待するのはimportと、必要に応じた非破壊の属性調整です。既定値の違いによる差分もあるため、実体を表す構成へ調整してから進めます。作成場所が違うなど置換が必要な差分なら、ここでは止めてコードを修正します。

演習B:コード内の名前を変える

resourceのラベルをadoptedからarchiveに変え、importブロックは取り込み完了後に削除します。同時に以下を追加します。

HCL
moved {
  from = google_storage_bucket.adopted
  to   = google_storage_bucket.archive
}

planに「移動」が出て実体の作成・削除がないことを確認します。movedを先に消すと、まだ更新していない別環境が移動を認識できなくなるため、共有モジュールでは履歴として残します。movedは同じstate内のリファクタリングです。stateをまたぐ移管は別の手順と排他管理が必要です。

driftは原因を聞いてから扱う

手動の変更が見つかったら、①事故なのか、②緊急対応なのか、③所有者が別なのかを確認します。コードに戻すなら通常plan/apply。手動変更を採用するならコードを更新してから通常planで整合を確認します。refresh-onlyは実体の現状をstateへ反映しますが、コードを変更しません。次の通常planで再び元へ戻そうとする可能性があります。

BASH
terraform -chdir=examples/import plan -refresh-only
terraform -chdir=examples/import plan

定期drift検査ではplan -detailed-exitcodeを使えます。0は差分なし、1はエラー、2は差分ありです。2を障害と混同しないようCIの終了コード処理を明示します。

理解度チェック
refresh-onlyでstateを更新すれば、.tfのコードも手動変更に合わせて書き換わる?
第 18 章実習目安 100分

テストと品質の仕組み

速い検査と、実環境でしか分からないこと。

この章の到達点
fmt・validate・mock test・実環境の受け入れ試験の守備範囲を区別する。

検査を層に分ける

検査見つかるもの見つからないもの
fmt整形の不統一構成が目的に合うか
validate型、参照、providerスキーマの不整合IAM、実際の割当量、DNS伝播
terraform test + mock重要な設定の契約違反Google Cloudの実API挙動
planレビュー実体との差、削除・置換・権限変更適用後のアプリの全機能
実環境での試験接続、公開範囲、監視、復元まだ試していない故障条件

演習A:基本の検証

BASH
bash helpers/validate.sh
terraform -chdir=labs/00-local test

ヘルパーはbackendを接続せずに初期化してvalidateします。provider取得にはインターネットが必要ですが、Google Cloudのリソースは作りません。既にリモートbackendで運用中の作業フォルダがある場合は、検査専用の別チェックアウトで実行すると初期化設定を混ぜずに済みます。

演習B:安全性を契約として書く

完全なコードを見る · modules/platform/tests/security.tftest.hcl

演習ZIP内の同じファイルを掲載しています。

modules/platform/tests/security.tftest.hcl
mock_provider "google" {}
variables {
  project_id       = "unit-test-project"
  region           = "asia-northeast1"
  name             = "tfbook-dev"
  environment      = "dev"
  network_id       = "projects/unit-test-project/global/networks/test"
  sql_tier         = "db-f1-micro"
  ha               = false
  protect_data     = true
  db_password      = "nonsecret-placeholder-for-mock-test-only"
  password_version = 1
}
run "data_protection" {
  command = plan
  assert {
    condition     = !google_sql_database_instance.main.settings[0].ip_configuration[0].ipv4_enabled
    error_message = "DBの公開IPv4を有効にしないでください。"
  }
  assert {
    condition     = google_sql_database_instance.main.settings[0].backup_configuration[0].point_in_time_recovery_enabled
    error_message = "PITRを有効にしてください。"
  }
  assert {
    condition     = google_sql_database_instance.main.deletion_protection && google_sql_database_instance.main.settings[0].deletion_protection_enabled
    error_message = "TerraformとAPI両方の削除保護が必要です。"
  }
  assert {
    condition     = google_sql_user.app.database_roles == tolist(["pg_read_all_data"])
    error_message = "教材のDB実行ユーザーに管理者ロールを付与しないでください。"
  }
}
BASH
terraform -chdir=modules/platform init -backend=false
terraform -chdir=modules/platform test

このテストはGoogle providerをmockに置き換え、planだけを使います。DBの公開IP無効、PITR、削除保護、読み取り用DBロールを確認します。練習用コピーでipv4_enabledをtrueへ変え、テストが落ちることを確かめて戻します。単に実装と同じ式をテストに書くのでなく、守るべき要求を表現します。

アプリのテスト

BASH
python3 -m venv app/.venv
app/.venv/bin/pip install -r app/requirements.txt
cd app
.venv/bin/python -m unittest -v
cd ..

DBなしでも/healthzが応答すること、DB障害では/readyzが503となり秘密を返さないことをテストします。DB接続成功のmockだけでは本物の通信は証明できないので、第11章の実接続確認を別に残します。

provider更新は独立した変更にする

.terraform.lock.hclは選択版とチェックサムを記録するのでGitへ保存します。Terraform本体、provider、モジュール、コンテナの版は別物です。制約を見直す → changelogと移行ガイドを読む → init -upgrade → テスト → devのplan/apply → 本番の順で進めます。新機能を使う変更と大規模アップグレードを一度に混ぜません。

本番でPolicy as Codeを導入するなら、plan JSONに対して「DB公開禁止」「本番削除禁止」「許可リージョン」「IAMの過剰付与」を検査できます。JSONも機密として扱い、policyを通すための例外には理由と期限を付けます。

理解度チェック
terraform validateが成功したら、DNSとGoogle CloudのIAMも正しいと分かる?
第 19 章復旧演習目安 120分

障害対応と復旧演習

慌てたapplyより、現状と戻し先。

この章の到達点
失敗途中のapply、state障害、DB復旧を区別し、具体的な復旧記録を残す。

失敗したapplyは「何も起きていない」ではない

VPCは作れたがDBの割当量で失敗するなど、途中成功は通常あり得ます。まず並行実行を止め、エラーと時刻、成功したリソース、stateの状態を確認します。原因を直したらplanで残りの変更を読みます。無条件にdestroyして最初からやり直すと、成功済みのデータや共有資源を失います。

変更を止める影響・時刻・直前変更層を切り分ける入口 → アプリ → DB復旧して確認データ・指標・整合性失敗したapplyには、成功済みのリソースが残り得る。
失敗時は現状を把握してから、最小の復旧を選ぶ↔ 狭い画面では図を左右にスクロールできます

最初の10分のrunbook

  1. 利用者への影響と開始時刻を確認し、追加変更を止める。
  2. 直前のcommit・image digest・Cloud Run revision・DB変更を確認する。
  3. DNS/TLS → LB → Cloud Run → Secret/IAM → VPC → DBの順に、成功と失敗の境界を絞る。
  4. データ影響を考慮して復旧策を選ぶ。旧imageへ戻す、誤設定を直す、DBを別インスタンスへ復元する、を区別する。
  5. 実際の成功指標を確認し、恒久修正をコードと手順へ戻す。

演習A:stateを失うと何が起こるか

クラウドstateを使った破壊実験はしません。labs/00-localのコピーでファイルを作り、Terraformを停止した上でstateを別の安全な場所へ退避します。planが新規作成を提案することを確認してキャンセルし、元のstateを戻します。実体が残っていても、対応づけを失うと認識できないことを確かめます。

GCSのstateを復元する本当の事故では、書き込みを止め、現在stateを保管し、復元候補のgeneration、lineage、serial、更新時刻を確認します。古いstateを戻すだけで現在の実体と合うとは限りません。復元後にplanを読み、必要なimportや整合修正を判断します。stateをGitで上書きしないでください。

演習B:DBのPITRを別インスタンスで試す

これは追加DB料金が発生する発展演習です。PITR有効化後にログが蓄積していることと、復元可能な期間を確認します。業務データではなく、学習用DBに許可された管理ユーザーで検証用テーブルと数件のデータを入れてから実施します。

  1. UTC時刻と検証用データを記録する。
  2. 学習用データを1件変更し、変更前の時刻を復元対象に選ぶ。
  3. Cloud SQLコンソールのPITRから別のインスタンス名で復元する。
  4. VPC内の許可された接続環境から、対象時点の行・件数・権限を確認する。
  5. DB接続名とSecret版を含めたアプリ切り替え手順を検証し、所要時間を測る。
  6. 復元した一時インスタンスを識別して削除し、料金残存を確認する。

PITRは新しいインスタンスを作りますが、それはTerraformのstateへ自動追加されません。採用する場合は、元DBのresourceを上書きする前に別resourceとしてimportし、設定差分を確認します。旧DBを削除する計画と新DBへ向ける変更を分けます。教材アプリはデータを作らないので、SELECT 1が通るだけでデータ復元の検証済みとは言えません。

復旧結果を記録する

記録書く内容
影響どの機能・利用者・時間帯か
検出監視がいつ気づき、人がいつ確認したか
復旧使ったバックアップ・時刻・新インスタンス・確認したデータ
実測RTO/RPOの目標と結果
改善コード、検査、通知、手順の変更と担当
理解度チェック
PITRで復元したDBは、元のTerraform stateに自動で登録される?
第 20 章有料実習目安 75分

片付けと日常運用

消すときにも、設計が現れる。

この章の到達点
依存関係を逆順にたどって削除し、保護解除と残存費用を確認する。

削除前のチェック

作業project、backend bucket、prefix、対象resourceを確認します。学習用に見えても、共有のstateやDNSを削除しないこと。必要なデータと証跡を保管し、依存する利用者がいないことを確認します。復元テストで作った一時DBなど、Terraform管理外の資源も棚卸しします。

順序は「利用する側から」

監視・入口operations → edgeアプリruntimeの保護を解除基盤・認証foundation → identityDNS・管理外の復元DB・保持データを別に棚卸しする。
削除は利用側から。stateは最後まで維持する↔ 狭い画面では図を左右にスクロールできます
  1. CIのWorkflowを無効化し、並行実行がないことを確認。
  2. operationsを削除して、不要なアラート・予算を止める。
  3. edgeを削除。作成したDNSレコード・子ゾーン・親側のNS委任も片付ける。
  4. runtimeの削除保護をfalseに変更してapplyし、その後destroy。
  5. foundationの両方のDB削除保護をfalseにする変更をapplyし、その後destroy。
  6. identityを最後に削除する。実行は管理者の個人ADC等で行い、削除されるCI権限に依存しない。
BASH
# 実際に構築したルートだけで実行。各destroyの一覧を確認する。
terraform -chdir=environments/dev/operations destroy
terraform -chdir=environments/dev/edge destroy
# runtime/terraform.tfvars: protect_service = false に編集後
terraform -chdir=environments/dev/runtime plan -out=unprotect.tfplan
terraform -chdir=environments/dev/runtime apply unprotect.tfplan
terraform -chdir=environments/dev/runtime destroy
# foundation/terraform.tfvars: protect_data = false に編集後
python3 helpers/with-db-password.py environments/dev/foundation plan -out=unprotect.tfplan
python3 helpers/with-db-password.py environments/dev/foundation apply unprotect.tfplan
python3 helpers/with-db-password.py environments/dev/foundation destroy
terraform -chdir=environments/dev/identity destroy

Cloud RunやCloud SQLの削除保護は、falseにした設定を実際にapplyしてstate/APIに反映してから削除します。設定だけ変えて直ちにdestroyすると、保存されている保護状態が解除されておらず止まる場合があります。入力ヘルパーを利用するfoundationは削除時も同じ秘密の入力が必要です。

非同期の削除は待つ

Cloud SQLを削除した直後にPSA接続の削除が「まだ利用中」で失敗する場合があります。コンソールとサービスの削除状態を確認し、時間を置いてplan -destroyを読み直します。state rmやネットワークの強制解除でエラーを隠さないでください。Cloud RunのDirect VPC egressもIP解放に時間がかかり得ます。

stateバケットは最後まで残す

bootstrapはprevent_destroy=trueなので通常の削除から守られます。stateを残したまま保管する運用も有効です。学習projectを完全に閉じる場合は、すべての管理対象が削除済みであることを確認し、bootstrapのstateをローカルへ移行してから保護解除・バケット内容の削除を個別に計画します。現用backendのバケットを先に消さないでください。

削除後の残存費用を確認する

Artifact Registryのイメージ、保持中のSecret旧版、SQLの保持バックアップ、Cloud Storageのバージョンやソフトデリート、ログ保持、独自ドメイン、外部IP、復元した一時DBを確認します。Terraformに管理されていない資源はdestroyでは消えません。課金レポートには反映遅延があるため、後日も対象projectの費用を確認します。

管理を続ける場合のリズム

頻度確認すること
変更ごとplan、エンドポイント、監視、No changes
毎週費用、drift、アクセス権の増分、失敗したジョブ
毎月provider/イメージ更新、保存量、DB容量、バックアップ状況
定期訓練復元、切り戻し、担当不在時のアクセス、通知の到達
理解度チェック
全体を片付けるとき、最初にstateバケットを削除するのは?
第 21 章総合演習目安 240分

卒業課題:運用できる構成へ

「できた」を、証拠で確かめる。

この章の到達点
自分の要件から構成を選び、変更と復旧を第三者へ引き継げる。

依頼:小さなWebサービスのインフラ担当になる

あなたは3人の開発チームのインフラ担当です。アプリはHTTP APIとWeb画面、PostgreSQLを使います。毎日デプロイし、利用者が増える予定です。devとprodを分離し、独自ドメイン、運用通知、秘密管理、バックアップを用意してください。

仮の要件は、通常200リクエスト/秒、ピーク600リクエスト/秒、正常応答のSLO 99.9%、RPO 5分・RTO 60分です。これは達成済みの構成を示す数字ではありません。負荷試験・復旧試験で成立するかを検証し、費用とトレードオフを説明する課題です。要件を満たせないときは根拠を示して構成や要件を調整します。

提出物は5つ

  1. 構成図と判断記録:通信の入口/出口、IAM、state、データ、依存サービスを示す。採用理由と代替案を記録する。
  2. 再現可能なコード:dev/prodのroot、モジュール、固定版、CI、README。新しい作業環境から再現する手順を記す。
  3. 変更の証拠:1回のアプリ更新について、commit、digest、plan、承認、実エンドポイント確認を結びつける。
  4. 運用の証拠:監視通知の受信、drift検出、DB復元、復旧時間の実測を記録する。
  5. コスト表と撤収手順:月額の前提、ピーク時の変化、保持費用、全体を閉じる手順を記す。

合格判定チェックリスト

発展課題を要件で選ぶ

新しい要件考える技術・設計
利用者のアップロード非公開GCS、署名URL、サイズ・形式検証、ライフサイクル
時間のかかる処理Cloud Tasks / Pub/Sub / Cloud Run Jobs、再試行、冪等性
機密性の高い管理画面IAPやアプリの認証・認可、操作ログ
リージョン全体の障害別リージョン復旧、DB複製、フェイルオーバーと切り戻し
組織共通の統制Folder、Shared VPC、組織ポリシー、監査ログ、請求分離
長期鍵を減らすIAM DB認証、WIFの条件強化、秘密の短期化

技術を増やすほど管理できるようになるわけではありません。障害を減らすのか、変更を速くするのか、復旧時間を短くするのかを明確にして追加します。目的のない複雑さを減らすことも、インフラを管理する能力の一部です。

自分用の運用ノートを残す

「誰が、どの認証で、どのproject/stateを操作するか」「どの監視が何を意味するか」「データを戻す最短経路」「未検証の前提」をREADMEへ書きます。完成後も小さな変更と復旧訓練を繰り返すことで、設計を現実の制約へ合わせ続けられます。

理解度チェック
卒業課題の最も強い合格証拠は?
REFERENCE

用語・コマンド

読みながら戻れる、手元のリファレンス。

よく使うコマンド

作業先は terraform -chdir=対象ディレクトリ ... で固定します。

コマンド役目注意点
terraform initproviderとbackendを準備する最初や構成変更時。移行では-migrate-stateを使う。
terraform fmt -check -recursive整形ルールの差分を検出ファイルを書き換えず確認。
terraform validate設定の型・参照を検査実環境の権限や動作を保証しない。
terraform plan -out=review.tfplan適用する計画を保存planファイルは機密として保管。
terraform show review.tfplan保存した計画を読む削除・置換・IAM・公開設定を確認。
terraform apply review.tfplan保存した計画を適用通常のyes確認は出ない。事前に確認する。
terraform plan -detailed-exitcode差分の有無を終了コード化0=差分なし / 1=エラー / 2=差分あり。
terraform plan -refresh-only手動変更のstate反映を計画コードは更新されない。
terraform state list管理中のアドレスを表示作業環境と管理範囲を確認。
terraform outputrootの出力を確認sensitiveをraw/jsonで出す操作に注意。
terraform consoleHCLの式を対話で試す文字列、型、関数の練習に。
terraform testテスト定義を実行mockなしのapplyは実体を作り得る。
terraform init -upgrade許可範囲内のprovider等を更新lockfile差分と移行ガイドをレビュー。
terraform plan -destroy削除計画を読む作業先・保護・データ・依存関係を確認。
terraform force-unlock LOCK_ID残留ロックを解除実行者が動いていないことを先に確認。

用語集

IaC
Infrastructure as Code。インフラの構成をコードで管理する方法。
HCL
Terraformの構成を書くための宣言的な言語。
provider
外部サービスのAPIとTerraformをつなぐプラグイン。
resource
Terraformが作成・変更・削除を管理する実体。
data source
既存の資源や設定を読み取るための参照。
resource address
google_storage_bucket.practiceのような、構成とstate内の識別名。
root module
Terraformを実行するディレクトリ。state管理の入口。
子モジュール
root等からmoduleで呼び出す再利用部品。
state
コード内のアドレスと実体IDを対応づける管理記録。
backend
stateを保存する場所とロックなどの機能。
state lock
同じstateへの同時書き込みを防ぐ排他制御。
drift
コードが想定する構成と実リソースの設定のずれ。
plan
提案される作成・更新・削除・置換の計画。
apply
計画に従って実リソースを変更する操作。
置換
既存リソースを削除し、別の実体として作り直す変更。
import
既存実体をTerraformのアドレスへ対応づける操作。
moved
state内のアドレス移動をコードで伝える宣言。
ADC
実行環境に応じて認証情報を探すGoogle Cloudの仕組み。
IAM
誰が、何に対して、何をできるかを管理する仕組み。
サービスアカウント
アプリや自動処理のためのGoogle Cloudの主体。
impersonation
許可された利用者が特定SAとして短期の認証を行うこと。
WIF
外部IdPのIDを検証し、短期のGoogle Cloud認証へ連携する仕組み。
OIDC
IDトークンで主体や実行条件を伝える認証の規格。
VPC
Google Cloud内の仮想ネットワーク。
subnet
リージョン内でIPアドレスを割り当てる範囲。
CIDR
10.20.0.0/24のようにネットワーク範囲を表す記法。
ingress / egress
サービスに入る通信 / サービスから出る通信。
PSA
Google管理サービスとのprivate接続に使うPrivate Services Access。
Cloud NAT
VPC内の対象リソースが外部へ出る際のアドレス変換。
revision
Cloud Runの構成とコンテナを固定したデプロイ単位。
digest
コンテナイメージの内容を識別するハッシュ。
Serverless NEG
LBからCloud Run等のサーバーレスbackendを参照する単位。
DNS / TLS
名前をIP等へ解決する仕組み / 通信を暗号化し相手を検証する仕組み。
接続プール
DB接続を再利用し、同時接続数を制限する仕組み。
sensitive
値の表示を秘匿する指定。stateから自動除外する指定ではない。
ephemeral
実行中だけ使う値。使用できる位置には制約がある。
write-only
providerへ渡すがplan/stateへ値を保存しない対応属性。
HA
High Availability。障害時も利用可能な状態を保つ設計。
PITR
Point-in-Time Recovery。復元可能な範囲の特定時点へデータを戻す方法。
RPO / RTO
許容するデータ損失の時間 / 復旧までの目標時間。
SLI / SLO
信頼性を測る指標 / その目標値。
冪等性
同じ処理を繰り返しても意図しない重複結果を増やさない性質。
SOURCES & VERIFICATION

出典と検証

公式資料をたどれる、更新可能な教材。

仕様確認日は2026年9月6日。解説と演習構成は独自に作成し、HashiCorp、Google Cloud、各公式repositoryを一次資料として参照しています。公式ページが更新されたら、教材の固定版と照合してください。

検証の境界

  • Terraform本体とproviderを固定し、全実行ルートのfmt/validateとmockテストを確認。
  • クラウド不要のローカル作成・再計画・削除と、サンプルアプリのテストを実施。
  • ブラウザで章移動、検索、進捗、テスト、計算、モバイル表示を確認。
  • Google Cloudの実プロジェクトへのapply、実課金、実際のDNS/TLS/DB復元は未実施。各章の完了条件を自分の学習環境で確認。

検証記録の詳細は完全版ZIPのQUALITY.mdを参照してください。

デザイン

指定のstellararc-inc/design-systemの配布CSS、ロールトークン、文字組み、インラインSVG規定を適用。オフラインでも読めるようCSSを内包し、フォントは指定のフォールバックを使用します。

公式資料一覧