LESSON 30分

「クラウドネイティブの第一歩は、アプリケーションそのものの設計原則を理解することだ。12-Factor App は Heroku のエンジニアたちが数千のアプリ運用から抽出した知恵の結晶だよ」と佐藤CTOは語り始めた。

推定読了時間: 30分


12-Factor App とは

2012年に Heroku の共同創業者 Adam Wiggins が提唱した、クラウドネイティブアプリケーション開発のための方法論です。SaaS アプリケーションを構築する際のベストプラクティスを12の要素にまとめています。

なぜ今も重要なのか

観点説明
ポータビリティ特定のクラウドベンダーに依存しない
スケーラビリティ水平スケールを前提とした設計
運用性デプロイの自動化と一貫性
回復力障害からの迅速な復旧

Factor 1: コードベース (Codebase)

バージョン管理される1つのコードベースと、多数のデプロイ

graph TD
    Repo[(Git Repository<br/>1 codebase)]
    Dev([dev])
    Staging([staging])
    Prod([prod])

    Repo --> Dev
    Repo --> Staging
    Repo --> Prod

    classDef repoStyle fill:#1E293B,stroke:#0F172A,color:#FFFFFF
    classDef devStyle fill:#2563EB,stroke:#1D4ED8,color:#FFFFFF
    classDef stagingStyle fill:#D97706,stroke:#B45309,color:#FFFFFF
    classDef prodStyle fill:#DC2626,stroke:#B91C1C,color:#FFFFFF

    class Repo repoStyle
    class Dev devStyle
    class Staging stagingStyle
    class Prod prodStyle
// package.json で環境ごとのデプロイスクリプトを定義
{
  "scripts": {
    "deploy:dev": "cdk deploy --context env=dev",
    "deploy:staging": "cdk deploy --context env=staging",
    "deploy:prod": "cdk deploy --context env=prod"
  }
}

「1つのリポジトリから複数の環境にデプロイする。環境ごとにコードを分けるのは絶対にやめてくれ」


Factor 2: 依存関係 (Dependencies)

依存関係を明示的に宣言し分離する

// package.json - 全ての依存を明示的に宣言
{
  "dependencies": {
    "@aws-sdk/client-s3": "^3.500.0",
    "express": "^4.18.2",
    "pg": "^8.11.3"
  },
  "devDependencies": {
    "typescript": "^5.3.3",
    "@types/node": "^20.11.0"
  },
  "engines": {
    "node": ">=20.0.0"
  }
}
# Dockerfile - システム依存も明示的に
FROM node:20-slim
# 暗黙的なシステム依存に頼らない
RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

Factor 3: 設定 (Config)

設定を環境変数に格納する

// ❌ Bad: ハードコードされた設定
const dbHost = "prod-db.example.com";
const apiKey = "sk-1234567890";

// ✅ Good: 環境変数から取得
interface AppConfig {
  dbHost: string;
  dbPort: number;
  apiKey: string;
  logLevel: string;
}

function loadConfig(): AppConfig {
  return {
    dbHost: requireEnv("DB_HOST"),
    dbPort: parseInt(requireEnv("DB_PORT"), 10),
    apiKey: requireEnv("API_KEY"),
    logLevel: process.env.LOG_LEVEL ?? "info",
  };
}

function requireEnv(key: string): string {
  const value = process.env[key];
  if (!value) {
    throw new Error(`Required environment variable ${key} is not set`);
  }
  return value;
}
# Kubernetes ConfigMap / Secret
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DB_HOST: "postgres.default.svc.cluster.local"
  DB_PORT: "5432"
  LOG_LEVEL: "info"
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:
  API_KEY: "sk-1234567890"

Factor 4: バックエンドサービス (Backing Services)

バックエンドサービスをアタッチされたリソースとして扱う

graph LR
    subgraph DevEnv["dev環境: localhost:5432"]
        AppDev([App])
        DBDev[(Database<br/>local)]
        AppDev --> DBDev
    end

    subgraph ProdEnv["prod環境: prod-db.xxx.rds.amazonaws.com"]
        AppProd([App])
        DBProd[(Database<br/>RDS)]
        AppProd --> DBProd
    end

    classDef appStyle fill:#2563EB,stroke:#1D4ED8,color:#FFFFFF
    classDef dbDevStyle fill:#059669,stroke:#047857,color:#FFFFFF
    classDef dbProdStyle fill:#D97706,stroke:#B45309,color:#FFFFFF
    classDef devEnvStyle fill:#ECFDF5,stroke:#059669,color:#065F46
    classDef prodEnvStyle fill:#FFF7ED,stroke:#D97706,color:#92400E

    class AppDev,AppProd appStyle
    class DBDev dbDevStyle
    class DBProd dbProdStyle
    class DevEnv devEnvStyle
    class ProdEnv prodEnvStyle
// バックエンドサービスはURLで抽象化
interface BackingService {
  database: string;   // postgres://user:pass@host:5432/db
  cache: string;      // redis://host:6379
  queue: string;      // amqp://host:5672
  storage: string;    // s3://bucket-name
  smtp: string;       // smtp://host:587
}

// ローカルのPostgreSQLもAmazon RDSも同じインターフェース
const db = new Pool({ connectionString: process.env.DATABASE_URL });

Factor 5: ビルド、リリース、実行 (Build, Release, Run)

ビルド、リリース、実行の3つのステージを厳密に分離する

# GitHub Actions でのステージ分離
name: Deploy Pipeline
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - run: npm test
      - name: Build Docker Image
        run: docker build -t app:${{ github.sha }} .
      - name: Push to ECR
        run: |
          aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REPO
          docker push $ECR_REPO:${{ github.sha }}

  release:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Create Release
        run: |
          # ビルド成果物 + 設定 = リリース
          helm upgrade --install app ./chart \
            --set image.tag=${{ github.sha }} \
            --values values-prod.yaml

  # 実行は Kubernetes が管理

Factor 6: プロセス (Processes)

アプリケーションを1つもしくは複数のステートレスなプロセスとして実行する

// ❌ Bad: メモリにセッションを保持
const sessions = new Map<string, SessionData>();
app.use((req, res, next) => {
  const session = sessions.get(req.cookies.sessionId);
  // スケールアウト時にセッションが失われる
});

// ✅ Good: 外部ストアにセッションを保持
import RedisStore from "connect-redis";
import { createClient } from "redis";

const redisClient = createClient({ url: process.env.REDIS_URL });
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
}));

「ステートレスであることが水平スケールの前提だ。状態は外部ストアに逃がせ」


Factor 7: ポートバインディング (Port Binding)

ポートバインディングを通じてサービスを公開する

// 自己完結型のHTTPサーバー
import express from "express";

const app = express();
const port = parseInt(process.env.PORT ?? "3000", 10);

app.get("/health", (req, res) => res.json({ status: "ok" }));

app.listen(port, "0.0.0.0", () => {
  console.log(`Server listening on port ${port}`);
});
# Kubernetes Service で公開
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000

Factor 8: 並行性 (Concurrency)

プロセスモデルによってスケールアウトする

# 各プロセスタイプを独立してスケール
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 4  # Webプロセスは4台
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker
spec:
  replicas: 8  # Workerプロセスは8台
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: scheduler
spec:
  replicas: 1  # Schedulerは1台

Factor 9: 廃棄容易性 (Disposability)

高速な起動とグレースフルシャットダウンで堅牢性を最大化する

import { createServer } from "http";

const server = createServer(app);
server.listen(3000);

// グレースフルシャットダウン
function gracefulShutdown(signal: string) {
  console.log(`Received ${signal}. Starting graceful shutdown...`);

  server.close(async () => {
    console.log("HTTP server closed");

    // 進行中のリクエストの完了を待つ
    await Promise.all([
      db.end(),           // DB接続を閉じる
      redisClient.quit(), // Redis接続を閉じる
      consumer.stop(),    // メッセージ消費を停止
    ]);

    console.log("All connections closed. Exiting.");
    process.exit(0);
  });

  // 強制終了のタイムアウト
  setTimeout(() => {
    console.error("Forced shutdown after timeout");
    process.exit(1);
  }, 30_000);
}

process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
# Kubernetes の terminationGracePeriodSeconds と連携
spec:
  terminationGracePeriodSeconds: 30
  containers:
    - name: app
      lifecycle:
        preStop:
          exec:
            command: ["/bin/sh", "-c", "sleep 5"]

Factor 10: 開発/本番一致 (Dev/Prod Parity)

開発、ステージング、本番環境をできるだけ一致させる

ギャップ従来の手法12-Factor App
時間数週間~数ヶ月数時間~1日
人的開発者が書き、運用者がデプロイ開発者がデプロイ
ツール開発: SQLite / 本番: PostgreSQL全環境で PostgreSQL
# docker-compose.yml - ローカルでも本番と同じスタック
services:
  app:
    build: .
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/app
      - REDIS_URL=redis://cache:6379
  db:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data
  cache:
    image: redis:7-alpine

Factor 11: ログ (Logs)

ログをイベントストリームとして扱う

// ❌ Bad: ファイルに書き込み
import fs from "fs";
fs.appendFileSync("/var/log/app.log", message);

// ✅ Good: 構造化ログを stdout に出力
import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  formatters: {
    level: (label) => ({ level: label }),
  },
  timestamp: pino.stdTimeFunctions.isoTime,
});

logger.info({ userId: user.id, action: "login" }, "User logged in");
// 出力: {"level":"info","time":"2026-02-14T10:00:00.000Z","userId":"123","action":"login","msg":"User logged in"}
# Fluentd / Fluent Bit でログを収集
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
data:
  fluent-bit.conf: |
    [INPUT]
        Name              tail
        Path              /var/log/containers/*.log
        Parser            docker
    [OUTPUT]
        Name              cloudwatch_logs
        Match             *
        region            ap-northeast-1
        log_group_name    /ecs/my-app
        log_stream_prefix container-

Factor 12: 管理プロセス (Admin Processes)

管理タスクを1回限りのプロセスとして実行する

// scripts/migrate.ts - マイグレーションスクリプト
import { runMigrations } from "./db/migrations";

async function main() {
  console.log("Starting database migration...");
  await runMigrations();
  console.log("Migration complete");
  process.exit(0);
}

main().catch((err) => {
  console.error("Migration failed:", err);
  process.exit(1);
});
# Kubernetes Job として実行
apiVersion: batch/v1
kind: Job
metadata:
  name: db-migration
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: my-app:latest
          command: ["npx", "tsx", "scripts/migrate.ts"]
          envFrom:
            - configMapRef:
                name: app-config
            - secretRef:
                name: app-secrets
      restartPolicy: Never
  backoffLimit: 3

Beyond 12-Factor: 追加の原則

近年、12-Factor を拡張した考え方も提唱されています。

Factor名称説明
XIIIAPI FirstAPI を最初に設計する
XIVTelemetryメトリクス・トレース・ログの統合
XVSecurityセキュリティをデザインに組み込む
Beyond 12-Factor の詳細

API First

# OpenAPI Spec を先に書く
openapi: "3.1.0"
info:
  title: "User Service"
  version: "1.0.0"
paths:
  /users/{id}:
    get:
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string

Telemetry (OpenTelemetry)

import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
  }),
});
sdk.start();

まとめ

Factor原則クラウドネイティブへの貢献
Codebase1リポジトリ / 多デプロイ一貫性の確保
Dependencies明示的な宣言と分離再現可能なビルド
Config環境変数に格納環境間のポータビリティ
Backing Servicesアタッチされたリソース疎結合
Build/Release/Runステージの分離信頼性のあるデプロイ
Processesステートレス水平スケーラビリティ
Port Binding自己完結型デプロイの柔軟性
Concurrencyプロセスモデル効率的なスケール
Disposability高速起動/停止堅牢性
Dev/Prod Parity環境の一致バグの早期発見
Logsイベントストリーム可観測性
Admin Processes1回限りのプロセス運用の自動化

チェックリスト

  • アプリケーションは単一のコードベースからデプロイされている
  • 全ての依存関係が明示的に宣言されている
  • 設定は環境変数で管理されている
  • バックエンドサービスはURLで接続を切り替えられる
  • ビルド・リリース・実行が分離されている
  • プロセスはステートレスである
  • グレースフルシャットダウンが実装されている
  • ログは stdout に構造化形式で出力されている

次のステップへ

次のレッスンでは、12-Factor App の原則を実現するための基盤技術であるコンテナ化について、Docker のベストプラクティスを学びます。