EXERCISE 60分

12-Factor App、コンテナ化、イミュータブルインフラ、GitOps の原則を実践的に確認する演習です。


Mission 1: 12-Factor App 違反の修正

以下のアプリケーションコードには複数の 12-Factor App 違反があります。全ての違反を特定し、修正してください。

// app.ts - 12-Factor App 違反だらけのコード
import express from "express";
import fs from "fs";

const app = express();

// データベース接続
const DB_HOST = "prod-db.mycompany.internal";
const DB_PASSWORD = "SuperSecret123!";

// セッション管理
const sessions: Record<string, any> = {};

// ログ
const logFile = fs.createWriteStream("/var/log/app/access.log", { flags: "a" });

app.use((req, res, next) => {
  logFile.write(`${new Date().toISOString()} ${req.method} ${req.url}\n`);
  next();
});

app.post("/login", (req, res) => {
  const sessionId = Math.random().toString(36);
  sessions[sessionId] = { user: req.body.username, loginAt: Date.now() };
  res.cookie("session", sessionId);
  res.json({ ok: true });
});

app.get("/dashboard", (req, res) => {
  const session = sessions[req.cookies.session];
  if (!session) return res.status(401).json({ error: "Not logged in" });
  res.json({ user: session.user });
});

app.listen(8080, () => {
  console.log("Server started on port 8080");
});

要件:

  • 全ての 12-Factor 違反を列挙すること
  • 修正後のコードを書くこと
  • どの Factor に違反しているか番号で示すこと
解答例

違反一覧:

違反箇所該当Factor説明
DB_HOST ハードコードFactor 3: Config設定が環境変数でない
DB_PASSWORD ハードコードFactor 3: Config秘密情報がコード内
sessions をメモリ保持Factor 6: Processesステートフルなプロセス
ファイルにログ出力Factor 11: Logsログをファイルに書いている
ポート 8080 ハードコードFactor 7: Port Bindingポートが固定

修正後のコード:

import express from "express";
import session from "express-session";
import RedisStore from "connect-redis";
import { createClient } from "redis";
import pino from "pino";

const app = express();

// Factor 3: 設定を環境変数から取得
const config = {
  dbHost: process.env.DB_HOST ?? (() => { throw new Error("DB_HOST required"); })(),
  dbPassword: process.env.DB_PASSWORD ?? (() => { throw new Error("DB_PASSWORD required"); })(),
  redisUrl: process.env.REDIS_URL ?? "redis://localhost:6379",
  port: parseInt(process.env.PORT ?? "3000", 10),
  sessionSecret: process.env.SESSION_SECRET ?? (() => { throw new Error("SESSION_SECRET required"); })(),
};

// Factor 11: ログを stdout にストリーム出力
const logger = pino({ level: process.env.LOG_LEVEL ?? "info" });

app.use((req, res, next) => {
  logger.info({ method: req.method, url: req.url }, "Request received");
  next();
});

// Factor 6: セッションを外部ストア(Redis)に保持
const redisClient = createClient({ url: config.redisUrl });
redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: config.sessionSecret,
  resave: false,
  saveUninitialized: false,
}));

app.post("/login", (req, res) => {
  req.session.user = req.body.username;
  res.json({ ok: true });
});

app.get("/dashboard", (req, res) => {
  if (!req.session.user) return res.status(401).json({ error: "Not logged in" });
  res.json({ user: req.session.user });
});

// Factor 7: ポートを環境変数から取得
app.listen(config.port, "0.0.0.0", () => {
  logger.info({ port: config.port }, "Server started");
});

Mission 2: 最適な Dockerfile の作成

以下の要件を満たす Dockerfile を作成してください。

要件:

  • TypeScript の Node.js アプリケーション
  • マルチステージビルド
  • 本番イメージは distroless ベース
  • レイヤーキャッシュを最適化
  • 非 root ユーザーで実行
  • ヘルスチェックを含む
  • イメージサイズを最小限に
{
  "name": "cloud-native-api",
  "version": "1.0.0",
  "scripts": {
    "build": "tsc",
    "start": "node dist/main.js"
  },
  "dependencies": {
    "express": "^4.18.2",
    "pg": "^8.11.3"
  },
  "devDependencies": {
    "typescript": "^5.3.3",
    "@types/express": "^4.17.21",
    "@types/node": "^20.11.0"
  }
}
解答例
# syntax=docker/dockerfile:1

# ============ Stage 1: Dependencies ============
FROM node:20-slim AS deps

WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --ignore-scripts

# ============ Stage 2: Build ============
FROM node:20-slim AS builder

WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY package.json tsconfig.json ./
COPY src/ ./src/

RUN npm run build

# 本番依存のみ再インストール
RUN --mount=type=cache,target=/root/.npm \
    npm ci --omit=dev --ignore-scripts

# ============ Stage 3: Production ============
FROM gcr.io/distroless/nodejs20-debian12

WORKDIR /app

# ビルド成果物と本番依存のみコピー
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# distroless のデフォルト非rootユーザー
USER nonroot

EXPOSE 3000

# distroless ではHEALTHCHECK命令は使えないため
# Kubernetes の livenessProbe / readinessProbe で代替

CMD ["dist/main.js"]

ポイント:

  • 3段階のマルチステージでキャッシュ効率を最大化
  • BuildKit のキャッシュマウントで npm キャッシュを共有
  • distroless はシェルがないため HEALTHCHECK 命令は使えない → K8s の probe で代替
  • devDependencies は本番イメージに含まれない

Mission 3: イミュータブルインフラの設計

あなたのチームが運用するWebアプリケーションで以下の問題が発生しています。

現状の問題:

  • 本番サーバー5台の設定がそれぞれ微妙に異なる
  • 「サーバーAでは動くがサーバーBでは動かない」障害が頻発
  • デプロイは運用チームがSSHで各サーバーにログインして実施
  • ロールバックに毎回30分以上かかる

課題: イミュータブルインフラへの移行計画を設計してください。

以下を含めること:

  1. 現状分析(何が問題か)
  2. 移行後のアーキテクチャ図
  3. イメージビルドパイプラインの設計
  4. デプロイ戦略(Blue-Green or Canary)
  5. ロールバック手順
解答例

1. 現状分析

問題原因影響
設定のドリフト手動でのパッチ適用再現不可能な環境
サーバー固有の障害Snowflake Serverデバッグの長期化
デプロイの属人化SSH手動デプロイデプロイ頻度の低下
ロールバック遅延手順書ベースの復旧障害影響の拡大

2. 移行後のアーキテクチャ

graph TD
    A["Git Push"] --> B["CI/CD Pipeline"]
    B --> C["Packer Build"]
    C --> D["AMI"]
    D --> E["Launch Template"]
    E --> F["Blue ASG (Active)"]
    E --> G["Green ASG (Standby)"]
    F --> H["ALB"]

    style A fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style B fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af
    style D fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
    style E fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
    style F fill:#d1fae5,stroke:#059669,color:#065f46
    style G fill:#f3f4f6,stroke:#9ca3af,color:#374151
    style H fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af

3. パイプライン設計

# .github/workflows/build-deploy.yml
name: Build & Deploy
on:
  push:
    branches: [main]

jobs:
  build-ami:
    runs-on: ubuntu-latest
    outputs:
      ami_id: ${{ steps.packer.outputs.ami_id }}
    steps:
      - uses: actions/checkout@v4
      - name: Build AMI
        id: packer
        run: |
          packer build -machine-readable app.pkr.hcl | \
            tee build.log
          AMI_ID=$(grep 'artifact,0,id' build.log | cut -d: -f2)
          echo "ami_id=$AMI_ID" >> $GITHUB_OUTPUT

  deploy:
    needs: build-ami
    runs-on: ubuntu-latest
    steps:
      - name: Update Launch Template
        run: |
          aws ec2 create-launch-template-version \
            --launch-template-id $LT_ID \
            --source-version '$Latest' \
            --launch-template-data '{"ImageId":"${{ needs.build-ami.outputs.ami_id }}"}'

      - name: Deploy to Green
        run: |
          aws autoscaling update-auto-scaling-group \
            --auto-scaling-group-name app-green \
            --desired-capacity 5

      - name: Wait for healthy
        run: |
          aws autoscaling wait group-in-service \
            --auto-scaling-group-name app-green

      - name: Switch traffic
        run: |
          aws elbv2 modify-listener \
            --listener-arn $LISTENER_ARN \
            --default-actions '[{"Type":"forward","TargetGroupArn":"'$GREEN_TG_ARN'"}]'

4. Blue-Green デプロイ戦略を選択

理由: 切り替えが即座に行え、ロールバックも ALB の切り替えだけで完了するため。

5. ロールバック手順

# ALBのリスナーを Blue に戻すだけ(所要時間: 数秒)
aws elbv2 modify-listener \
  --listener-arn $LISTENER_ARN \
  --default-actions '[{"Type":"forward","TargetGroupArn":"'$BLUE_TG_ARN'"}]'

従来30分 → 数秒に短縮。


Mission 4: GitOps ワークフローの設計

以下のマイクロサービス構成に対して、GitOps ワークフローを設計してください。

システム構成:

  • api-gateway (Node.js)
  • user-service (Go)
  • order-service (Python)
  • notification-service (Node.js)

要件:

  • 開発・ステージング・本番の3環境
  • 各サービスは独立してデプロイ可能
  • 本番デプロイには承認が必要
  • ドリフト検出と自動修復

設計すべきもの:

  1. Git リポジトリ構成
  2. ArgoCD Application の定義
  3. 環境プロモーションのフロー
  4. ドリフト検出の仕組み
解答例

1. リポジトリ構成

graph TD
    Root["k8s-manifests/"] --> Apps["apps/"]
    Root --> Infra["infrastructure/"]
    Root --> Argo["argocd/"]

    Apps --> AG["api-gateway/"]
    Apps --> US["user-service/"]
    Apps --> OS["order-service/"]
    Apps --> NS["notification-service/"]

    AG --> AGB["base/
deployment.yaml
service.yaml
hpa.yaml
kustomization.yaml"] AG --> AGO["overlays/
dev / staging / prod"] US --> USB["base/"] US --> USO["overlays/"] OS --> OSB["base/"] OS --> OSO["overlays/"] NS --> NSB["base/"] NS --> NSO["overlays/"] Infra --> IN["ingress-nginx/"] Infra --> CM["cert-manager/"] Infra --> MO["monitoring/"] Argo --> AP["projects/"] Argo --> AA["applications/"] style Root fill:#1e293b,stroke:#475569,color:#f8fafc style Apps fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style Infra fill:#d1fae5,stroke:#059669,color:#065f46 style Argo fill:#f3e8ff,stroke:#7c3aed,color:#5b21b6 style AG fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style US fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style OS fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style NS fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af

2. ArgoCD Application

# argocd/projects/microservices.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: microservices
  namespace: argocd
spec:
  sourceRepos:
    - https://github.com/myorg/k8s-manifests.git
  destinations:
    - namespace: "dev-*"
      server: https://kubernetes.default.svc
    - namespace: "staging-*"
      server: https://kubernetes.default.svc
    - namespace: "prod-*"
      server: https://kubernetes.default.svc
  roles:
    - name: prod-deployer
      policies:
        - p, proj:microservices:prod-deployer, applications, sync, microservices/*, allow
---
# argocd/applications/user-service-prod.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: user-service-prod
  namespace: argocd
spec:
  project: microservices
  source:
    repoURL: https://github.com/myorg/k8s-manifests.git
    targetRevision: main
    path: apps/user-service/overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: prod-services
  syncPolicy:
    automated:
      prune: true
      selfHeal: true  # ドリフト自動修復
    syncOptions:
      - CreateNamespace=true

3. 環境プロモーションフロー

dev (自動sync) → staging (自動sync) → prod (PR承認必要)

CI パイプラインがイメージタグを更新:

# CI がイメージビルド後に実行
cd k8s-manifests
kustomize edit set image myorg/user-service=myorg/user-service:v1.2.3
git commit -am "chore: update user-service to v1.2.3"
git push origin main  # dev/staging は自動sync
# prod は PR を作成して承認後にマージ

4. ドリフト検出

ArgoCD の selfHeal: true により、kubectl 等での手動変更は自動的にGitの状態に戻される。通知は Slack に送信。