Docker/Kubernetes実践入門

Docker/Kubernetes実践入門

「Dockerなら動く」という甘い罠。Kubernetesへの移行で地獄を見た僕の記録

「コンテナ化すれば、どこでも動くようになる。それがインフラの正義だ」

そんな輝かしい理想を抱いてDockerを導入した数ヶ月後、私は絶望の淵に立っていた。
「ローカルでは動いたのに、なぜかステージング環境でDBに繋がらない」「イメージサイズが巨大すぎてビルドに10分かかる」「Kubernetes(K8s)への移行を検討した瞬間、ネットワーク周りの仕様の複雑さに脳が溶けそうになった」

今回は、私がコンテナ技術とオーケストレーションの荒波に揉まれながら学んだ、「教科書通りにはいかない現場のリアル」をありのままに書き記す。これからDockerやK8sを触ろうとしている人のための、遠回りな経験から得た教訓集だ。


1. Dockerの落とし穴:ただ「動く」だけでは不十分な理由

最初に多くの人が陥るのが、「Dockerfileを書けばいいだけでしょ?」という安易な思考だ。確かにコンテナは動く。しかし、実用的なアプリケーションを運用するとなると、「いかにクリーンで効率的なイメージを作るか」という設計思想が問われる。

#### 肥大化するイメージとレイヤーの罠
最初、私はNode.jsのアプリをDocker化する際、ただ`npm install`を実行しただけのDockerfileを書いていた。その結果、イメージサイズは1GBを超え、ビルドのたびにキャッシュが効かず数分間待たされるという事態に陥った。

原因は「Layer(レイヤー)」の仕組みを十分に理解していなかったことだ。
Dockerは各命令(RUN, COPY, ADDなど)を実行するたびに新しいレイヤーを作成する。Dockerfileで`COPY . .`を安易に実行すると、ソースコードの微細な変更があるたびに、その後の`npm install`を含むすべてのステップが再計算される。これがビルド時間の増大とイメージの肥大化を招く要因だ。

これを解決するために必須となるのが、Multi-stage Buildである。
ビルド用の環境(ビルドツールやコンパイラが含まれる)と、実行用の軽量な環境(AlpineやDistrolessなど)を分けることで、不要な要素を削ぎ落とし、セキュリティも向上させることができる。

以下は、私が「ただ動く」状態から「運用に耐えうる」構成へと改善したDockerfileの比較例だ。

# --- 悪い例(初心者がやりがちな構成) ---
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "src/index.js"]

# --- 良い例(Multi-stage Buildを採用) ---
# Build Stage: ビルドに必要なツールをすべて揃える環境
FROM node:18-alpine AS builder
WORKDIR /app
# 依存関係だけを先にコピー。これにより、package.jsonが変わらない限りキャッシュが効く
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/index.js"]

このように、「何をそのレイヤーに残すべきか」を意識するだけで、CI/CDの速度やデプロイの安定性は劇的に変わる。また、`.dockerignore`を適切に設定して、ローカルの`node_modules`や`.git`ディレクトリがイメージに含まれないようにするのは、もはやマナーに近い。


2. Kubernetesへの移行:ここからが本当の地獄の始まり

Docker Composeで動いていたアプリをKubernetesへ移行する時、多くの開発者が「ただYAMLの記述を増やすだけ」と勘違いする。しかし、K8sは単なるコンテナ管理ツールではない。それは分散システムを制御するための巨大な抽象化レイヤーである。

私が特に苦しめられたのは、以下の3点だ。

#### ① ネットワークの抽象化(Service, Ingress)
Docker Composeでは「localhost」や「サービス名」を指定するだけで通信ができればよかったが、K8sではServiceという概念を介して安定したエンドポイントを確保する必要がある。
さらに、外部公開のためのIngressの設定(Nginx Ingress Controllerなど)を組み込む際、パスの書き換えやSSL証明書のハンドリングといった「本来ならWebサーバーの設定で済んでいたこと」がすべてK8sのリソース定義に落とし込まれる。この抽象化のレイヤーを理解せずに進むと、どこで通信が遮断されているのか特定できず、デバッグの迷宮に迷い込むことになる。

#### ② 状態の管理(StatefulSet vs Deployment)
「DBをコンテナに入れるな」という格言は、K8sの世界ではより深刻な問題として現れる。
PodはEphemeral(一時的)な存在であり、ノードの故障やアップデートによって再起動すれば中身は消える。これを知らずにPersistent Volume (PV) や Persistent Volume Claim (PVC) を正しく設定せずにデータベースを動かそうとすると、再起動のたびにデータが消失する恐怖を味わうことになる。ステートフルなアプリケーションを動かすためのStatefulSetなどの仕組みを適切に使い分けることが不可欠だ。

#### ③ 設定の分離(ConfigMap, Secret)
環境変数として直接値を流し込むのではなく、`ConfigMap`や`Secret`を使うのが基本だが、ここでの「型」の扱い(特にBase64エンコードの有無など)や、更新時の反映タイミングなど、Docker Composeとは異なる挙動に戸惑うことが多い。


3. 実践的な比較:なぜKubernetesが必要なのか?

ここで、私が実際に頭を抱えながら整理した、開発環境と本番環境における運用思想の差異をテーブルにまとめる。この違いを理解することが、K8sへのスムーズな移行への第一歩だ。

項目Docker / Docker Compose (Local/Dev)Kubernetes (Production/Scale)
スケーリング手動または単純なレプリカ数指定HPA (Horizontal Pod Autoscaler) による自動拡張
セルフヒーリングコンテナ停止時に再起動するのみ異常検知によるPodの自動再生成・入れ替え
サービス発見名前解決(DNS)が単純なネットワーク内で行われるCoreDNSを介した高度な名前解決とLoadBalancer連携
ストレージホストマウントによる直接的な永続化PV/PVC、StorageClassを用いた動的なボリューム割り当て
構成管理YAMLファイルに直接記述することが多いConfigMap/Secretの分離とConfigの動的注入

4. 最後に:挫折を乗り越えるためのアドバイス

Kubernetesの世界は広義の意味で巨大であり、最初からすべてを完璧に理解しようとすると必ず挫折する。私が失敗した最大の理由は、「いきなり全部盛り」をしようとしたことだ。

もしあなたがこれからK8sに挑むなら、以下のステップを踏むことを強く勧める。

1. まずはDockerの基礎を徹底的に固める。
(Multi-stage Build, .dockerignore, 軽量なベースイメージの選定など)
2. ローカル環境でDocker Composeを使い倒す。
ネットワークやボリュームの概念を、コンテナ単体で動かす感覚を掴む。
3. MinikubeやKindを使って、小さなK8sクラスターを構築する。
最初からクラウドのマネージドサービス(EKS/GKE等)に突っ込むのではなく、ローカルで「Service」「Ingress」「ConfigMap」を一つずつ手動で定義して試行錯誤する。

技術の裏側にある「なぜこの仕組みが必要なのか?」という意図を理解しながら進めれば、K8sは非常に強力な武器になってくれるはずだ。

最初は難解なエラーメッセージに絶望するだろう。しかし、そのエラー一つひとつを解決するたびに、インフラの構造への理解が深まっていく感覚は、この技術スタックを学ぶ醍醐味でもある。

「動いた」の先にある、「安定して動き続けるための設計」を追求すること。 それこそが、コンテナとオーケストレーションの真髄だ。