目次
31 章

フルスタックアプリを EKS に配備する

6部キャップストーン — 本書の最後の章です。React の Next.js (App Router + RSC + Server Actions) アプリと、モダンPython の FastAPI (SQLAlchemy 2.x + Pydantic v2) アプリを、同じ TODO ドメインの上で1つの EKS クラスタに一緒に配備します。Terraform + Karpenter + IRSA + ALB Controller + ExternalDNS + cert-manager のクラスタセットアップから、RDS + External Secrets + RDS IAM auth の DB 連携、Helm + ArgoCD ApplicationSet の環境別配備、Prometheus + Grafana + Loki + OpenTelemetry の可観測性、HPA + Karpenter のオートスケーリング、k6 負荷テスト + OpenCost コスト推定、そして第26章 + 第30章の運用サイクルの適用までを13本の PR で一連の流れとして扱います。第1章〜第30章のすべての道具が1つのシステムの中でどう噛み合うのかの視野が、本キャップストーンで手に入ります。

本書の最後の章です。6部キャップストーンは、第1章〜第30章のすべての道具が1つのシステムの中でどう噛み合うのかを1つのプロジェクトとして束ねる総合実習です。架空の会社ではなく、本シリーズの他の2冊の本の成果物をそのまま入力として持ち込みます — React 6部の Next.js TODO アプリと モダンPython 4部の FastAPI TODO バックエンドが同じドメインの上で動作しています。本章ではその2つを1つの EKS クラスタの上に一緒に配備して、Kubernetes トラックのすべての要素を1つのシステムの中で再び確認します。

本章の目標は次のとおりです。

  • https://todo.example.com に Next.js が、https://api.todo.example.com に FastAPI が立ち上がっている状態
  • RDS PostgreSQL がバックアップ · Multi-AZ · External Secrets と連携された状態
  • GitHub push → ECR → ArgoCD ApplicationSet 自動同期の一連の流れ
  • Prometheus + Grafana + Loki + OpenTelemetry の可観測性スタックが2つのアプリを同じ方向で観測する状態
  • HPA + Karpenter がトラフィックの変動に自動反応する状態
  • 月に約 $80 ~ $120 の運用コストの仮説が OpenCost で検証された状態

進行は 13本の PR 単位です。各 PR が次の PR の入力になる累積構造で、1つの PR の変更量は意図的に小さく置いてレビュー可能な大きさに維持します。

目標のアーキテクチャ #

todo システムの一枚絵
[Browser]
   |
   | HTTPS (Route 53 + ACM)
   v
[ALB] -- AWS Load Balancer Controller
   |
   |-- /          -> [Next.js Pod x N] (SSR + RSC + Server Actions)
   `-- /api/*     -> [FastAPI Pod x M] (REST + Pydantic v2)
                          |
                          | PgBouncer
                          v
                       [RDS PostgreSQL] (Multi-AZ)
                          ^
                          |
                       [External Secrets] <- [AWS Secrets Manager]
                          ^
                          | IRSA
                       [ServiceAccount]

この図が本章の13 PR が到達する最終形態です。図の各矢印が本書の1章以上で解かれたつながりです — 本章はそれらを1つのシステムとして束ねる段階です。

PR #1 — ドメインとアーキテクチャの決定 #

最初の PR はコードなしの ADR (Architecture Decision Record) 1枚です。

docs/adr/0001-eks-architecture.md
# ADR-0001: フルスタック todo システムの K8s 配備アーキテクチャ

## コンテキスト
Next.js (App Router + RSC) + FastAPI + PostgreSQL の todo システムを
運用環境に配備しなければならない。

## オプション
1. ECS Fargate (マネージドコンテナ)
2. EKS (Kubernetes)
3. Lambda + RDS (サーバーレス)

## 決定
EKS を採用。

## 根拠
- 2つのアプリ (Next.js + FastAPI) の役割が異なり、ライフサイクルの隔離が必要
- HPA · Karpenter のオートスケーリングのモデルがトラフィックパターンに合う
- GitOps (ArgoCD) の運用標準モデルを活用
- 本書の 1 ~ 30 章のすべての道具の総合検証

## 結果
- 月 $80 ~ $120 のコスト仮説 (28章のモデルで検証予定)
- 運用カレンダー (26章) の定期サイクルを適用
- AWS 本の ECS Fargate 章と比較学習が可能

AWS の同じキャップストーンが ECS Fargate のルートを扱うので、2冊の本を比べると 「K8s vs マネージドコンテナ」の運用上の違い が明確に見えます。本章は K8s を選んだあとの流れを本格的に扱います。

PR #2 — EKS クラスタの新規セットアップ #

21章 EKS クラスタセットアップ の Terraform マニフェストが入力です。本キャップストーンでは1つだけ前提を変えます — Karpenter を最初から導入 します。

terraform/main.tf
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  # ... 21章のまま
}

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "todo-${var.env}"
  cluster_version = "1.32"
  enable_irsa     = true

  cluster_addons = {
    coredns            = { most_recent = true }
    kube-proxy         = { most_recent = true }
    vpc-cni            = { most_recent = true }
    aws-ebs-csi-driver = {
      most_recent              = true
      service_account_role_arn = module.ebs_csi_irsa.iam_role_arn
    }
  }

  # 最小ノードのみ ON_DEMAND で維持、残りは Karpenter がその場で
  eks_managed_node_groups = {
    system = {
      desired_size   = 2
      min_size       = 2
      max_size       = 3
      instance_types = ["t3.medium"]
      capacity_type  = "ON_DEMAND"
      labels         = { role = "system" }
      taints = [{
        key    = "system"
        value  = "true"
        effect = "NO_SCHEDULE"
      }]
    }
  }
}

module "karpenter" {
  source = "terraform-aws-modules/eks/aws//modules/karpenter"
  cluster_name        = module.eks.cluster_name
  irsa_oidc_provider_arn = module.eks.oidc_provider_arn
}

system ノードグループは Karpenter · CoreDNS · モニタリングスタックのようなシステムコンポーネントだけを置き、アプリケーション (Next.js / FastAPI) ワークロードは Karpenter が立ち上げるノードへ流すパターンです。13章 オートスケーリング の Karpenter モデル + 28章 コスト最適化 §「Karpenter — Cluster Autoscaler との決定ツリー」を組み合わせた形です。

kustomize/karpenter/default-nodepool.yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64", "arm64"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ["t", "m", "c"]
        - key: karpenter.k8s.aws/instance-cpu
          operator: In
          values: ["2", "4", "8"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 30s
    budgets:
      - nodes: "10%"
        duration: 10m
        schedule: "0 9 * * mon-fri"

disruption.budgets30章 アップグレード戦略 の blast radius の要点です — 平日の業務時間に一度に 10 % 以下のノードのみ置き換えます。

補助コンポーネントのセット #

ALB Controller + ExternalDNS + cert-manager
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system --set clusterName=todo-prod --set serviceAccount.create=false

helm install external-dns external-dns/external-dns \
  -n external-dns --create-namespace \
  --set provider=aws --set "domainFilters[0]=todo.example.com"

helm install cert-manager jetstack/cert-manager \
  -n cert-manager --create-namespace --set installCRDs=true

22章 アプリ配備の骨格 §「cert-manager と external-dns」で挙げたセットアップそのままです。

PR #3 — ネームスペース / RBAC / NetworkPolicy の骨格 #

ワークロードを立ち上げる前に隔離の骨格を定めておきます。

kustomize/namespaces/todo.yaml
---
apiVersion: v1
kind: Namespace
metadata:
  name: todo-frontend
  labels:
    team: web
    env: prod
    role: frontend
---
apiVersion: v1
kind: Namespace
metadata:
  name: todo-backend
  labels:
    team: backend
    env: prod
    role: backend
---
apiVersion: v1
kind: Namespace
metadata:
  name: todo-data
  labels:
    team: backend
    env: prod
    role: data

3つのネームスペースの分離 — frontend / backend / data — が本キャップストーンの隔離の単位です。7章 Namespace とラベル のラベル標準 (team / env / role) が 25章 モニタリング · アラート のグルーピングキーであり、28章 のコスト配分のキーとして活用されます。

NetworkPolicy — 隔離の本格 #

netpol — frontend のみ backend api を呼べる
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: todo-backend-ingress
  namespace: todo-backend
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: todo-api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              role: frontend
        - namespaceSelector:
            matchLabels:
              role: backend   # 同じ backend の他のワークロードも許可
      ports:
        - port: 8000

14章 RBAC / NetworkPolicy / ResourceQuota の NetworkPolicy モデルが本格的な隔離へつながります。frontend が直接 RDS へ行けず、必ず backend を経由する 強制された流れです。

ResourceQuota — チーム別の上限 #

quota — backend ネームスペースの上限
apiVersion: v1
kind: ResourceQuota
metadata:
  name: todo-backend-quota
  namespace: todo-backend
spec:
  hard:
    requests.cpu: "10"
    requests.memory: "20Gi"
    limits.cpu: "20"
    limits.memory: "40Gi"
    persistentvolumeclaims: "5"

14章 の ResourceQuota がマルチチーム環境のコスト隔離の最初の保護線です。

PR #4 — PostgreSQL RDS + External Secrets #

23章 DB 連携 の Terraform マニフェストがほぼそのまま入ってきます。違いは dev で Aurora Serverless v2 をオプションとして置く点です。

terraform/modules/todo-rds/main.tf
module "rds" {
  source  = "terraform-aws-modules/rds/aws"
  version = "~> 6.0"

  identifier = "todo-${var.env}"

  engine               = "postgres"
  engine_version       = "16.3"
  major_engine_version = "16"
  instance_class       = var.env == "prod" ? "db.t4g.small" : "db.t4g.micro"

  allocated_storage             = 20
  manage_master_user_password   = true
  multi_az                      = var.env == "prod"
  backup_retention_period       = var.env == "prod" ? 30 : 7
  performance_insights_enabled  = true
  deletion_protection           = var.env == "prod"
}

コスト仮説を小さく置くためにインスタンスクラスを db.t4g.small と置きます — 23 章の db.m6g.large より小さいオプションです。todo ドメインの負荷が小さいので十分です。

ExternalSecret — todo-api の DB 認証情報
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: todo-api-db
  namespace: todo-backend
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: todo-api-db
    template:
      data:
        DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@pgbouncer.todo-backend.svc:5432/todo?sslmode=disable"
  data:
    - secretKey: username
      remoteRef:
        key: rds!cluster-todo-prod
        property: username
    - secretKey: password
      remoteRef:
        key: rds!cluster-todo-prod
        property: password

23章 のマニフェストそのままであり、29章 シークレット運用 §「パスワード0」の RDS IAM auth は本キャップストーンではオプションとして置きます — todo のトラフィックが小さいので PgBouncer + パスワードモデルで十分です。

PR #5 — FastAPI バックエンドの配備 #

モダンPython 4部キャップストーンの FastAPI todo バックエンドが入力です。(modern-python は旧 Python 講座との差別の意味を活かして「モダン」の接頭辞を維持します。) コンテナ化は本書の範囲外ですが、Dockerfile の核心を挙げておきます。

Dockerfile — multi-stage
FROM python:3.13-slim AS builder
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen --no-dev

FROM python:3.13-slim AS runtime
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY src/ src/
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

Deployment #

charts/todo-api/templates/deployment.yaml — 一部
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todo-api
  namespace: todo-backend
  labels:
    app.kubernetes.io/name: todo-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: todo-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app.kubernetes.io/name: todo-api
    spec:
      serviceAccountName: todo-api
      containers:
        - name: api
          image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/todo-api:1.0.0
          ports:
            - containerPort: 8000
              name: http
          envFrom:
            - secretRef:
                name: todo-api-db
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          readinessProbe:
            httpGet:
              path: /health/ready
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health/live
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 10"]
      terminationGracePeriodSeconds: 60

22章 アプリ配備の骨格 の標準マニフェストに 30章 の graceful shutdown の要素 (preStop + terminationGracePeriodSeconds) まで組み合わせた形です。

ServiceAccount + IRSA #

todo-api の ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
  name: todo-api
  namespace: todo-backend
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/todo-prod-api
automountServiceAccountToken: false   # 16章のセキュリティの要点

16章 RBAC / ServiceAccount 深掘り の IRSA + 29章 シークレット運用 §「automountServiceAccountToken: false」のセキュリティの要点が1つのマニフェストの中にあります。

PR #6 — Next.js フロントの配備 #

React 6部キャップストーンの Next.js TODO アプリが入力です。App Router + RSC + Server Actions のモデルが K8s の中では次のように動作します。

Next.js (App Router) が K8s の中で
[Browser]
   |
   | HTTPS
   v
[ALB]
   |
   v
[Next.js Pod]  -- Node.js サーバー (next start)
   |
   | RSC レンダリング時 fetch
   v
[todo-api Service]  -- ClusterIP, FastAPI を指す
   |
   v
[todo-api Pod]

Server Actions は Next.js Pod の中でそのまま実行されます。外部 API 呼び出しが必要な場合は同じクラスタの中の todo-api Service へ呼びます。

charts/todo-web/templates/deployment.yaml — 一部
apiVersion: apps/v1
kind: Deployment
metadata:
  name: todo-web
  namespace: todo-frontend
spec:
  replicas: 2
  template:
    spec:
      serviceAccountName: todo-web
      containers:
        - name: web
          image: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/todo-web:1.0.0
          ports:
            - containerPort: 3000
              name: http
          env:
            - name: TODO_API_URL
              value: "http://todo-api.todo-backend.svc.cluster.local:80"
            - name: NODE_ENV
              value: "production"
          resources:
            requests:
              cpu: 200m
              memory: 256Mi   # SSR + RSC のメモリ仮説
            limits:
              cpu: 1
              memory: 512Mi

Next.js Pod のメモリ仮説は 11章 リソース要求と上限 の測定結果で定めます — SSR + RSC の1リクエスト当たりのメモリ占有が一定程度累積されるので、requests を 256 Mi と置くのが保守的な出発点です。28章 コスト最適化 の VPA recommendation で1か月後に適正値へ収束させます。

PR #7 — Ingress + ALB #

ingress — 2つのホストの1つの ALB
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: todo
  namespace: todo-frontend
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
    alb.ingress.kubernetes.io/ssl-redirect: '443'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:...
    alb.ingress.kubernetes.io/group.name: todo
    external-dns.alpha.kubernetes.io/hostname: "todo.example.com,api.todo.example.com"
spec:
  rules:
    - host: todo.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: todo-web
                port:
                  number: 80
    - host: api.todo.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: todo-api.todo-backend
                port:
                  number: 80

alb.ingress.kubernetes.io/group.name: todo が決定的 — 2つのホストが 同じ ALB 1台を共有 します。28章 コスト最適化 §「ALB の LCU」で挙げたコスト削減パターンが本節で直接適用されます。

external-dns が2つのホストの A レコードを Route 53 に自動登録し、ACM 証明書はワイルドカード (*.todo.example.com) で1枚あれば十分です。22章 の Ingress マニフェストがマルチホストパターンへ拡張された形です。

PR #8 — Helm チャートで束ねる #

ここまで書いたマニフェストを Helm チャート2つで束ねます。

charts/ ディレクトリ
charts/
├── todo-web/
│   ├── Chart.yaml
│   ├── values.yaml
│   ├── values-dev.yaml
│   ├── values-prod.yaml
│   └── templates/
│       ├── deployment.yaml
│       ├── service.yaml
│       ├── hpa.yaml
│       └── pdb.yaml
├── todo-api/
│   ├── Chart.yaml
│   ├── values.yaml
│   ├── values-dev.yaml
│   ├── values-prod.yaml
│   └── templates/
│       ├── deployment.yaml
│       ├── service.yaml
│       ├── serviceaccount.yaml
│       ├── externalsecret.yaml
│       ├── hpa.yaml
│       ├── pdb.yaml
│       └── servicemonitor.yaml
└── todo-infra/
    ├── Chart.yaml
    └── templates/
        ├── namespaces.yaml
        ├── networkpolicy.yaml
        ├── resourcequota.yaml
        └── ingress.yaml

3つのチャートの分離が核心です。

  • todo-infra — ネームスペース · NetworkPolicy · ResourceQuota · Ingress。2つのアプリが共有するインフラ。
  • todo-api — backend のすべてのマニフェスト。
  • todo-web — frontend のすべてのマニフェスト。

22章 アプリ配備の骨格 §「Helm チャートで束ねる」のパターンがマルチアプリ環境でどう分かれるかの本格的な適用です。Chart.yaml の dependencies で束ねるオプションもありますが、シンプルさのために本キャップストーンはフラットな構造に置いて ArgoCD ApplicationSet で統合します。

PR #9 — GitOps: ArgoCD ApplicationSet #

20章 GitOps + 24章 CI / CD パイプライン のモデルが ApplicationSet 1つのマニフェストに整理されます。

argocd/applicationset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: todo
  namespace: argocd
spec:
  generators:
    - matrix:
        generators:
          - list:
              elements:
                - app: todo-infra
                - app: todo-api
                - app: todo-web
          - list:
              elements:
                - env: dev
                  cluster: https://kubernetes.default.svc
                - env: prod
                  cluster: https://kubernetes.default.svc
  template:
    metadata:
      name: '{{`{{.app}}`}}-{{`{{.env}}`}}'
    spec:
      project: todo
      source:
        repoURL: https://github.com/myorg/todo-manifests.git
        targetRevision: main
        path: charts/{{`{{.app}}`}}
        helm:
          valueFiles:
            - values.yaml
            - values-{{`{{.env}}`}}.yaml
      destination:
        server: '{{`{{.cluster}}`}}'
        namespace: todo-{{`{{.app}}`}}
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

matrix generator が 3 アプリ × 2 環境 = 6 個の Application を1つのマニフェストで自動生成します。dev は自動 sync、prod は ApplicationSet の別のインスタンスで手動 sync モードへ分岐するのが運用の標準ですが、本キャップストーンは単純化のために両方とも automated に置きます。

24章 の GitHub Actions OIDC + ECR push + マニフェスト repo 自動 commit サイクルが本マニフェストの入力です — コード push 一度で dev / prod の両方の環境が自動 sync されます。

PR #10 — 可観測性 #

19章 可観測性 + 25章 モニタリング · アラート の kube-prometheus-stack がそのまま入ってきます。違いは OpenTelemetry Collector を追加して2つのアプリのトレースを束ねることです。

otel-collector — DaemonSet パターン
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: otel
  namespace: monitoring
spec:
  mode: daemonset
  config:
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
    exporters:
      prometheus:
        endpoint: 0.0.0.0:8889
      otlp/tempo:
        endpoint: tempo.monitoring.svc:4317
    service:
      pipelines:
        traces:
          receivers: [otlp]
          exporters: [otlp/tempo]
        metrics:
          receivers: [otlp]
          exporters: [prometheus]

Next.js の OpenTelemetry SDK と FastAPI の OTel instrumentation が同じ endpoint へトレースを送ると、2つのアプリをまたぐ1リクエストの全体経路 が Tempo で見えます。RSC レンダリング時の fetch 呼び出しが FastAPI のどのハンドラを経由して RDS まで到達したかが1つのトレース画面で追跡されます。

ServiceMonitor + PrometheusRule #

todo-api の 4 golden signals
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: todo-api
  namespace: todo-backend
  labels:
    release: prometheus
spec:
  groups:
    - name: todo-api.golden-signals
      rules:
        - alert: TodoApiHighErrorRate
          expr: |
            sum(rate(http_requests_total{app="todo-api",status=~"5.."}[5m]))
              / sum(rate(http_requests_total{app="todo-api"}[5m])) > 0.05
          for: 5m
          labels:
            severity: critical
        # ... latency, traffic, saturation も同様

25章 のマニフェストそのままであり、同じルールが todo-web にも適用されます。アラートの severity ルーティングは 25 章の Alertmanager マニフェストがそのまま入ってきます。

PR #11 — オートスケーリング #

13章 オートスケーリング の HPA と 28章 の Karpenter NodePool が連携して2段階の自動反応を作ります。

todo-api の HPA
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: todo-api
  namespace: todo-backend
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: todo-api
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300
2段階の自動反応
トラフィック増加
   |
   v
HPA: 30 秒で todo-api Pod 2 -> 5 -> 10 -> 20
   |
   | ノードのリソース不足
   v
Karpenter: 30 秒 ~ 1 分で新しいノード (spot 優先) プロビジョニング
   |
   v
Pending だった Pod が新しいノードにスケジューリングされる

この2段階が一連の流れで回る形が K8s オートスケーリングの目標です。負荷テストでその形を次の PR で測定します。

PR #12 — 負荷テストとコスト推定 #

k6/script.js — 負荷シナリオ
import http from "k6/http";
import { check } from "k6";

export const options = {
  stages: [
    { duration: "2m", target: 50 },
    { duration: "5m", target: 200 },
    { duration: "2m", target: 500 },
    { duration: "5m", target: 500 },
    { duration: "2m", target: 0 },
  ],
};

export default function () {
  const res = http.get("https://todo.example.com/api/todos");
  check(res, {
    "status is 200": (r) => r.status === 200,
    "duration < 500ms": (r) => r.timings.duration < 500,
  });
}
k6 負荷実行
k6 run k6/script.js

測定する内容です。

  • HPA scale-up の応答時間 — トラフィック 50 → 200 の間で Pod が何秒で増えるか
  • Karpenter のノード追加時間 — Pod が Pending にとどまった時間
  • P95 latency — 負荷のピーク (500 VUs) で latency がどう変わるか
  • 5xx 比率 — 負荷中にエラー率が 25 章の閾値 (5 %) を超えるか

コスト検証 #

OpenCost — 負荷テスト後のコスト測定
helm install opencost opencost/opencost \
  -n opencost --create-namespace

OpenCost の出力で1か月の仮説コストを検証します。

項目予想 (月)
EKS コントロールプレーン$73
ノード (system t3.medium × 2 ON_DEMAND)$60
ノード (アプリケーション spot 平均 1.5 台)$20
RDS db.t4g.small Multi-AZ$30
ALB (1 台, LCU)$20
NAT Gateway + データ転送$35
ECR / Route 53 / その他$10
合計約 $248

28章 §「請求書 review チェックリスト」の各項目を本実測値と比較します。prod の目標が本書の標準ガイド ($200 ~ $300) の中に収まったか が検証指標です。

学習用環境では次の調整で月 $40 ~ $80 まで減らせます。

  • prod の Multi-AZ RDS を dev の単一 AZ へ
  • ALB 1台 (すでに共有)
  • system ノードグループも spot へ
  • NAT Gateway を Single NAT へ

PR #13 — 運用チェックリストの適用 #

最後の PR は 26章 運用チェックリスト の定期カレンダーと 30章 アップグレード戦略 のアップグレードチェックリストを本システムに適用します。

docs/runbooks/todo-operations.md
# todo システム運用カレンダー

## 毎日
- Grafana の todo ダッシュボードの5パネル点検
- Alertmanager のアクティブアラートのレビュー

## 毎週
- ECR Trivy スキャン結果 (todo-api, todo-web 両方)
- 新規セキュリティパッチのレビュー

## 毎月
- OpenCost のチーム / ワークロード別コスト 1, 2, 3 位
- VPA recommendation の未反映ワークロード
- ArgoCD の OutOfSync 状態の点検

## 四半期
- EKS マイナーアップグレード (30章の13ステップ)
- RDS Performance Insights の right-sizing 信号
- RBAC audit
- 復旧訓練 (PITR シミュレーション)
- kube-bench CIS 点検

## 半期
- 外部セキュリティ監査
- DR シミュレーション (Velero restore)

## 年
- クラスタアーキテクチャのレビュー
- マニフェストの現代化

このマニフェストが git に入るのが本キャップストーンの最後の PR です。コードだけでなく運用手順も git の単一ソースに 置くのが GitOps の本質的な目標です。

事後の振り返り — 30章がどう束ねられたか #

13 PR を経て、本書の章が1つのシステムの中でどう噛み合ったかを整理します。

本書の章本キャップストーンでの役割
1 ~ 3章マニフェストの一行を読む視野
4章 Deploymenttodo-api / todo-web の RollingUpdate 戦略
5章 Servicetodo-api ↔ todo-web の cluster DNS 接続
6章 ConfigMap · Secret環境変数注入の標準
7章 Namespace とラベルfrontend / backend / data 分離
9章 PV / PVC / StorageClassEBS CSI Driver (直接 PV は使わない — RDS)
10章 IngressALB 1台 + group.name で2つのホスト
11章 リソース要求と上限Next.js 256 Mi · FastAPI 128 Mi の出発点
12章 ヘルスチェック3種 probe + graceful shutdown
13章 オートスケーリングHPA + Karpenter の2段階の自動反応
14章 RBAC / NetworkPolicy / Quotaネームスペース隔離 + チーム別の上限
15章 CNI 深掘りVPC CNI が Pod に直接 IP を付与 (背景)
16章 IRSAtodo-api の AWS 認証情報
17章 Admission ControllerKyverno ポリシー (選択)
18章 CRD と OperatorESO, Karpenter, ALB Controller, OTel
19章 可観測性OpenTelemetry + Tempo のトレース
20章 GitOpsArgoCD ApplicationSet の1つのマニフェスト
21章 EKS セットアップTerraform の出発点
22章 アプリ配備の骨格todo-api / todo-web の標準9つの束
23章 DB 連携RDS + ESO + PgBouncer
24章 CI / CD パイプラインGitHub Actions OIDC → ECR → ApplicationSet
25章 モニタリング · アラートPrometheusRule + Alertmanager ルーティング
26章 運用チェックリスト毎日 / 毎週 / 毎月 / 四半期 / 半期 / 年
27章 kubectl デバッグ事故時の5分の標準フロー
28章 コスト最適化OpenCost + Karpenter spot + ALB 共有
29章 シークレット運用ESO + automountServiceAccountToken
30章 アップグレード戦略preStop · PDB · Karpenter disruption budgets

この表が本キャップストーンの一行まとめです — 30章が1つのシステムの中でそれぞれの位置に収まる形 が K8s トラックの目標です。

AWS 本との比較 #

AWS 本(公開予定)の6部キャップストーンが同じ todo システムを ECS Fargate のルートで扱います。2冊の本を比較学習すると、同じドメインを2つのプラットフォームで実装したときの運用の違いが明確に見えます。

観点本書 (EKS)AWS (ECS Fargate)
出発点のコスト月 $200 ~ $300月 $80 ~ $150
運用の表面K8s の豊富さ + 学習曲線AWS コンソール + 少ないオブジェクト
自動化道具Karpenter, HPA, ArgoCDService Auto Scaling, CodePipeline
可観測性Prometheus + GrafanaCloudWatch Container Insights
マルチクラウドの可能性可能 (K8s 標準)AWS 依存
チームの学習コスト大きい小さい

小さなチーム + 単一ドメインなら ECS Fargate のほうが効率的で、マルチドメイン + GitOps + 豊富なワークロードパターン + マルチクラウドオプションが必要なら EKS が適しています。本キャップストーンの決定 (EKS) は学習価値と本書の30章の総合検証の結果 です。

片付け — クラスタの削除 #

学習用クラスタはキャップストーンが終わった後すぐに片付けるのがコスト面の標準です。

リソース片付けの順序
# 1. ArgoCD Application 削除 (ワークロード片付け)
kubectl delete applicationset todo -n argocd

# 2. RDS deletion_protection 解除後 terraform destroy
# (prod の場合 deletion_protection がオンなので terraform 変数で false 後 apply)

# 3. ALB / Route 53 の自動片付け確認
# external-dns が hostname の A レコードを自動削除

# 4. terraform destroy
terraform destroy

# 5. ECR repository 削除 (イメージの残り)
aws ecr delete-repository --repository-name todo-api --force
aws ecr delete-repository --repository-name todo-web --force

この順序が安全な片付けの標準です — Application から片付けないと Terraform が ALB 依存性に阻まれて destroy が失敗します。

練習問題 #

  1. 本キャップストーンの13 PR を実際に自分の GitHub 組織に適用してみて、最後の負荷テストの結果を OpenCost のコスト出力とともに1ページに整理します。予想コスト仮説 ($248 程度) と実測値の格差がどこで発生したか (特に NAT データ転送 · ALB LCU · spot 比率) を 28章 コスト最適化 の §「請求書 review チェックリスト」と照合します。
  2. 本キャップストーンの ApplicationSet マニフェストを分岐して、dev と prod の sync ポリシーが異なる動作をするよう修正します (dev は automated + selfHeal、prod は手動 sync)。わざと dev のマニフェストに壊れた値 (例: 存在しないイメージタグ) を適用して selfHeal がどう保護するか、prod の手動 sync がどう人のゲートとして作動するかを一段落で比較します。
  3. AWS 本の同じ todo システムの ECS Fargate キャップストーンをたどった後、2つの実装の運用上の違いを自分のシナリオに照らして一表に比較します。どの時点でどのプラットフォームが適しているか の決定ツリーを自分のドメイン (トラフィックパターン · チーム規模 · クラウド依存の許容度) に合わせて1ページに整理します。

一行まとめ: 6部キャップストーンは modern-react の Next.js と modern-python の FastAPI を同じ EKS クラスタに13個の PR で一緒に配備する。Terraform + Karpenter + IRSA + ALB Controller + ExternalDNS + cert-manager のクラスタセットアップ、frontend / backend / data のネームスペース分離 + NetworkPolicy + ResourceQuota、RDS + External Secrets + PgBouncer の DB 連携、Helm チャート3つ (infra + api + web)、ArgoCD ApplicationSet の matrix generator が 3 アプリ × 2 環境の 6 Application を1つのマニフェストで自動生成、OpenTelemetry が2つのアプリをまたぐ1つのトレースを作り、HPA + Karpenter が2段階の自動反応を作り、k6 + OpenCost が1か月約 $248 のコスト仮説を検証し、最後の PR が毎日 / 毎週 / 毎月 / 四半期 / 半期 / 年 の運用カレンダーを git に置く流れ。1 ~ 30章の30種の道具が1つのシステムの中で1つの位置に収まる形が K8s トラックの目標。小さなチーム + 単一ドメインなら AWS の ECS Fargate のほうが効率的なことがあり、マルチドメイン + GitOps + マルチクラウドオプションが必要なら EKS が適している。

本の終わり — 次のステップ #

本キャップストーンで、本書の30章が1つのシステムの中でどう噛み合うのかの視野が完成しました。しかし本書は K8s のゴールではありません — 出発点です。次のトラックへ進める主題を挙げておきます。

  • Service Mesh — Istio · Linkerd。mTLS · 細かいトラフィックルーティング · observability mesh。
  • MLOps on K8s — Kubeflow · KServe · Argo Workflows。ML モデルの学習 · 配備 · サービングの専用スタック。
  • マルチクラスタ — 単一クラスタの限界を越えるパターン。クラスタフェデレーション · マルチ region · ArgoCD ApplicationSet のマルチクラスタモード。
  • eBPF 深掘り — Cilium のその先。セキュリティ / 可観測性 / ネットワーキングの次世代。
  • eks-anywhere / on-prem K8s — マネージドを離れたクラスタ運用の要点。

これらの主題は別の本の領域であり、本書の30章がその出発点に立つ視野を作ってくれます。

最後に 付録A — docker-compose から K8s へ が入門読者のための移行ガイドとして本を閉じます。本書をすべてたどってきた読者には付録ですが、Docker / docker-compose まで来てみて本書を初めて開いた読者には出発点になります。

X