【完成版】Docker/Kubernetes実践入門

技術ブログおよびガジェットレビューの専門編集者として、ご提示いただいたドラフを推敲・校正いたしました。

今回のリライトでは、「読者の共感を生むストーリーテリング」と「技術的な正確性の両立」を重視しました。特に、現場で実際に起こるトラブル(地雷)を具体的にイメージさせる表現に磨きをかけ、プロのエンジニアが書くブログとしての品格を高めています。


【完成版】Docker/Kubernetes実践入門

【絶報のデバッグ】DockerとKubernetesを実戦投入して気づいた「地雷」の数々(深夜の備忘録)

深夜2時。モニターから放たれる青白い光だけが、静まり返った部屋に反射している。
私は再び `kubectl get pods` を叩き続けながら、絶望に近い表情で画面を見つめていた。

ステータスは `CrashLoopBackOff`。
「なぜ動かないのか?」――ログを追いかければ一瞬で解決するはずのことなのに、原因の特定までに数時間を溶かしている。

「ローカルの Docker Compose で動いたんだから、Kubernetes でも動くはずだろ……」

そんな甘い期待は、マイクロサービスへの移行やスケーラビリティの追求といった高尚な目的の前で、無情にも打ち砕かれる。DockerとKubernetesの世界には、初心者(あるいは慣れていない中級者)が必ずと言っていいほど踏んでしまう「地雷」が至る所に埋められているのだ。

今日は、私がこれまでの開発現場で血を流しながら学んだ、コンテナ化とオーケストレーションにおける「ハマりポイント」の深淵について、あえて愚直に書き残しておこうと思う。


1. Dockerの落とし穴:それは単なる「隔離」ではない

多くのエンジニアが最初に躓くのは、「Dockerイメージをビルドしたのに、なぜか動かない」という問題だ。これは多くの場合、コンテナの抽象化の仕組みを正確に理解していないことに起因する。

イメージレイヤーとキャッシュの呪い

Dockerのビルドプロセスにおいて、各 `RUN` や `COPY` 指令は積み重なる「層(レイヤー)」として管理される。ここで初心者が陥りやすいのが、「環境変数の不適切な扱い」と「不要なファイルの混入」である。

例えば、`node_modules` を含むディレクトリを無防備なまま `COPY` してしまうケースだ。あるいは、ビルド時のみ必要なツール(curlやnpmなど)を削除せずにイメージに含めてしまうこともある。これらは最終的なイメージサイズを肥大化させるだけでなく、セキュリティリスクを増幅させる要因になる。

ここで必須となるのがマルチステージビルドの導入だ。ビルド用環境と実行用環境を完全に分離することで、ランタイムに必要なコンポーネントだけを抽出する手法である。

# --- 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
COPY package.json .

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

この構成を採用することで、イメージサイズを数百MB単位で削減でき、デプロイ速度も劇的に向上する。しかし、ここで見落としがちなのが「ビルドコンテキスト」の管理だ。`.dockerignore` を適切に設定していないと、巨大な `.git` ディレクトリや一時ファイルまでもがDockerデーモンに送信され、CI/CDパイプラインの実行時間を無駄に圧迫する。この「些細な差」が、運用のストレスを生む要因になることを覚えておいてほしい。

環境変数の迷宮

Docker Composeでは `env_file` を使って直感的に管理できていた環境変数が、Kubernetesへ移行した瞬間から戦略的な思考を要求されるようになる。ConfigMapとSecretの使い分け、そして権限管理……。

「ローカルでは動くのに、Pod内では環境変数が読み込めない」というトラブルの多くは、コンテナ起動時のシェルスクリプトによる初期化処理と、Docker/K8sが注入する環境変数の優先順位の競合で発生している。マニフェストを書き換える前に、まず「どこから変数が注入されているか」のパスを確実に把握することが重要だ。


2. Kubernetesの絶望:オーケストレーションの複雑性

Kubernetes(K8s)に足を踏み入れた瞬間、私たちは「宣言型」のパラダイムに突き動かされる。しかし、その洗練された抽象化の裏側には、ネットワークやリソース管理といった生々しいインフラの苦悩が隠されている。

CrashLoopBackOffと生存確認(Liveness/Readiness)

K8sで最も頻繁に遭遇するエラーの一つが `CrashLoopBackOff` だ。これは「コンテナは起動したが、即座に異常終了した」ことを示す警告である。

ここで初心者が陥る罠は、「アプリケーションの起動完了」と「プロセスの生存」を混同することにある。
例えば、JavaやPythonの重いフレームワークを使用している場合、プロセス自体は立ち上がっていても、内部的な初期化(データベース接続待ちなど)が終わっていない間にK8sが「このコンテナは死んでいる」と判断し、再起動を繰り返す。

これを防ぐのが `readinessProbe` と `livenessProbe` の適切な設計だ。

  • Readiness Probe: 「トラフィックを受け入れられる状態か?」を確認する。失敗すればサービスから切り離される。
  • Liveness Probe: 「プロセス自体が生きてるか?(死んでいたら再起動せよ)」を判断する。
この違いを理解せずに、両方に同じタイムアウトを設定したり、逆に過剰に厳しい設定にしたりすることで、不要な再起動ループや、予期せぬトラフィックの消失を引き起こすことになる。

リソース制限とOOMKiller

「自分のマシンでは動いたのに、K8s上では特定のタイミングで落ちる」という現象の正体は、多くの場合 OOMKiller (Out of Memory Killer) である。

Kubernetesでは `limits` と `requests` を設定するが、このバランス設計は非常にシビアだ。

  • requests: Podを動かすために最低限必要なリソース(予約枠)。

  • limits: これを超えたらK8sによって強制終了(OOMKill)される上限。


特にメモリに関しては、PythonやNode.jsなどのガベージコレクションの挙動と、コンテナのメモリ制限の相性が非常に悪いケースがある。アプリケーションが要求するメモリ量が一時的にスパイクしただけで、K8sは容赦なくPodを処刑する。この「リソースの壁」を意識せずにデプロイを行うと、原因不明の瞬断に悩まされることになるだろう。

ネットワークとサービス発見

`ClusterIP`, `NodePort`, `LoadBalancer`……。これらを使い分けるのは基礎知識だが、実際に現場で苦しむのはDNSの解決だ。 Kubernetes内部ではCoreDNSが動いているが、コンテナ間での通信において「名前解決が間に合わない」「特定のノードからしか届かない」といった問題が発生することがある。これはCNI(Container Network Interface)の設定や、ServiceとEndpointの紐付けに起因する。ネットワークの抽象化は便利だが、その裏にある物理的な制約を常に意識する必要がある。

3. 実践的な構成例:安定性を担保するためのDeployment定義

単に「動く」だけでなく、「運用に耐えうる」設定を意識したマニフェストを書くことがプロの仕事だ。以下は、リソース制限とプローブを適切に組み込んだ、実戦で通用するデプロイメントの典型的な例である。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app-deployment
  labels:
    app: web-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
      - name: web-app
        image: my-registry.com/web-app:v1.2.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        readinessProbe:
          http1.1:
            path: /healthz/ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
        livenessProbe:
          http1.1:
            path: /healthz/live
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 20
        env:
        - name: NODE_ENV
          value: "production"
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: db_host

この設定において、`initialDelaySeconds` を適切に取ることは極めて重要だ。アプリケーションの起動プロセスに時間がかかる場合、準備が整う前にプローブを叩きに行くと、不必要な再起動ループを引き起こすからだ。また、リソースの `limits` は「なんとなく余裕を持って」設定するのではなく、「実測値に基づいた最大想定値」を基に設計する必要がある。


4. 技術比較:Docker Compose vs Kubernetes

なぜあえて複雑なKubernetesを導入するのか? それは単に「コンテナを使いたいから」ではない。スケール、耐障害性、そして宣言的な管理といった「運用上の要件」を満たすためだ。しかし、その恩恵を得るためには、我々は一定の複雑さを引き受ける必要がある。

特徴項目Docker ComposeKubernetes (K8s)
主な用途開発環境、小規模な単一サーバーでの運用大規模プロダクション、マルチノードによるオーケストレーション
スケーリング手動または単純なレプリカ数指定(同一ホスト内)自動スケーリング (HPA) による動的なリソース配分
自己修復なし(プロセスが死んでも再起動するだけ)あり(ノード故障時のPod再配置、自動再起動)
ネットワークホスト内のブリッジネットワークを主に使用複雑なCNIによる動的なサービス発見とロードバランシング
学習コスト低い。Dockerの基本概念で習得可能。高い。YAML定義、リソース管理、抽象度の理解が必要。
設定の宣言性シンプルな構成を記述インフラ全体の状態(Desired State)を詳細に定義

結びに代えて:地雷を回避するためのマインドセット

DockerやKubernetesは魔法の杖ではない。それらは「複雑なシステムを、より予測可能な形で管理する」ための高度な道具である。
我々が困難に直面する原因の多くは、「ツールが提供する抽象化の裏側にある現実(ネットワークレイテンシ、リソース制約、プロセスのライフサイクル)」を見落とすことにある。

  • Dockerにおいては、軽量でクリーンなイメージを追求せよ。
  • Kubernetesにおいては、ステートレスな設計と適切なプローブ設定を徹底せよ。
この「地雷」の場所をあらかじめ把握しておくことで、深夜のデバッグ時間を削減し、よりクリエイティブな開発に集中できるようになるはずだ。

結局のところ、最高の技術は正しく理解し、意図した通りに制御できている時にのみ、その真価を発揮する。コンテナの世界へようこそ。ここからの旅は険しいが、乗り越えた先には広大なスケーラビリティの景色が待っている。