概要
Pydantic v2 BaseSettings — 起動時 Fail-Fast
os.getenv() は全て str | None を返し、バリデーションなし。BaseSettings は環境変数を自動で型変換・バリデーションし、不備があれば 起動時に即死(ValidationError)させる。「深夜バッチが半分で落ちる」問題を根絶する。
Annotated + Field で「型としての制約」
Annotated[int, Field(gt=0, le=10_000)] は「1以上10,000以下の整数」を型レベルで表現する。if ガードや docstring に頼らず、コードが制約の唯一の真実(SSOT) になる(Ch2 型の活用)。
GitHub Actions OIDC でキーレスデプロイ
SA キーを GitHub Secrets に保存する設計は、漏洩リスク・90日期限管理・ローテーション忘れがつきまとう。OIDC 一時トークンは GitHub Actions セッション中のみ有効。「キーを作らない設計」が最も安全な鍵管理。
Cloud Run v2 IAM 認証で公開 API を廃止
allUsers への roles/run.invoker 付与は「誰でも無制限に API 呼び出し可能」な状態。--no-allow-unauthenticated で IAM 認証を強制し、認可された SA・ユーザーのみに制限する。
問題 A: コーディング — Pydantic v2 Settings × Bad→Good リファクタリング
以下の「悪いコード」は、ECサイトのバッチ処理基盤(Argo Workflows)の設定読み込みを担うクラスです。問題点を全て洗い出し、Pydantic v2 BaseSettings・Annotated + Field・カスタムバリデーター を使って Bad→Good にリファクタリングしてください。
制約・前提条件
- Python 3.12+、Pydantic v2(
pydantic-settings含む)を使うこと GCP_PROJECT_IDは必須フィールドとし、未設定なら起動時にエラーを出すことBATCH_SIZEは 1〜10,000 の範囲制約をAnnotated+Field(gt=0, le=10000)で付けることENVは"staging"/"production"のみ許可(Literalで型制約)SLACK_WEBHOOK_URLはhttps://hooks.slack.com/で始まること(カスタムバリデーター)- Google スタイル docstring・インラインコメント・名前付き定数を含めること
期待する回答形式: 問題点の列挙(番号付き)+ 改善後コード + 実行例(input→output)+ 適用した設計パターン名と書籍対応章
悪いコード (Before) — カテゴリ A
このコードには 7つの設計上の問題 が隠れています。見つけてみてください。
bad_config.py — 問題だらけの設定管理
import os
class Config:
def __init__(self):
# 問題1: 全フィールドが str | None
# 型ヒントがなく mypy では検知不可
self.project_id = os.getenv("GCP_PROJECT_ID")
self.bq_dataset = os.getenv("BQ_DATASET", "mart")
# 問題2: int() 変換は例外なく None → TypeError
self.batch_size = int(os.getenv("BATCH_SIZE", "100"))
# 問題3: RETRY_LIMIT=-1 でも通る(範囲バリデーションなし)
self.retry_limit = int(os.getenv("RETRY_LIMIT", "3"))
# 問題4: 不正 URL でも None でもエラーなし
self.webhook_url = os.getenv("SLACK_WEBHOOK_URL")
# 問題5: "prodution"(タイポ)でも通る
self.env = os.getenv("ENV", "staging")
# 問題6: シークレット値を print でログに出す
print(cfg.project_id)
# 問題7: モジュールレベルで即時インスタンス化
# → テスト時に環境変数モックが困難
cfg = Config()
問題点サマリー(7点)
1型ヒントが皆無(Ch2)—
os.getenv() は str | None を返すが型注釈なし。mypy/pyright で型エラーを検知できない2必須フィールドに None が混入(Ch10)—
GCP_PROJECT_ID 未設定でも None のまま処理が進む。実行時エラーがランダムに発生する3範囲バリデーションなし(Ch2)—
BATCH_SIZE=-1 でも通る。BQ Insert で意図しない挙動になる4ENV の値が任意文字列(Ch2)—
ENV=prodution(タイポ)でもエラーなし。本番意図のコードがステージングで動く5Webhook URL のフォーマット検証なし(Ch10)— 不正 URL が Slack API エラーとして本番で初めて判明する
6
print でシークレット値をログ出力(セキュリティ)— Cloud Logging に GCP プロジェクト ID や Webhook URL が残る7モジュールレベルで即時インスタンス化(Ch11 テスト容易性)—
cfg = Config() は import 時に実行される。pytest で環境変数モックが困難ヒント A(段階的開示)
ヒント1 — 方向性
os.getenv() は全て str | None 型で、型安全性がない。Pydantic v2 BaseSettings は .env ファイルや環境変数を自動で読み込み、型変換・バリデーションを実行してくれる。必須フィールド(デフォルトなし)は未設定なら即座に ValidationError を送出するため、「Fail-Fast」なエラー処理が自動的に実現する。Annotated[int, Field(gt=0, le=10_000)] は「1以上10,000以下の整数」を型レベルで表現できる。
ヒント2 — アプローチ
class AppConfig(BaseSettings)で継承し、model_config = SettingsConfigDict(env_file=".env")を設定- 必須フィールドはデフォルト値なしで定義:
gcp_project_id: str - 範囲制約は
Annotated[int, Field(gt=0, le=10_000)]で表現 Literal["staging", "production"]で env フィールドを型制約@field_validator("slack_webhook_url")でカスタムバリデーション@model_validator(mode="after")でクロスフィールド検証(本番環境では webhook 必須)get_config()ファクトリ関数を用意してテスト可能に
ヒント3 — コードの骨格
from typing import Annotated, Literal
from pydantic import Field, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
SLACK_WEBHOOK_PREFIX: str = "https://hooks.slack.com/"
class AppConfig(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
gcp_project_id: str # 必須(デフォルトなし → 未設定で ValidationError)
bq_dataset: str = "mart"
batch_size: Annotated[int, Field(gt=0, le=10_000)] = 100
retry_limit: Annotated[int, Field(ge=1, le=10)] = 3
env: Literal["staging", "production"] = "staging"
slack_webhook_url: str | None = None
@field_validator("slack_webhook_url")
@classmethod
def validate_webhook(cls, v: str | None) -> str | None:
if v is not None and not v.startswith(SLACK_WEBHOOK_PREFIX):
raise ValueError(f"SLACK_WEBHOOK_URL は {SLACK_WEBHOOK_PREFIX!r} で始まる必要があります")
return v
@model_validator(mode="after")
def validate_prod_has_webhook(self) -> "AppConfig":
if self.env == "production" and self.slack_webhook_url is None:
raise ValueError("production 環境では SLACK_WEBHOOK_URL が必須です")
return self
def get_config() -> AppConfig:
return AppConfig()
問題点分析 — カテゴリ A
| # | 問題点 | 分類 | 改善方法 |
|---|---|---|---|
| 1 | 型ヒントが皆無 | 型安全性 Ch2 | 全フィールドに型ヒント付与 |
| 2 | 必須フィールドが None になりうる | エラー処理 Ch10 | gcp_project_id: str(デフォルトなし)で Fail-Fast |
| 3 | 範囲バリデーションなし | 型安全性 Ch2 | Annotated[int, Field(gt=0, le=10_000)] |
| 4 | ENV が任意文字列 | 型安全性 Ch2 | Literal["staging", "production"] |
| 5 | Webhook URL フォーマット検証なし | エラー処理 Ch10 | @field_validator でプレフィックスチェック |
| 6 | print でシークレット漏洩 | セキュリティ | print 除去(設定ロガーの level=DEBUG で管理) |
| 7 | モジュールレベルで即時インスタンス化 | テスト容易性 Ch11 | get_config() ファクトリ関数に切り出す |
模範解答 A
Before — 型なし・バリデーションなし・テスト困難
import os
class Config:
def __init__(self):
self.project_id = os.getenv("GCP_PROJECT_ID")
self.bq_dataset = os.getenv("BQ_DATASET", "mart")
self.batch_size = int(os.getenv("BATCH_SIZE", "100"))
self.retry_limit = int(os.getenv("RETRY_LIMIT", "3"))
self.webhook_url = os.getenv("SLACK_WEBHOOK_URL")
self.env = os.getenv("ENV", "staging")
print(cfg.project_id)
cfg = Config()
After — 型安全・Fail-Fast・テスト可能
"""config.py — Pydantic v2 BaseSettings による設定管理。
Ch2: 型の活用(Annotated, Field, Literal)
Ch10: エラー処理(起動時 Fail-Fast)
Ch11: テスト容易性(ファクトリ関数)
"""
from __future__ import annotations
from typing import Annotated, Literal
from pydantic import Field, field_validator, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
# 定数: マジックストリング排除
SLACK_WEBHOOK_PREFIX: str = "https://hooks.slack.com/"
DEFAULT_BQ_DATASET: str = "mart"
DEFAULT_BATCH_SIZE: int = 100
DEFAULT_RETRY_LIMIT: int = 3
class AppConfig(BaseSettings):
"""Argo Workflows バッチ処理基盤の設定。
環境変数 / .env ファイルから自動読み込み。
バリデーションエラーは起動時に ValidationError で即死。
Attributes:
gcp_project_id: GCP プロジェクト ID(必須)。
bq_dataset: BigQuery データセット名。
batch_size: BQ Insert バッチサイズ(1〜10,000)。
retry_limit: Argo ジョブのリトライ上限(1〜10)。
env: 実行環境("staging" or "production" のみ)。
slack_webhook_url: Slack 通知先 Webhook URL(任意)。
"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
# 必須: デフォルトなし → 未設定で ValidationError(Fail-Fast)
gcp_project_id: str
bq_dataset: str = DEFAULT_BQ_DATASET
# Annotated + Field: 型レベルで制約を表現(Ch2)
batch_size: Annotated[int, Field(gt=0, le=10_000)] = DEFAULT_BATCH_SIZE
retry_limit: Annotated[int, Field(ge=1, le=10)] = DEFAULT_RETRY_LIMIT
# Literal: 許容値を型制約(タイポを起動時に検知)
env: Literal["staging", "production"] = "staging"
slack_webhook_url: str | None = None
@field_validator("slack_webhook_url")
@classmethod
def validate_webhook_prefix(cls, v: str | None) -> str | None:
"""Slack Webhook URL のプレフィックスを検証する。
Raises:
ValueError: URL が所定プレフィックスで始まらない場合。
"""
if v is None:
return v # 未設定は OK
if not v.startswith(SLACK_WEBHOOK_PREFIX):
raise ValueError(
f"SLACK_WEBHOOK_URL は {SLACK_WEBHOOK_PREFIX!r} で"
f"始まる必要があります。受け取った値: {v!r}"
)
return v
@model_validator(mode="after")
def validate_prod_has_webhook(self) -> "AppConfig":
"""本番環境では Slack 通知を必須にする。
Raises:
ValueError: env='production' で webhook が未設定の場合。
"""
if self.env == "production" and self.slack_webhook_url is None:
raise ValueError(
"production 環境では SLACK_WEBHOOK_URL が必須です。"
)
return self
def get_config() -> AppConfig:
"""設定ファクトリ関数(テスト容易性 Ch11)。
モジュールレベルで即時インスタンス化しないことで、
pytest の monkeypatch.setenv で環境変数モックが容易になる。
Returns:
バリデーション済みの AppConfig インスタンス。
Raises:
ValidationError: 必須フィールド未設定 or 制約違反の場合。
"""
return AppConfig()
# ケース1: 正常系(全フィールド設定済み)
import os
os.environ.update({
"GCP_PROJECT_ID": "my-ec-production",
"BATCH_SIZE": "500",
"ENV": "production",
"SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/xxx/yyy/zzz",
})
cfg = get_config()
print(cfg.gcp_project_id) # "my-ec-production"
print(cfg.batch_size) # 500 ← int 型に自動変換
print(cfg.env) # "production"
# ケース2: 必須フィールド GCP_PROJECT_ID 未設定
del os.environ["GCP_PROJECT_ID"]
get_config()
# → pydantic_core.ValidationError: 1 validation error for AppConfig
# gcp_project_id
# Field required [type=missing, input_url=...]
# ケース3: BATCH_SIZE が範囲外
os.environ["GCP_PROJECT_ID"] = "proj"
os.environ["BATCH_SIZE"] = "99999"
get_config()
# → ValidationError: batch_size
# Input should be less than or equal to 10000 [type=less_than_equal]
# ケース4: ENV タイポ
os.environ["BATCH_SIZE"] = "100"
os.environ["ENV"] = "prodution" # タイポ
get_config()
# → ValidationError: env
# Input should be 'staging' or 'production' [type=literal_error]
# ケース5: 不正 Webhook URL
os.environ["ENV"] = "staging"
os.environ["SLACK_WEBHOOK_URL"] = "http://example.com/hook"
get_config()
# → ValidationError: slack_webhook_url
# SLACK_WEBHOOK_URL は 'https://hooks.slack.com/' で始まる必要があります
# ケース6: production で Webhook 未設定(クロスフィールドバリデーション)
os.environ.pop("SLACK_WEBHOOK_URL", None)
os.environ["ENV"] = "production"
get_config()
# → ValidationError: AppConfig
# production 環境では SLACK_WEBHOOK_URL が必須です
| ポイント | 適用した設計原則/パターン | 書籍対応章 |
|---|---|---|
BaseSettings 継承で環境変数を型変換 | 型の活用(Value Object) | Ch2 |
Annotated[int, Field(gt=0, le=10_000)] | 型レベルで制約を表現 | Ch2 |
Literal["staging", "production"] | 型による許容値制限(タイポ検知) | Ch2 |
@field_validator でフォーマット検証 | Fail-Fast エラー処理 | Ch10 |
@model_validator でクロスフィールド検証 | クラス設計(責務の集約) | Ch3 |
get_config() ファクトリ関数 | テスト容易性(依存注入の入口) | Ch11 |
名前付き定数 SLACK_WEBHOOK_PREFIX | マジックストリング排除 | Ch2/Ch7 |
# tests/test_config.py
import pytest
from pydantic import ValidationError
from config import get_config, AppConfig
# pytest の monkeypatch で環境変数を安全にモック
class TestAppConfig:
def test_valid_staging(self, monkeypatch):
"""正常系: staging 設定が正しく読み込まれる。"""
monkeypatch.setenv("GCP_PROJECT_ID", "test-project")
monkeypatch.setenv("BATCH_SIZE", "200")
monkeypatch.setenv("ENV", "staging")
cfg = get_config()
assert cfg.gcp_project_id == "test-project"
assert cfg.batch_size == 200 # str → int 変換済み
assert cfg.env == "staging"
def test_missing_project_id_raises(self, monkeypatch):
"""必須フィールド未設定で ValidationError が上がる。"""
monkeypatch.delenv("GCP_PROJECT_ID", raising=False)
with pytest.raises(ValidationError, match="gcp_project_id"):
get_config()
def test_batch_size_out_of_range(self, monkeypatch):
"""BATCH_SIZE=0 で ValidationError(gt=0 制約違反)。"""
monkeypatch.setenv("GCP_PROJECT_ID", "proj")
monkeypatch.setenv("BATCH_SIZE", "0")
with pytest.raises(ValidationError, match="batch_size"):
get_config()
def test_invalid_env_literal(self, monkeypatch):
"""ENV タイポで ValidationError(Literal 制約違反)。"""
monkeypatch.setenv("GCP_PROJECT_ID", "proj")
monkeypatch.setenv("ENV", "prodution")
with pytest.raises(ValidationError, match="env"):
get_config()
def test_production_without_webhook_raises(self, monkeypatch):
"""production + webhook 未設定でクロスフィールドバリデーション。"""
monkeypatch.setenv("GCP_PROJECT_ID", "proj")
monkeypatch.setenv("ENV", "production")
monkeypatch.delenv("SLACK_WEBHOOK_URL", raising=False)
with pytest.raises(ValidationError, match="production"):
get_config()
問題 B: インフラ — GitHub Actions OIDC × Cloud Run v2 キーレスデプロイ
ECサイトの注文イベントAPIは Cloud Run v2 で稼働しており、現状の課題:
現状の課題:
- GitHub Actions の CI/CD で SA キー JSON を GitHub Secrets に保存してデプロイしている
- SA キーの 90日有効期限管理が属人的 で、切れると本番デプロイが止まる
- Cloud Run サービスが
allUsersにroles/run.invokerを付与(誰でも呼び出し可能) - デプロイ用 SA に
roles/editorが付与(最小権限違反)
要件
| # | 要件 |
|---|---|
| 1 | GitHub Actions OIDC + Workload Identity Federation を Terraform で設定し、SA キーを撤廃すること |
| 2 | デプロイ用 SA の権限を最小化すること(roles/run.developer + roles/artifactregistry.writer のみ) |
| 3 | Cloud Run v2 の認証を IAM に変更し(--no-allow-unauthenticated)、呼び出し元に roles/run.invoker を付与すること |
| 4 | google-github-actions/auth@v2 + google-github-actions/deploy-cloudrun@v2 を使った GitHub Actions ワークフローを作成すること |
ヒント B(段階的開示)
ヒント1 — 方向性
GitHub Actions OIDC の本質は「GitHub のトークン(JWT)を Google の Workload Identity Pool で信頼し、一時的な GCP 認証情報を払い出す」仕組み。SA キーは不要になる。OIDC トークンは GitHub Actions セッション(通常 1h 以内)のみ有効で、漏洩しても即座に無効化される。
attribute_condition で特定リポジトリ・ブランチからのリクエストのみ許可することが重要。
ヒント2 — Terraform のリソース構成
google_iam_workload_identity_pool→ WIF Pool の作成google_iam_workload_identity_pool_provider(OIDC)→ GitHub の issuer_uri を登録google_service_account→ デプロイ用 SA(最小権限)google_project_iam_member× 2 →roles/run.developer+roles/artifactregistry.writergoogle_service_account_iam_member→roles/iam.workloadIdentityUser(principalSet で特定リポジトリ)- GitHub Actions:
permissions: id-token: writeが必須
ヒント3 — Terraform の骨格と GitHub Actions YAML の要点
WIF Pool + Provider(Terraform)
resource "google_iam_workload_identity_pool" "github" {
workload_identity_pool_id = "github-actions-pool"
}
resource "google_iam_workload_identity_pool_provider" "github_oidc" {
workload_identity_pool_id = google_iam_workload_identity_pool.github.workload_identity_pool_id
workload_identity_pool_provider_id = "github-oidc-provider"
oidc {
issuer_uri = "https://token.actions.githubusercontent.com"
}
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.repository" = "assertion.repository"
}
attribute_condition = "attribute.repository == 'my-org/order-service'"
}
GitHub Actions 認証ステップ
permissions:
id-token: write # OIDC トークン発行に必須
contents: read
steps:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: >-
projects/PROJECT_NUMBER/locations/global/
workloadIdentityPools/github-actions-pool/
providers/github-oidc-provider
service_account: >-
cloudrun-deploy-sa@PROJECT_ID.iam.gserviceaccount.com
アーキテクチャ図 — GitHub Actions OIDC → WIF → Cloud Run v2
模範解答 B
# terraform/modules/github-actions-wif/main.tf
# GitHub Actions OIDC → Cloud Run v2 デプロイのキーレス認証基盤
# ── Workload Identity Pool ────────────────────────────────────────────────
resource "google_iam_workload_identity_pool" "github_actions" {
project = var.project_id
workload_identity_pool_id = "github-actions-pool"
display_name = "GitHub Actions WIF Pool"
description = "GitHub Actions OIDC 用 Workload Identity Pool"
}
# ── OIDC プロバイダー(GitHub の issuer_uri を信頼)─────────────────────────
resource "google_iam_workload_identity_pool_provider" "github_oidc" {
project = var.project_id
workload_identity_pool_id = google_iam_workload_identity_pool.github_actions.workload_identity_pool_id
workload_identity_pool_provider_id = "github-oidc-provider"
display_name = "GitHub OIDC Provider"
oidc {
# GitHub の OIDC issuer(固定値)
issuer_uri = "https://token.actions.githubusercontent.com"
}
# GitHub JWT クレーム → GCP attribute のマッピング
attribute_mapping = {
"google.subject" = "assertion.sub" # repo:org/repo:ref:refs/heads/main
"attribute.repository" = "assertion.repository" # "my-org/order-service"
"attribute.ref" = "assertion.ref" # "refs/heads/main"
"attribute.actor" = "assertion.actor" # GitHub ユーザー名(監査ログ用)
}
# セキュリティ境界: 特定リポジトリからのリクエストのみ許可
# フォークリポジトリからのデプロイを防ぐ
attribute_condition = "attribute.repository == 'my-org/order-service'"
}
# ── デプロイ用 Google Service Account(最小権限)────────────────────────────
resource "google_service_account" "deploy_sa" {
project = var.project_id
account_id = "cloudrun-deploy-sa"
display_name = "Cloud Run デプロイ SA(GitHub Actions 専用)"
description = "GitHub Actions OIDC 経由でのみ権限借用可能。SA キーなし。"
}
# Cloud Run v2 デプロイ権限(最小: deploy のみ、delete や IAM 管理は不可)
resource "google_project_iam_member" "deploy_run_developer" {
project = var.project_id
role = "roles/run.developer"
member = "serviceAccount:${google_service_account.deploy_sa.email}"
}
# Artifact Registry へのイメージ Push 権限(roles/editor より遥かに狭い)
resource "google_project_iam_member" "deploy_ar_writer" {
project = var.project_id
role = "roles/artifactregistry.writer"
member = "serviceAccount:${google_service_account.deploy_sa.email}"
}
# ── WIF バインディング: 特定リポジトリのみが SA を借用可能 ─────────────────
# principalSet で「my-org/order-service リポジトリからのリクエスト全て」を対象
resource "google_service_account_iam_member" "github_wif_binding" {
service_account_id = google_service_account.deploy_sa.name
role = "roles/iam.workloadIdentityUser"
# principalSet: Pool 内の特定 attribute を持つ identity の集合
member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github_actions.name}/attribute.repository/my-org/order-service"
}
# ── Cloud Run v2 IAM(認証あり: allUsers 廃止)──────────────────────────────
# 呼び出し元(ここではデプロイ SA + 他の承認 SA)のみに invoker を付与
resource "google_cloud_run_v2_service_iam_member" "invoker" {
project = var.project_id
location = var.region
name = "order-event-api"
role = "roles/run.invoker"
# デプロイ SA を invoker に追加(ヘルスチェック等のために必要)
member = "serviceAccount:${google_service_account.deploy_sa.email}"
}
# terraform/modules/github-actions-wif/outputs.tf
output "workload_identity_provider" {
description = "GitHub Actions ワークフローの workload_identity_provider 入力値"
value = google_iam_workload_identity_pool_provider.github_oidc.name
# 例: projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-oidc-provider
}
output "deploy_service_account_email" {
description = "GitHub Actions が借用するデプロイ用 SA のメールアドレス"
value = google_service_account.deploy_sa.email
}
# .github/workflows/deploy-cloudrun.yaml
# SA キーレスデプロイ: GitHub OIDC → WIF → Cloud Run v2
# google-github-actions/auth@v2 + deploy-cloudrun@v2
name: Deploy to Cloud Run v2
on:
push:
branches: [main]
# OIDC トークン発行に必須(これがないと WIF 認証が失敗する)
permissions:
id-token: write # ← 必須: GitHub OIDC トークン取得
contents: read
env:
PROJECT_ID: my-ec-production
REGION: asia-northeast1
SERVICE_NAME: order-event-api
AR_HOST: asia-northeast1-docker.pkg.dev
AR_REPO: mops-images
IMAGE_NAME: order-event-api
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # GitHub Environments で手動承認を設定可能
steps:
- name: Checkout
uses: actions/checkout@v4
# ── Google 認証(OIDC キーレス)─────────────────────────────────────
- name: Authenticate to Google Cloud
id: auth
uses: google-github-actions/auth@v2
with:
# terraform output で取得した値を GitHub Variables に登録して参照
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: ${{ vars.DEPLOY_SA_EMAIL }}
# SA キーは一切不要。OIDC トークンで一時的な認証情報を取得
# ── Docker イメージビルド & Push ────────────────────────────────────
- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker ${{ env.AR_HOST }} --quiet
- name: Build and Push Docker image
id: build
run: |
# digest ピン留め: latest タグではなく git SHA を使う
IMAGE_TAG="${{ env.AR_HOST }}/${{ env.PROJECT_ID }}/${{ env.AR_REPO }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
docker build --tag "${IMAGE_TAG}" .
docker push "${IMAGE_TAG}"
# IMAGE_TAG を後続ステップで参照できるよう出力
echo "image=${IMAGE_TAG}" >> "${GITHUB_OUTPUT}"
# ── Cloud Run v2 デプロイ ────────────────────────────────────────────
- name: Deploy to Cloud Run v2
id: deploy
uses: google-github-actions/deploy-cloudrun@v2
with:
service: ${{ env.SERVICE_NAME }}
region: ${{ env.REGION }}
image: ${{ steps.build.outputs.image }}
flags: |
--no-allow-unauthenticated
--min-instances=1
--max-instances=10
--memory=512Mi
--cpu=1
--ingress=all
- name: Deployment summary
run: |
echo "✅ Deployed: ${{ steps.deploy.outputs.url }}"
echo " Image: ${{ steps.build.outputs.image }}"
Bad vs Good 設計比較
| 観点 | Bad(現状) | Good(改善後) |
|---|---|---|
| 認証方式 | SA キー JSON を GitHub Secrets に保存 | OIDC 一時トークン(キーレス) |
| キー管理 | 90日期限の属人管理、切れると本番停止、漏洩リスク | キー不要。GitHub Actions セッション(〜1h)のみ有効 |
| デプロイ SA 権限 | roles/editor(過剰:全リソース変更可能) |
roles/run.developer + roles/artifactregistry.writer(最小権限) |
| Cloud Run 認証 | allUsers に roles/run.invoker(誰でも呼び出し可能) |
IAM 認証必須(--no-allow-unauthenticated) |
| 監査ログ | SA キー操作として記録(誰が実行したか不明) | リポジトリ名・ブランチ・actor が Cloud Audit Logs に含まれる |
| イメージタグ | latest タグ(どのビルドか追跡不可) |
:${{ github.sha }}(git コミットと1対1で対応) |
| フォークリポジトリ対策 | なし(フォークから SA キーを使ったデプロイが可能) | attribute_condition で特定リポジトリのみに制限 |
OIDC 認証フロー(時系列)
1. GitHub Actions ランナーが起動し、OIDC JWT トークンを GitHub から取得
JWT クレーム: { "repository": "my-org/order-service", "ref": "refs/heads/main", ... }
2. google-github-actions/auth@v2 が JWT を WIF に送信
→ WIF が attribute_condition を確認:
"attribute.repository == 'my-org/order-service'" → ✅ 一致
3. WIF が一時的な GCP 認証情報(access_token)を払い出す
有効期限: デフォルト 1時間(セッション中のみ)
4. 認証情報で Artifact Registry に docker push
roles/artifactregistry.writer → ✅
5. 認証情報で Cloud Run v2 に gcloud run deploy
roles/run.developer → ✅
6. SA キーは一切使用していない。漏洩するものが存在しない。
ポイント解説
カテゴリ A
1
Pydantic v2 BaseSettings の起動時 Fail-Fast(Ch10)
環境変数の不備をアプリ起動時に ValidationError として検知できる。
環境変数の不備をアプリ起動時に ValidationError として検知できる。
None のまま処理が進んで本番でランダムに落ちる問題を根絶できる。Argo Workflows のバッチは深夜に動くため、「バッチが半分終わったところで None エラー」より「起動直後に設定エラーで即死」の方が圧倒的に対処しやすい。
2
Annotated + Field で「型としての制約」(Ch2)batch_size: Annotated[int, Field(gt=0, le=10_000)] = 100 は「1以上10,000以下の整数」を型レベルで表現する。if ガードや docstring に頼らず、コードが制約の唯一の真実(SSOT) になる。IDE・mypy・Pydantic が全て同じ制約を認識する。
3
「production 環境では slack_webhook_url が必須」というビジネスルールは、
@model_validator でクロスフィールドバリデーション(Ch3)「production 環境では slack_webhook_url が必須」というビジネスルールは、
@field_validator では表現できない(単一フィールドの検証のみ)。@model_validator(mode="after") は全フィールドの値が確定した後に実行され、複数フィールドにまたがる条件を型安全に実装できる。
カテゴリ B
4
GitHub Actions OIDC でキーを「持たない設計」
SA キーは「漏洩リスク・期限管理・ローテーション」が常につきまとう。OIDC 一時トークンは GitHub Actions セッション中のみ有効で、漏洩しても即座に無効化される。「キーを作らない設計」が最も安全な鍵管理。
SA キーは「漏洩リスク・期限管理・ローテーション」が常につきまとう。OIDC 一時トークンは GitHub Actions セッション中のみ有効で、漏洩しても即座に無効化される。「キーを作らない設計」が最も安全な鍵管理。
attribute_condition でリポジトリを限定することで、フォークからの意図しないデプロイも防げる。
5
roles/editor → 最小権限への絞り込みroles/editor は「プロジェクト内のほぼ全リソースを変更できる」権限で、IAM 管理を除けば何でもできる。デプロイ SA に必要なのは roles/run.developer(Cloud Run デプロイのみ)と roles/artifactregistry.writer(イメージ Push のみ)の2つだけ。侵害されても影響範囲をこの2操作に限定できる。
6
Cloud Run
--no-allow-unauthenticated で公開 API を廃止allUsers への roles/run.invoker 付与は「インターネット上の誰でも無制限に API を呼び出せる」状態。DDoS・スクレイピング・意図しない API 濫用のリスクがある。IAM 認証を強制することで、認可された SA・ユーザーのみが呼び出せる。Cloud Endpoints や API Gateway と組み合わせることでさらに細かい認可制御が可能。
実務への応用
- Pydantic v2 Settings は Argo Workflows バッチ設定管理に直結: Argo WorkflowTemplate の
env:セクションからConfigMap/ExternalSecret経由で注入される環境変数を、バッチ起動直後にAppConfigでバリデーションすることで、設定ミスを「バッチ実行の最初の1秒」で検知できる。深夜バッチが途中で落ちるより圧倒的に早い - GitHub Actions OIDC は MOps チームの CI/CD セキュリティ基盤として必須: SA キーを GitHub Secrets に入れている運用は、退職者のキー失効・90日更新・ローテーション忘れで本番デプロイが止まるリスクがある。OIDC に移行することでこれらの運用コストをゼロにできる
get_config()ファクトリ関数でテスト可能な設計:pytestのフィクスチャでmonkeypatch.setenv("GCP_PROJECT_ID", "test-project")としてからget_config()を呼ぶことで、テスト専用の設定を簡単に作れる。モジュールレベルのcfg = Config()ではこれができない- WIF + OIDC は GKE Workload Identity と設計思想が同じ: 前日(05-30)の GKE Workload Identity は「K8s Pod が GSA を借用」、今日の GitHub Actions OIDC は「CI/CD Runner が GSA を借用」。どちらも「キーレスで一時的な権限借用」という同じ設計原則。GCP の認証設計を統一的に理解できる
- 証券マン視点: セキュリティ投資の ROI を数値化: SA キー漏洩インシデント対応コスト(調査・対応・ブランド毀損)は数百万円〜数千万円になりうる。OIDC 移行の工数(3〜5人日)と比べると、ペイバックが「侵害1件を防ぐだけで元が取れる」水準。セキュリティ投資はリスク低減の期待値で ROI を計算する
今日のまとめ
Pydantic v2
GitHub Actions OIDC + Workload Identity Federation でサービスアカウントキーを撤廃し、Cloud Run v2 の
BaseSettings + Annotated + Field + Literal を組み合わせることで、環境変数バリデーションを「型レベル × 起動時 Fail-Fast」で実現でき、実行時の予期せぬ None エラーを根絶できる(Ch2/Ch10)。GitHub Actions OIDC + Workload Identity Federation でサービスアカウントキーを撤廃し、Cloud Run v2 の
--no-allow-unauthenticated で認証を強制することで、「キーを持たない・公開しない」セキュリティ設計が完成する。どちらも「問題を実行時ではなく設計段階で排除する」という同じ設計哲学に基づく。