はじめに
コンテナオーケストレーションのデファクトスタンダードである Kubernetes(k8s)。 学習環境としてローカルにクラスタを構築する場合、minikube や Docker Desktop など様々な選択肢がありますが、今回は軽量かつ CI での利用も視野に入れた Kind (Kubernetes in Docker) を採用しました。
しかし、k8s 環境での開発には一つ大きな課題があります。それは、HTML や Python のコードを一行書き換えるたびに発生する 「コンテナのビルドとデプロイ」の手間です。毎回数秒〜数十秒待たされるのは、開発体験としてやりすぎですよね?そして「ローカルは Docker Compose でいいや」と妥協した結果、本番デプロイ時に「マニフェストの書き方違って動かない」となる可能性もあります。
そこで今回は、単に k8s でクラスタを立てるだけでなく、開発体験を意識して Skaffold を導入しました。これにより、以下のような開発環境を構築します。
-
マイクロサービス構成: フロントエンドとバックエンドを分離し、疎結合なアーキテクチャとする。
-
IaC (Infrastructure as Code): kubectl コマンドの手打ちではなく、マニフェストファイルで管理する。
-
開発体験の向上: Skaffold のファイル同期機能を活用し、ビルドなしで即座にコード変更を反映させる(ホットリロード)。
本記事では、これらの環境をシンプルな学習環境として構築した手順をまとめます。
0. 実行と確認
使用ツール
- Docker Desktop
- kind
- kubectl
- skaffold
ツールのインストールが初めての方はインストールが必要です。
macOS の場合(Homebrew)
brew install kind kubectl skaffold
Windows / その他
各公式サイトの手順に従ってインストールしてください。
- Docker: https://docs.docker.com/desktop/install/windows-install/
- Kind: https://kind.sigs.k8s.io/docs/user/quick-start/
- kubectl: https://kubernetes.io/docs/tasks/tools/
- Skaffold: https://skaffold.dev/docs/install/
コード
実行手順
- リポジトリをクローン
git clone https://github.com/shayate811/kubernetes-kind-skaffold
cd kubernetes-kind-skaffold
- Kind クラスターの作成。設定ファイルなしだとシングルノードのデフォルト構成になります。
kind create cluster --name microservices-demo
- Skaffold の実行 クラスタが起動したら、Skaffold で開発モードを開始します。
skaffold dev
(必要であれば別のターミナルを開いて実行)ノードが 1 台、Pod が 2 つ動いていることを確認
kubectl get nodes
kubectl get pods
# 出力例:
NAME STATUS ROLES AGE VERSION
microservices-demo Ready control-plane 1m v1.x.x
NAME READY STATUS RESTARTS AGE
ai-backend-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
web-frontend-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
ブラウザで http://localhost:8080 にアクセスすると、Nginx の画面が表示され、http://localhost:8080/api/ を叩くと Python コンテナからの応答 json 形式が返ってきます。(意図的に 機械学習、AI 等の処理時間として数秒待たせる設定にしてます。)
skaffold dev が動いているターミナルはそのままにして、frontend の index.html を編集する。すると即座に変更が反映される。
Syncing 1 files ... (青文字: 3 行目)
ここが最大のハイライトです。「Building...」ではなく「Syncing」となっている点が、Docker ビルドをスキップして高速に反映した証拠です。実際、すぐに http://localhost:8080 に index.html の変更内容が反映されていることを確認してみてください。
Watching for changes... (黄文字: 4 行目)
同期が一瞬で終わり、すぐに次の待機状態に戻っていることがわかります。
最後に起動中のサーバを止めて以下のコマンドでクラスタを削除する
kind delete cluster --name microservices-demo
1. アーキテクチャ
今回は、以下のシンプルな 1 ノードマイクロサービス構成を想定しました。
- Frontend (Web): Nginx (Alpine)。静的コンテンツを配信し、API リクエストをバックエンドへプロキシする。
- Backend (AI): Python (Flask)。重い処理(機械学習、AI 推論を想定)を担当する。
- Infrastructure: Kind (Kubernetes in Docker)。
ディレクトリ構成
責務の分離を意識し、以下のようなモノレポ構成としています。
kubernetes-kind-skaffold/
├── backend-ai/ # Backend (Python) のソースとDockerfile
├── frontend-web/ # Frontend (Nginx) のソースとDockerfile
├── k8s/ # Kubernetesマニフェスト (Deployment, Service, ConfigMap)
└── skaffold.yaml # Skaffoldによるワークフロー定義
2. Kubernetes マニフェストによる定義
インフラの状態はコードで定義され、バージョン管理されるべきです(IaC)。 そのため、yaml ファイルに記述する方法を採用しました。これにより、誰がいつ実行しても同じ環境が再現される状態を担保します。
Backend (ClusterIP)
バックエンドは外部に公開する必要がないため、ClusterIP でクラスタ内部からのみアクセス可能な状態にします。
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-backend
spec:
replicas: 1
selector:
matchLabels:
app: ai-backend
template:
metadata:
labels:
app: ai-backend
spec:
containers:
- name: ai-backend
image: my-ai-backend:v1
imagePullPolicy: IfNotPresent # Kind内のイメージを使う設定
ports:
- containerPort: 5000
---
apiVersion: v1
kind: Service
metadata:
name: ai-backend-svc
spec:
type: ClusterIP # 外部には公開しない
selector:
app: ai-backend
ports:
- port: 5000
targetPort: 5000
Frontend (LoadBalancer)
フロントエンドは外部からのアクセスを受け付けるため、LoadBalancer を採用します。また、Nginx の proxy_pass 設定により、/api/ へのリクエストのみをバックエンドへ転送する設定を ConfigMap で注入しています。
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-conf
data:
default.conf: |
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
}
# マイクロサービスのつなぎ目
# /api/ に来たアクセスを、裏の AIサービス(ai-backend-svc) に転送する
location /api/ {
proxy_pass http://ai-backend-svc:5000/;
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-frontend
spec:
replicas: 1
selector:
matchLabels:
app: web-frontend
template:
metadata:
labels:
app: web-frontend
spec:
containers:
- name: web-frontend
image: my-web-frontend:v1
imagePullPolicy: IfNotPresent # Kind内のイメージを使う設定
ports:
- containerPort: 80
volumeMounts:
- name: nginx-conf-vol
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
volumes:
- name: nginx-conf-vol
configMap:
name: nginx-conf
---
apiVersion: v1
kind: Service
metadata:
name: web-frontend-svc
spec:
type: LoadBalancer
selector:
app: web-frontend
ports:
- port: 80
targetPort: 80
⚠️ Kind 特有の「ハマったポイント」: imagePullPolicy について
ここで設定している imagePullPolicy: IfNotPresent は、Kind 環境において必須の設定です。これを書き忘れたり、Alwaysに設定するとエラーが発生し、Pod が永遠に起動しません。
なぜ必要なのか?
通常、Kubernetes はイメージを取得するために Docker Hub などの外部レジストリへアクセスしようとします(Pull)。
しかし、今回は「ローカルでビルドしたイメージ」を使用します。Kind のノードはホストマシンの Docker イメージを直接参照できないため、Skaffold が裏側で Kind ノード内へイメージをロード(転送)してくれています。
そのため、k8s に対して 「外部へ Pull しに行かず、ノード内にロードされたローカルイメージを使いなさい」と指示するために、IfNotPresentやNever を指定する必要があります。
containers:
- name: ai-backend
image: my-ai-backend:v1
imagePullPolicy: IfNotPresent # <--- これがないと動きません!
3. Skaffold による開発ワークフローの自動化
k8s 開発における最大の課題は、「コード修正 → ビルド → イメージロード → デプロイ」というサイクルの長さです。これを解消するために Skaffold を導入しました。
skaffold.yaml の設定
apiVersion: skaffold/v4beta3
kind: Config
metadata:
name: microservices-demo
build:
artifacts:
- image: my-ai-backend
context: backend-ai
docker:
dockerfile: Dockerfile
# Pythonファイルを直接コンテナに同期する(ビルドしない)
sync:
infer:
- "**/*.py"
- image: my-web-frontend
context: frontend-web
docker:
dockerfile: Dockerfile
# HTMLファイルを直接コンテナに同期する(ビルドしない)
sync:
infer:
- "**/*.html"
manifests:
rawYaml:
- k8s/*.yaml
deploy:
kubectl: {}
portForward:
- resourceType: service
resourceName: web-frontend-svc
port: 80
localPort: 8080
まとめ: 開発体験の変化
Skaffold の File Sync 機能を利用することで、以下のような開発体験を実現しました。
-
ローカルで index.html や app.py を編集し、保存する。
-
Skaffold が変更を検知。html の変更だけだなと判断。
-
Docker ビルドをスキップし、変更ファイルのみを稼働中のコンテナに転送。
-
数ミリ秒〜数秒で変更が反映される。
これは、Node.js 開発における nodemon のようなホットリロードのような体験を、k8s という重厚なコンテナオーケストレーション環境上で実現するものです。
Kind と Skaffold を組み合わせることで、ローカル PC 上に k8s 環境を再現しつつ、スクリプト言語のような軽快な開発サイクルを回すことが可能になりました。
参考文献
⚠️本記事はzennでも公開しているものになります
https://zenn.dev/shayate811/articles/k8s-beginner
