Docker/Kubernetes実践入門

プロの技術編集者として、ご提示いただいたドラフトを査読・推敲いたしました。

今回のリライトでは、以下のポイントに重点を置いて修正を行っています。
1. 語彙の洗練: 「現場感」を出すため、よりエンジニアの日常的な動線に近い表現(「ハマりどころ」「ベストプラクティス」など)へ調整。
2. 技術的深掘り: 単に機能を紹介するだけでなく、「なぜそうすべきか」という根拠(セキュリティ、リソース管理の挙動など)を補強。
3. 可読性の向上: 重要なポイントが視覚的に飛び込んでくるよう、強調表現や構造を整理。
4. 整合性の確認: マークダウンのテーブル構造および末尾のフッター情報の完全な維持。

以下に【完成版の記事】を出力します。


Docker/Kubernetes実践入門

現場の「地獄」から学ぶ、本質的な運用と最適化の技術

近年のインフラ構成において、DockerやKubernetes(K8s)はもはや単なる選択肢ではなく、モダンなシステムを構築するための「標準」となりました。しかし、いざ実務の最前線に立つと、多くのエンジニアが共通の壁に突き当たります。

「ローカルでは動くのに、なぜか本番環境で挙動がおかしい」
「Podが原因不明の再起動(CrashLoop)を繰り返す」
「イメージサイズが肥語化しすぎて、デプロイ時間が耐えられない」

これらの問題は、技術そのものの不備ではなく、「抽象化されたレイヤーの裏側にある仕組み」への理解不足から生じることがほとんどです。

この記事では、中堅エンジニアとして数々のトラブルを乗り越えてきた筆者が、教科書には詳しく載っていないが現場で確実にハマるポイントを深掘りします。単なるコマンドの羅列ではなく、なぜその問題が発生するのかという内部構造まで踏み込んで解説していきます。


1. Dockerにおける「落とし穴」とビルド最適化の勘所

Dockerを使い始めた初期段階では、`Dockerfile`を書いて `docker build` が成功すれば満足してしまいがちです。しかし、プロダクション環境を見据えた設計を行うなら、以下のポイントは無視できない重要な要素となります。

レイヤー構造とビルドキャッシュの罠

Dockerのイメージは、各命令(RUN, COPY, ADD等)ごとに「レイヤー」として積み重なる構造を持っています。初心者が陥りやすい失敗の一つに、頻繁に更新されるソースコードと同じディレクトリを `COPY` し、その直後に依存関係のインストールを行う構成です。

# 【不適切な例】
FROM node:18
WORKDIR /app
COPY . .
RUN npm install

この書き方だと、ソースコードをたった1文字修正しただけで、それ以降のすべてのレイヤー(この場合は `npm install`)がキャッシュを無効化され、再計算されることになります。CI/CDパイプラインにおいて、これが原因でビルド時間が膨れ上がるのは現場では大きなストレスです。

これを回避するための鉄則が「マルチステージビルド」と「適切なコピー順序の設計」です。

マルチステージビルドによる最適化

実用的な構成では、ビルド環境と実行環境を完全に分離します。これにより、不要なビルドツールやキャッシュを含まない軽量でクリーンなイメージを作成できます。

# --- Build Stage ---
FROM node:18-alpine AS builder
WORKDIR /app
# 依存関係の定義ファイルだけを先にコピー(キャッシュ効率の最大化)
COPY package*.json ./
RUN npm install
# その後にソースコードをコピー
COPY . .
RUN npm run build

# --- Production Stage ---
FROM node:18-alpine
WORKDIR /app
# builderステージから、実行に必要な成果物だけを抽出
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/main.js"]

この手法のメリットは単に「イメージサイズが減る」ことだけではありません。攻撃のベクトル(アタックサーフェス)を最小限に抑え、ランタイム環境をクリーンに保つというセキュリティ上の大きな利点があります。また、`.dockerignore` を適切に設定し、`node_modules` や `.git` がビルドコンテキストに含まれないようにすることも、現場では必須の作法です。

環境変数の扱い(ENV vs ARG)

「環境変数ならすべて `ENV` で定義すればいいだろう」と考えるのは危険な設計です。
  • ARG: ビルド時のみ有効な変数。イメージ作成時のオプション(バージョン指定など)に使用します。
  • ENV: コンテナ実行時にも維持される変数。アプリケーションの動作挙動を制御するために使用します。
プロダクション環境において、パスワードやAPIキーなどの機密情報を `ENV` にハードコードしてはいけません。これらはKubernetesのSecretや、AWS Secrets Managerのような外部シークレットマネージャーから注入するのが鉄則です。

2. Kubernetesにおける「地獄のデバッグ」と解決策

Dockerが個々のコンテナを動かすための技術であるのに対し、Kubernetesはそれらをオーケストレーション(管理・運用)するための巨大なプラットフォームです。しかし、その抽象度の高さゆえに、トラブルシューティングの難易度は跳ね上がります。

「なぜかPodが立ち上がらない」の正体

`kubectl get pods` を実行した際に `CrashLoopBackOff` や `Pending` が表示されたとき、多くのエンジニアは混乱します。これらはK8sからの警告メッセージであり、その裏には具体的な原因が隠れています。

#### ① ImagePullBackOff / ErrImagePull
これは多くの場合、単純な設定ミスです。しかし、単に「名前を間違えた」だけでなく、認証情報の不足(ImagePullSecretsの設定漏れ)や、タグの不適切な指定(`latest` を使わず具体的なバージョンを指定すべき理由)が含まれます。

#### ② OOMKilled (Out of Memory)
Podが突如として再起動する場合、原因の多くはリソース制限にあります。Kubernetesでは `resources.limits.memory` を適切に設計しないと、ノード全体のメモリを圧迫し、Linuxカーネルによってプロセスが強制終了(OOR)されます。

#### ③ Liveness Probe と Readiness Probe の混同
ここが最も陥りやすい落とし穴の一つです。

  • Liveness Probe: 「コンテナが生きているか?」を確認します。失敗するとK8sはPodを再起動します。
  • Readiness Probe: 「リクエストを受け取れる状態か?」を確認します。失敗すると、ServiceからそのPodへのトラフィックを遮断します。
例えば、DB接続待ちの間にLiveness Probeが失敗すると、アプリが立ち上がる前に再起動を繰り返す(CrashLoop)ことになります。逆に、重いバッチ処理の最中にReadiness Probeが反応しなくなると、ロードバランサーから切り離されます。これらを正しく設計しないと、不安定なシステムになってしまいます。

実践的なDeployment定義例

以下は、リソース制限とプローブを適切に組み込んだ実戦的な構成例です。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-gateway
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-gateway
  template:
    metadata:
      labels:
        app: api-gateway
    spec:
      containers:
      - name: main
        image: my-repo/api-gateway:v1.2.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "256Mi"
            cpu: "500m"
          limits:
            memory: "512Mi"
            cpu: "1000m"
        readinessProbe:
          http:
            path: /healthz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          http:
            path: /healthz
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20


3. 比較と選択:Docker Compose vs Kubernetes

「結局、いつKubernetesを使うべきなのか?」という問いは、現場のエンジニアが常に直面する重要な課題です。すべての環境をK8sで動かす必要はありません。

項目Docker ComposeKubernetes
主な用途開発環境、小規模な単一サーバー運用大規模プロダクション、マルチノード構成
スケーラビリティ手動または単純なレプリカ設定自動スケーリング(HPA)による高度な制御
自己修復基本なし(コンテナ落ちれば再起動のみ)自動検知と再スケジュール(Self-healing)
ネットワーク単純なブリッジ/ホストネットワークCNIを用いた複雑なサービスメッシュ、Ingress
学習コスト低い(直感的で理解しやすい)高い(概念が多く習得に時間がかかる)

選定の判断基準

  • 開発環境: Docker Composeが圧倒的に有利です。シンプルにコンテナを立ち上げ、ローカルでのデバッグに集中できるためです。
  • 小規模なWebサービス: 運用コストを最小限に抑えるなら、Docker Composeや単一サーバーでの実行で十分なケースが多く存在します。
  • 高可用性・大規模トラフィック: これらが必要な時点でKubernetesを選択すべきです。オートスケーリング、ローリングアップデート、セルフヒーリングといった機能は、手動運用では限界があるためです。

4. まとめ:技術の深淵を理解するために

DockerやKubernetesを「ただ使える」レベルから、「現場で信頼される」プロフェッショナルなレベルに引き上げるためには、抽象化の裏側にある仕組みを理解することが不可欠です。

  • Dockerにおいては、レイヤー構造を意識したビルド最適化と、環境変数の適切な分離。
  • Kubernetesにおいては、リソース管理(Requests/Limits)とヘルスチェック(Liveness/Readiness)の緻密な設計。
これらを疎かにすると、運用フェーズに入った際に「原因不明の不安定さ」として表面化します。技術のトレンドを追いかけるだけでなく、「なぜその機能が存在するのか」「どのような課題を解決するために作られたのか」を深く掘り下げていくことこそが、中堅エンジニアとしての腕の見せ所です。

次の一歩として、意図的にリソース制限を厳しくしたり、ネットワークを複雑に切り離したりする「壊しながらのデバッグ」を試してみることを強く推奨します。あえて極限状態を意図的に作り出し、その挙動を観察することこそが、最も早く上達する近道です。