LESSON 30分

「コンテナはクラウドネイティブの基盤だ。ただ Docker を使えばいいという話じゃない。セキュリティ、サイズ、ビルド効率、全てを考慮した設計が必要だよ」と佐藤CTOが続けた。

推定読了時間: 30分


OCI (Open Container Initiative) 仕様

コンテナの標準仕様を理解することが重要です。

仕様説明
OCI Image Specコンテナイメージのフォーマット
OCI Runtime Specコンテナの実行方法 (runc, crun)
OCI Distribution Specイメージの配布方法 (レジストリ)
graph TB
    subgraph OCI["OCI Image Spec"]
        direction TB
        subgraph Layers["Image Layers"]
            direction LR
            L1["Layer 1<br/>(base)"]
            L2["Layer 2<br/>(deps)"]
            L3["Layer 3<br/>(app)"]
            Ln["..."]
        end
        Config{{"Config JSON"}}
        Manifest([Manifest])
    end

    Layers --> Config
    Layers --> Manifest

    classDef ociBox fill:#1a1a2e,stroke:#16213e,color:#e94560
    classDef layerStyle fill:#0f3460,stroke:#533483,color:#e2e2e2
    classDef configStyle fill:#533483,stroke:#e94560,color:#ffffff
    classDef manifestStyle fill:#16213e,stroke:#0f3460,color:#e2e2e2

    class OCI ociBox
    class L1,L2,L3,Ln layerStyle
    class Config configStyle
    class Manifest manifestStyle

マルチステージビルド

基本パターン

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

WORKDIR /app

# 依存関係のインストール(キャッシュ効率化)
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts

# ソースコードのコピーとビルド
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build

# 本番依存のみ再インストール
RUN npm ci --omit=dev --ignore-scripts

# ============ Stage 2: Runtime ============
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 ./

# 非rootユーザーで実行(distrolessのデフォルト)
USER nonroot

EXPOSE 3000

CMD ["dist/main.js"]

サイズ比較

ベースイメージサイズ用途
node
~1GB開発のみ
node
~200MBビルドステージ
node
~130MB軽量ランタイム
distroless/nodejs20~120MB本番推奨
scratch数MBGo/Rust バイナリ

Distroless イメージ

「distroless はシェルすらないイメージだ。攻撃対象領域を極限まで減らせる」

# Go アプリケーション - scratch / distroless
FROM golang:1.22 AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-w -s" -o /server ./cmd/server

# distroless - TLS証明書やtzdata が含まれる
FROM gcr.io/distroless/static-debian12

COPY --from=builder /server /server

USER nonroot:nonroot

ENTRYPOINT ["/server"]
distroless vs alpine vs scratch の比較
特徴scratchdistrolessalpine
シェルなしなしあり (sh)
パッケージマネージャなしなしapk
TLS証明書なしありあり
タイムゾーンデータなしありあり
デバッグ困難debug タグあり容易
CVE数0極小
推奨用途静的バイナリ本番ワークロード開発/テスト

レイヤーキャッシュの最適化

# ❌ Bad: 毎回全てのレイヤーが再ビルド
FROM node:20-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build

# ✅ Good: 依存関係のキャッシュを活用
FROM node:20-slim
WORKDIR /app

# Step 1: 依存定義ファイルのみコピー(変更頻度低)
COPY package.json package-lock.json ./

# Step 2: 依存インストール(キャッシュ活用)
RUN npm ci

# Step 3: ソースコピー(変更頻度高)
COPY . .

# Step 4: ビルド
RUN npm run build

BuildKit キャッシュマウント

# syntax=docker/dockerfile:1
FROM node:20-slim AS builder

WORKDIR /app
COPY package.json package-lock.json ./

# npm キャッシュをマウント(ビルド間で共有)
RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build

セキュリティのベストプラクティス

非 root ユーザー

FROM node:20-slim

# 専用ユーザーを作成
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser

WORKDIR /app
COPY --chown=appuser:appuser . .

USER appuser

CMD ["node", "dist/main.js"]

.dockerignore

# .dockerignore
node_modules
.git
.gitignore
*.md
.env*
.vscode
coverage
.nyc_output
Dockerfile
docker-compose*.yml

イメージスキャン

# GitHub Actions でのイメージスキャン
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: "my-app:${{ github.sha }}"
    format: "sarif"
    output: "trivy-results.sarif"
    severity: "CRITICAL,HIGH"
    exit-code: "1"  # 脆弱性があればCIを失敗させる

ヘルスチェック

FROM node:20-slim

WORKDIR /app
COPY . .

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })"

CMD ["node", "dist/main.js"]
// ヘルスチェックエンドポイント
app.get("/health", async (req, res) => {
  try {
    await db.query("SELECT 1");
    res.json({ status: "healthy", timestamp: new Date().toISOString() });
  } catch (err) {
    res.status(503).json({ status: "unhealthy", error: (err as Error).message });
  }
});

app.get("/ready", async (req, res) => {
  const checks = await Promise.allSettled([
    db.query("SELECT 1"),
    redis.ping(),
  ]);
  const allHealthy = checks.every((c) => c.status === "fulfilled");
  res.status(allHealthy ? 200 : 503).json({
    status: allHealthy ? "ready" : "not_ready",
  });
});

マルチアーキテクチャビルド

# buildx でマルチアーキテクチャイメージをビルド
docker buildx create --name multiarch --use
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag my-registry/my-app:latest \
  --push .
# GitHub Actions でのマルチアーキテクチャビルド
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

「Graviton インスタンスは x86 より2割安い。ARM ビルドを用意しておけばコスト削減の選択肢が広がる」


まとめ

プラクティス効果
マルチステージビルドイメージサイズ削減
distroless ベースイメージセキュリティ向上
レイヤーキャッシュ最適化ビルド時間短縮
非 root ユーザー権限昇格防止
ヘルスチェック自動復旧
イメージスキャン脆弱性の早期発見
マルチアーキテクチャARM対応 / コスト削減

チェックリスト

  • マルチステージビルドを使用している
  • 本番イメージは distroless または minimal ベース
  • コンテナは非 root ユーザーで実行されている
  • .dockerignore が適切に設定されている
  • ヘルスチェックが定義されている
  • CI でイメージスキャンを実行している
  • レイヤーキャッシュが最適化されている

次のステップへ

次のレッスンでは、コンテナ化と密接に関連する「イミュータブルインフラストラクチャ」の概念を学びます。