概要
遷移テーブル(Ch5/Ch7)
if-elif チェーンで書かれた状態遷移を dict[OrderStatus, frozenset[OrderStatus]] に置き換え、新ステータス追加時に既存コード変更ゼロ(OCP)を実現する。
コレクション操作の隠蔽(Ch7)
内部 dict を直接公開せず、意図を表すメソッド(transition, get_orders_by_status)経由のみで操作。外部からの不整合な状態変更を防ぐ。
Workload Identity Federation(GKE)
K8s ServiceAccount → Google Service Account のバインディングを Terraform で管理。ノード SA への過剰権限付与を解消し、Pod 単位で最小権限を実現する。
External Secrets Operator(ESO)
refreshInterval: 1h で Secret Manager → K8s Secret を自動同期。Pod 再起動なしで Secret を更新でき、Kustomize オーバーレイで本番/ステージ差分を吸収する。
問題 A: コーディング — 状態機械の設計(Ch7 コレクション操作)
以下の「悪いコード」は、ECサイトの注文ステータス遷移を管理するクラスです。問題点を全て洗い出し、Ch7(コレクション操作の隠蔽) および Ch2(型の活用) を適用してリファクタリングしてください。
制約・前提条件
- Python 3.12+、型ヒント・
dataclass(slots=True, frozen=True)を使うこと - 注文ステータスを
StrEnumで定義すること - 遷移ルールをデータとして定義し、ロジックから分離すること(OCP)
- 無効遷移は
InvalidTransitionErrorをraiseすること(print禁止) get_orders_by_statusはリスト内包表記またはfilterを使うこと- Google スタイル docstring・インラインコメント・名前付き定数を含めること
期待する回答形式: 問題点の列挙(番号付き)+ 改善後コード + 実行例(input→output)+ 適用した設計パターン名と書籍対応章
悪いコード (Before) — カテゴリ A
このコードには 8つの設計上の問題 が隠れています。見つけてみてください。
bad_order_state.py — 問題だらけの注文ステータス管理
class OrderManager:
def __init__(self):
self.orders = {} # order_id -> status (str)
def update_status(self, order_id, new_status):
current = self.orders.get(order_id)
if current is None:
print(f"order {order_id} not found")
return False
# 問題1: if-elif で遷移ルールをベタ書き(OCP 違反)
if current == "pending":
if new_status in ["confirmed", "cancelled"]:
self.orders[order_id] = new_status
return True
else:
print(f"invalid: {current} -> {new_status}")
return False
elif current == "confirmed":
if new_status in ["shipped", "cancelled"]:
self.orders[order_id] = new_status
return True
else:
print(f"invalid: {current} -> {new_status}")
return False
elif current == "shipped":
if new_status == "delivered":
self.orders[order_id] = new_status
return True
else:
print(f"invalid: {current} -> {new_status}")
return False
elif current == "delivered":
print("already delivered")
return False
elif current == "cancelled":
print("already cancelled")
return False
def get_orders_by_status(self, status):
result = []
for order_id, s in self.orders.items():
if s == status:
result.append(order_id)
return result
def add_order(self, order_id):
self.orders[order_id] = "pending"
問題点サマリー(8点)
1if-elif で遷移ルールをハードコード(OCP 違反 Ch5) — 新ステータス追加のたびに関数を修正しなければならない
2ステータスが str — タイポ不検出 —
"confimed" などのタイポを静的に検知できない3型ヒントが皆無 — mypy / pyright で型検査不可
4print + return False — 呼び出し元が失敗を検知できない — 上位レイヤー(Argo Workflows など)が注文遷移失敗を気づけない
5
self.orders が public — Ch7 コレクション操作の隠蔽違反 — 外部から mgr.orders["ORD-001"] = "delivered" で検証なしに変更可能6
get_orders_by_status がループでリスト構築 — リスト内包表記で意図を簡潔に表現できる7
"pending" などのマジックストリング — 初期ステータスが複数箇所にばらばらに書かれると変更漏れが起きる8終端状態の処理が個別 elif — 遷移テーブルで統一表現できる(
frozenset() = 遷移先なし)ヒント A(段階的開示)
ヒント1 — 方向性
if current == "pending": if new_status in ["confirmed", "cancelled"]: ... という遷移ルールは「遷移 = データ」として辞書に切り出せる。dict[OrderStatus, frozenset[OrderStatus]] が遷移テーブルになり、transition() の本体は「テーブルを引いて in で確認」のみになる。さらに内部 dict を _orders(アンダースコアプレフィックス)にして、外部からはメソッド経由のみでアクセスさせると Ch7 の「コレクション操作の隠蔽」を満たせる。
ヒント2 — アプローチ
OrderStatus(StrEnum)でPENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLEDを定義- 遷移テーブル
VALID_TRANSITIONS: Final[dict[OrderStatus, frozenset[OrderStatus]]]をモジュールレベルの定数として定義 - 終端状態は
frozenset()(空集合)で表現 — 特別な elif は不要 transition()は「テーブルを引いてnew_status not in allowedならInvalidTransitionErrorを raise」のみget_orders_by_statusは[oid for oid, s in self._orders.items() if s == status]
ヒント3 — コードの骨格
from enum import StrEnum
from typing import Final
from dataclasses import dataclass, field
class OrderStatus(StrEnum):
PENDING = "pending"
CONFIRMED = "confirmed"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
# 遷移ルール = データ(if-elif を置き換える)
VALID_TRANSITIONS: Final[dict[OrderStatus, frozenset[OrderStatus]]] = {
OrderStatus.PENDING: frozenset({OrderStatus.CONFIRMED, OrderStatus.CANCELLED}),
OrderStatus.CONFIRMED: frozenset({OrderStatus.SHIPPED, OrderStatus.CANCELLED}),
OrderStatus.SHIPPED: frozenset({OrderStatus.DELIVERED}),
OrderStatus.DELIVERED: frozenset(), # 終端状態
OrderStatus.CANCELLED: frozenset(), # 終端状態
}
class InvalidTransitionError(ValueError): ...
@dataclass
class OrderStateManager:
_orders: dict[str, OrderStatus] = field(default_factory=dict)
def transition(self, order_id: str, new_status: OrderStatus) -> None:
current = self._get_or_raise(order_id)
allowed = VALID_TRANSITIONS[current]
if new_status not in allowed:
raise InvalidTransitionError(...)
self._orders[order_id] = new_status
def get_orders_by_status(self, status: OrderStatus) -> list[str]:
return [oid for oid, s in self._orders.items() if s == status]
問題点分析 — カテゴリ A
| # | 問題点 | 分類 | 改善方法 |
|---|---|---|---|
| 1 | if-elif で遷移ルールをハードコード | OCP 違反 Ch5 | VALID_TRANSITIONS dict に切り出す |
| 2 | ステータスが str — タイポ不検出 | 型安全性 Ch2 | OrderStatus(StrEnum) で型安全化 |
| 3 | 型ヒントが皆無 | 型安全性 Ch2 | 全引数・戻り値に型ヒント付与 |
| 4 | print + return False — 呼び出し元が失敗を検知できない | エラー処理 | InvalidTransitionError / OrderNotFoundError を raise |
| 5 | self.orders が public(Ch7 違反) | コレクション隠蔽 Ch7 | _orders にしてメソッド経由のみ公開 |
| 6 | get_orders_by_status がループでリスト構築 | コレクション操作 Ch7 | リスト内包表記に置き換え |
| 7 | "pending" などのマジックストリング | 定数管理 | INITIAL_STATUS = OrderStatus.PENDING で定数化 |
| 8 | 終端状態が個別 elif | OCP 違反 Ch5 | frozenset() で統一表現 |
模範解答 A
"""order_state.py — 注文ステータス遷移管理(状態機械パターン)。
良いコード・悪いコードで学ぶ設計入門(改訂新版)
- Ch2: 型の活用(StrEnum, 型ヒント, frozen dataclass)
- Ch7: コレクション操作の隠蔽(内部コレクションを直接公開しない)
- Ch5: 条件分岐の削減(遷移テーブルで if-elif を廃止)
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Final
# ── 定数 ─────────────────────────────────────────────────────────────────
INITIAL_STATUS: Final[str] = "pending" # マジックストリング排除
# ── 列挙型(Ch2: 型の活用)────────────────────────────────────────────────
class OrderStatus(StrEnum):
"""注文ステータス。文字列互換でありつつ型安全。"""
PENDING = "pending"
CONFIRMED = "confirmed"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
# ── 遷移テーブル(Ch5: ルール = データ)─────────────────────────────────────
# 新ステータス追加時はここに1行追加するだけ(OCP)
VALID_TRANSITIONS: Final[dict[OrderStatus, frozenset[OrderStatus]]] = {
OrderStatus.PENDING: frozenset({OrderStatus.CONFIRMED, OrderStatus.CANCELLED}),
OrderStatus.CONFIRMED: frozenset({OrderStatus.SHIPPED, OrderStatus.CANCELLED}),
OrderStatus.SHIPPED: frozenset({OrderStatus.DELIVERED}),
OrderStatus.DELIVERED: frozenset(), # 終端状態: frozenset() = 遷移先なし
OrderStatus.CANCELLED: frozenset(), # 終端状態
}
# ── カスタム例外 ─────────────────────────────────────────────────────────
class OrderNotFoundError(KeyError):
"""指定された注文IDが存在しない場合の例外。"""
class InvalidTransitionError(ValueError):
"""許可されていないステータス遷移が要求された場合の例外。"""
# ── 値オブジェクト(Ch3 + Ch4)─────────────────────────────────────────────
@dataclass(slots=True, frozen=True) # frozen=True で不変、slots=True でメモリ効率化
class Order:
"""注文を表す値オブジェクト。不変(frozen=True)で副作用なし。
Attributes:
order_id: 注文ID。
status: 現在のステータス。
"""
order_id: str
status: OrderStatus
# ── 状態管理クラス(Ch7: コレクション操作の隠蔽)────────────────────────────
@dataclass
class OrderStateManager:
"""注文ステータスの状態機械。
内部コレクション(_orders)を直接公開せず、
意図が明確なメソッド経由でのみ操作する(Ch7)。
"""
_orders: dict[str, OrderStatus] = field(default_factory=dict)
def add_order(self, order_id: str) -> None:
"""新規注文を初期ステータス(PENDING)で登録する。
Args:
order_id: 注文ID。
Raises:
ValueError: 既に存在する注文IDが指定された場合。
"""
if order_id in self._orders:
raise ValueError(f"Order already exists: {order_id!r}")
self._orders[order_id] = OrderStatus.PENDING # 定数経由(マジックストリング排除)
def transition(self, order_id: str, new_status: OrderStatus) -> None:
"""注文ステータスを遷移させる。
Args:
order_id: 注文ID。
new_status: 遷移先ステータス。
Raises:
OrderNotFoundError: 注文IDが存在しない場合。
InvalidTransitionError: 許可されていない遷移が要求された場合。
Example:
>>> mgr = OrderStateManager()
>>> mgr.add_order("ORD-001")
>>> mgr.transition("ORD-001", OrderStatus.CONFIRMED)
"""
current = self._get_or_raise(order_id)
allowed = VALID_TRANSITIONS[current] # テーブルを引くだけ(if-elif なし)
if new_status not in allowed:
raise InvalidTransitionError(
f"Cannot transition order {order_id!r}: "
f"{current.value!r} -> {new_status.value!r}. "
f"Allowed next: {[s.value for s in allowed] or 'none (terminal state)'}"
)
self._orders[order_id] = new_status
def get_status(self, order_id: str) -> OrderStatus:
"""指定注文の現在ステータスを返す。
Args:
order_id: 注文ID。
Returns:
現在のステータス。
Raises:
OrderNotFoundError: 注文IDが存在しない場合。
"""
return self._get_or_raise(order_id)
def get_orders_by_status(self, status: OrderStatus) -> list[str]:
"""指定ステータスの注文ID一覧を返す(Ch7: コレクション操作の隠蔽)。
Args:
status: フィルタリングするステータス。
Returns:
該当する注文IDのリスト。
"""
# リスト内包表記で外部にコレクション操作を漏らさない(Ch7)
return [oid for oid, s in self._orders.items() if s == status]
def snapshot(self) -> list[Order]:
"""全注文の不変スナップショットを返す。
Returns:
Order 値オブジェクトのリスト(コピー)。
"""
return [Order(oid, s) for oid, s in self._orders.items()]
def _get_or_raise(self, order_id: str) -> OrderStatus:
"""order_id が存在しない場合に OrderNotFoundError を送出するヘルパー。"""
status = self._orders.get(order_id)
if status is None:
raise OrderNotFoundError(f"Order not found: {order_id!r}")
return status
正常系(複数注文のステータス遷移)
mgr = OrderStateManager()
mgr.add_order("ORD-001")
mgr.add_order("ORD-002")
mgr.add_order("ORD-003")
mgr.transition("ORD-001", OrderStatus.CONFIRMED)
mgr.transition("ORD-002", OrderStatus.CONFIRMED)
mgr.transition("ORD-002", OrderStatus.SHIPPED)
mgr.transition("ORD-003", OrderStatus.CANCELLED)
出力(各ステータスのフィルタリング)
print(mgr.get_orders_by_status(OrderStatus.CONFIRMED)) # ['ORD-001']
print(mgr.get_orders_by_status(OrderStatus.SHIPPED)) # ['ORD-002']
print(mgr.get_orders_by_status(OrderStatus.CANCELLED)) # ['ORD-003']
異常系(無効遷移)
try:
mgr.transition("ORD-001", OrderStatus.DELIVERED) # CONFIRMED → DELIVERED は不可
except InvalidTransitionError as e:
print(e)
# → Cannot transition order 'ORD-001': 'confirmed' -> 'delivered'.
# Allowed next: ['shipped', 'cancelled']
try:
mgr.transition("ORD-003", OrderStatus.CONFIRMED) # CANCELLED は終端状態
except InvalidTransitionError as e:
print(e)
# → Cannot transition order 'ORD-003': 'cancelled' -> 'confirmed'.
# Allowed next: none (terminal state)
新ステータス追加(transition() は変更不要)
# OrderStatus に RETURNING を追加
class OrderStatus(StrEnum):
...
RETURNING = "returning" # ← 1行追加
# 遷移テーブルに追加するだけ
VALID_TRANSITIONS = {
...
OrderStatus.DELIVERED: frozenset({OrderStatus.RETURNING}), # ← 変更
OrderStatus.RETURNING: frozenset({OrderStatus.CANCELLED}), # ← 追加
}
# OrderStateManager.transition() は変更不要(OCP)
| ポイント | 適用した設計原則/パターン | 書籍対応章 |
|---|---|---|
VALID_TRANSITIONS dict で if-elif 廃止 | State Machine + Registry Pattern | Ch5 条件分岐の削減 |
OrderStatus(StrEnum) で型安全化 | Value Object(型の活用) | Ch2 型の活用 |
_orders をメソッド経由のみ公開 | コレクション操作の隠蔽 | Ch7 |
Order(frozen=True, slots=True) | 不変の活用 | Ch4 |
InvalidTransitionError を raise | Fail-Fast エラー処理 | Ch10 |
終端状態を frozenset() で統一表現 | Open-Closed Principle | Ch5/Ch7 |
問題 B: インフラ — Terraform × GKE Autopilot Workload Identity + ESO v0.9+
ECサイトの注文 API が GKE Autopilot 上で稼働しており、現状は以下の問題がある:
現状の課題:
- Cloud SQL の接続情報(DSN)が Deployment の
envにベタ書きされている - GKE ノードの default service account に
roles/secretmanager.secretAccessorが付与されている(最小権限違反) - Secret の更新時に Pod の再起動が必要
- ステージング/本番でマニフェストを手動コピーしている
要件
| # | 要件 |
|---|---|
| 1 | Workload Identity Federation for GKE(v2 API)を Terraform で設定すること |
| 2 | K8s ServiceAccount → Google Service Account の IAM バインディングを Terraform で管理すること |
| 3 | Cloud SQL DSN を Secret Manager に格納し、external-secrets-operator(ESO v0.9+)で K8s Secret に同期すること |
| 4 | ESO の ExternalSecret で refreshInterval: 1h を設定し、Pod 再起動なしで Secret を更新できるようにすること |
| 5 | Kustomize のオーバーレイ(base / overlays/staging / overlays/production)で環境差分を吸収すること |
ヒント B(段階的開示)
ヒント1 — 方向性
Workload Identity の本質は「K8s Pod が GSA(Google Service Account)の権限を借用する」仕組み。ノード SA に権限を付与する旧パターンは「全 Pod が同じ権限を持つ」問題があった。v2(
iam.workloadIdentityUser)では google_service_account_iam_member で serviceAccount:PROJECT.svc.id.goog[NAMESPACE/KSA] を principal に指定することで、特定 Namespace の特定 KSA のみに借用権限を付与できる。ESO はこの権限を使って Secret Manager から値を取得し、K8s Secret として同期する。
ヒント2 — アプローチ
- Terraform:
google_service_account→google_project_iam_member(roles/secretmanager.secretAccessor)→google_service_account_iam_member(Workload Identity バインディング) - K8s ServiceAccount に
iam.gke.io/gcp-service-accountannotation を付与 - ESO:
ClusterSecretStoreにauth.workloadIdentityを設定し、ExternalSecretでsecretStoreRefを参照 - Kustomize:
base/に共通リソース、overlays/に JSON patch で project_id を差し替え
ヒント3 — Terraform の骨格
Workload Identity バインディング
resource "google_service_account_iam_member" "wi" {
service_account_id = google_service_account.order_api.name
role = "roles/iam.workloadIdentityUser"
# KSA → GSA のバインディング
member = "serviceAccount:${var.project_id}.svc.id.goog[${var.k8s_namespace}/${var.ksa_name}]"
}
ESO ExternalSecret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: order-api-secrets
spec:
refreshInterval: 1h # Pod 再起動なしで同期
secretStoreRef:
name: gcp-secretmanager-store
kind: ClusterSecretStore
target:
name: order-api-secrets
creationPolicy: Owner
data:
- secretKey: cloud_sql_dsn
remoteRef:
key: order-api-cloud-sql-dsn
version: latest
アーキテクチャ図 — Workload Identity + ESO + Kustomize
模範解答 B
# modules/gke-workload-identity/main.tf
# ── Google Service Account(GSA)──────────────────────────────────────────
resource "google_service_account" "order_api" {
account_id = "order-api-sa"
display_name = "Order API — GKE Workload Identity SA"
description = "ECサイト注文APIがSecretManagerにアクセスするためのGSA"
project = var.project_id
}
# ── Secret Manager へのアクセス権限(最小権限)──────────────────────────────
# ノード SA ではなく、この GSA にのみ secretAccessor を付与
resource "google_project_iam_member" "order_api_secret_accessor" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.order_api.email}"
}
# ── Workload Identity バインディング(KSA → GSA)────────────────────────────
# K8s Pod(KSA)が GSA の権限を借用できるようにする
# principal 形式: serviceAccount:.svc.id.goog[/]
resource "google_service_account_iam_member" "workload_identity_binding" {
service_account_id = google_service_account.order_api.name
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${var.project_id}.svc.id.goog[${var.k8s_namespace}/${var.k8s_service_account_name}]"
}
# ── Secret Manager シークレット定義 ────────────────────────────────────────
resource "google_secret_manager_secret" "cloud_sql_dsn" {
secret_id = "order-api-cloud-sql-dsn"
project = var.project_id
replication {
auto {} # 自動レプリケーション(単一リージョン指定不要)
}
labels = {
app = "order-api"
environment = var.environment
}
}
resource "google_secret_manager_secret_version" "cloud_sql_dsn" {
secret = google_secret_manager_secret.cloud_sql_dsn.id
secret_data = var.cloud_sql_dsn
lifecycle {
ignore_changes = [secret_data] # CI/CD パイプライン側で更新させる
}
}
# modules/gke-workload-identity/variables.tf
variable "project_id" {
type = string
description = "GCP プロジェクト ID"
}
variable "environment" {
type = string
description = "環境識別子(staging / production)"
validation {
condition = contains(["staging", "production"], var.environment)
error_message = "environment は staging または production のみ許可"
}
}
variable "k8s_namespace" {
type = string
description = "K8s Namespace(例: production)"
}
variable "k8s_service_account_name" {
type = string
description = "K8s ServiceAccount 名(例: order-api-ksa)"
}
variable "cloud_sql_dsn" {
type = string
sensitive = true
description = "Cloud SQL DSN(Secret Manager に格納する値)"
}
# k8s/base/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: order-api-ksa
namespace: production
annotations:
# Workload Identity Federation for GKE — GSA を紐付ける
iam.gke.io/gcp-service-account: order-api-sa@PROJECT_ID.iam.gserviceaccount.com
# k8s/base/clustersecretstore.yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: gcp-secretmanager-store
spec:
provider:
gcpsm:
projectID: PROJECT_ID # overlays で kustomize patch で上書き
auth:
workloadIdentity:
clusterLocation: asia-northeast1
clusterName: order-api-cluster
serviceAccountRef:
name: order-api-ksa
namespace: production
# k8s/base/externalsecret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: order-api-secrets
namespace: production
spec:
# refreshInterval: Pod 再起動なしで1時間ごとに Secret Manager から同期
refreshInterval: 1h
secretStoreRef:
name: gcp-secretmanager-store
kind: ClusterSecretStore
target:
name: order-api-secrets # 生成される K8s Secret 名
creationPolicy: Owner # ExternalSecret 削除時に K8s Secret も削除
template:
type: Opaque
data:
# K8s Secret のキー名 → Secret Manager からの値をテンプレートで注入
DATABASE_URL: "{{ .cloud_sql_dsn }}"
data:
- secretKey: cloud_sql_dsn
remoteRef:
key: order-api-cloud-sql-dsn # Secret Manager のシークレット名
version: latest
ディレクトリ構成
k8s/
├── base/
│ ├── kustomization.yaml # 共通リソース一覧
│ ├── serviceaccount.yaml # order-api-ksa
│ ├── clustersecretstore.yaml # gcp-secretmanager-store
│ ├── externalsecret.yaml # refreshInterval: 1h
│ └── deployment.yaml # order-event-api Deployment
├── overlays/
│ ├── staging/
│ │ ├── kustomization.yaml # base を参照 + patches 適用
│ │ └── patch-project-id.yaml # PROJECT_ID を staging 用に上書き
│ └── production/
│ ├── kustomization.yaml
│ └── patch-project-id.yaml # PROJECT_ID を production 用に上書き
overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: patch-project-id.yaml
target:
kind: ClusterSecretStore
name: gcp-secretmanager-store
overlays/production/patch-project-id.yaml
- op: replace
path: /spec/provider/gcpsm/projectID
value: "my-ec-production-project" # 本番 project_id
デプロイコマンド
# 本番デプロイ(マニフェスト手動コピー不要)
kubectl apply -k k8s/overlays/production/
# ステージングデプロイ
kubectl apply -k k8s/overlays/staging/
Workload Identity: 旧パターン vs v2 の比較
| 観点 | 旧パターン(ノード SA に権限付与) | Workload Identity v2 |
|---|---|---|
| 権限スコープ | ノード上の全 Pod が同じ権限を持つ → 不要な Pod も Secret にアクセス可能 |
特定 Namespace の特定 KSA にのみ権限を付与 → Pod 単位の最小権限 |
| 設定箇所 | ノード SA の IAM + GKE 設定 | Terraform で google_service_account_iam_member + KSA annotation のみ |
| 監査ログ | ノード SA での操作として記録(どの Pod かわからない) | KSA 名がログに含まれる(どの Pod の操作かトレース可能) |
| Secret ローテーション | Pod 再起動が必要 | ESO refreshInterval で自動同期(再起動不要) |
ESO refreshInterval の挙動
| 設定 | 挙動 | ユースケース |
|---|---|---|
refreshInterval: 1h | 1時間ごとに SM → K8s Secret を同期 | DBパスワードのローテーション |
refreshInterval: 0 | 同期しない(初回のみ) | 変更なし・高頻度アクセス制限 |
| Secret をファイルマウント | 同期後すぐに反映(Pod 再起動不要) | アプリが動的に設定ファイルを読む場合 |
| Secret を env マウント | Pod 再起動後に反映 | 環境変数は Pod 起動時に固定されるため |
ポイント解説
カテゴリ A
1
遷移テーブル(
ステータス遷移ルールを
VALID_TRANSITIONS)で if-elif を根絶(Ch5)ステータス遷移ルールを
dict[OrderStatus, frozenset[OrderStatus]] として定義すると、遷移の追加・変更はテーブルの1行変更で済む。終端状態は frozenset()(空集合)で統一表現でき、特別な elif current == "delivered": print("already delivered") が不要になる。
2
コレクション操作の隠蔽(Ch7)
self._orders(アンダースコアプレフィックス)にして transition(), get_orders_by_status(), snapshot() 経由のみでアクセスさせる。外部から mgr._orders["ORD-001"] = "delivered" という検証なしの変更を防ぐことで、状態機械の整合性を保証する。
3
注文エンティティを不変オブジェクトとして表現することで、意図しない状態変更によるバグを防ぐ。
Order(frozen=True, slots=True) 値オブジェクト(Ch4)注文エンティティを不変オブジェクトとして表現することで、意図しない状態変更によるバグを防ぐ。
snapshot() が Order のリストを返すので、呼び出し側が取得したスナップショットを変更しても内部状態に影響しない。
カテゴリ B
4
Workload Identity v2 で Pod 単位の最小権限を実現
google_service_account_iam_member の member に serviceAccount:PROJECT.svc.id.goog[NAMESPACE/KSA] を指定することで、特定 Namespace の特定 KSA のみが GSA 権限を借用できる。ノード SA への権限付与と異なり、同一ノード上の他 Pod には権限が及ばない。
5
ESO
External Secrets Operator が定期的に Secret Manager から値を取得し K8s Secret を更新する。Secret をファイルとしてマウントしている場合は Pod 再起動なしで新しい値が反映される。DBパスワードのローテーションや証明書の更新で再起動が不要になり、ダウンタイムを削減できる。
refreshInterval: 1h で Pod 再起動なし Secret 更新External Secrets Operator が定期的に Secret Manager から値を取得し K8s Secret を更新する。Secret をファイルとしてマウントしている場合は Pod 再起動なしで新しい値が反映される。DBパスワードのローテーションや証明書の更新で再起動が不要になり、ダウンタイムを削減できる。
6
Kustomize オーバーレイで環境差分を1箇所に集約
base/ に共通マニフェストを置き、overlays/staging/ と overlays/production/ の JSON patch(patch-project-id.yaml)でプロジェクト ID だけ差し替える。kubectl apply -k overlays/production/ で本番デプロイが完結し、手動コピーによる設定ミスを防げる。
実務への応用
- カテゴリ A: MOpsチームの施策ステータス(
DRAFT → SCHEDULED → RUNNING → COMPLETED / FAILED)は同様の状態機械パターンが当てはまる。遷移テーブルを dict で管理することで、「COMPLETED に戻ってしまった」「RUNNING のまま DRAFT に戻せた」というバグをコンパイル時に防げる。Argo Workflows のワークフローステップ管理にも応用可能。 - カテゴリ B: GKE Autopilot で稼働するデータパイプライン(BigQuery API, Secret Manager)へのアクセスを Workload Identity + ESO に統一することで、default SA への過剰な権限付与を解消できる。本番/ステージングの Terraform state を
environments/production/terraform.tfvarsとenvironments/staging/terraform.tfvarsで分けて管理する運用と組み合わせると、インフラコードのセキュリティ審査も効率化できる。
今日のまとめ
状態遷移ルールを
インフラ側では Workload Identity(KSA → GSA のバインディング)で Pod 単位の最小権限を実現し、ESO の
dict[OrderStatus, frozenset[OrderStatus]] で表現すると if-elif チェーンを根絶でき(Ch5)、内部コレクションをメソッド経由のみで公開することで整合性が保証される(Ch7)。インフラ側では Workload Identity(KSA → GSA のバインディング)で Pod 単位の最小権限を実現し、ESO の
refreshInterval で Pod 再起動なしの Secret 更新を可能にする。Kustomize オーバーレイにより本番/ステージングの差分を1箇所の patch に集約でき、手動コピーによる設定ミスをゼロにできる。