Kubernetes Deployment と Service の基本
Kubernetes (K8s) は コンテナを自動で起動・管理するためのオーケストレーションツール です。
最も基本的な構成は以下のようになります。
Deployment
↓
Pod
↓
Container
- Container
- Dockerコンテナそのもの
- Pod
- Kubernetesでコンテナを実行する最小単位(複数のアプリも持てる)
- Deployment
- Podを管理する仕組み
演習
flaskコンテナを3つ作ってHello World表示させるのを目標にしていくよ
必要なファイルとコマンド一覧
解説は下へ
- app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello Kubernetes!"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
- requirements.txt
flask
- Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
4.Dockerイメージ作成
docker build -t flask-hello:v1 .
5.deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-hello
spec:
replicas: 3
selector:
matchLabels:
app: flask-hello
template:
metadata:
labels:
app: flask-hello
spec:
containers:
- name: flask-hello
image: flask-hello:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5000
6.service.yaml
apiVersion: v1
kind: Service
metadata:
name: flask-hello-service
spec:
selector:
app: flask-hello
ports:
- port: 80
targetPort: 5000
type: LoadBalancer
7.サービス作成・Deployment作成
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
Deploymentとは
Deploymentは
- Podの作成
- Pod数の維持
- 自動復旧(Self Healing)
- ローリングアップデート
を行う管理機能です。
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask-hello
spec:
replicas: 3
selector:
matchLabels:
app: flask-hello
template:
metadata:
labels:
app: flask-hello
spec:
containers:
- name: flask-hello
image: flask-hello:v1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5000
各項目の説明
apiVersion
apiVersion: apps/v1
Deployment機能を利用するAPIバージョンです。
kind
kind: Deployment
作成するリソースの種類です。
今回は Deployment を作成します。
metadata
metadata:
name: flask-hello
Deploymentの名前です。
確認時に使用します。
kubectl get deployment
replicas
replicas: 3
同じPodを3個起動します。
Pod1
Pod2
Pod3
負荷分散や障害対策のために複数台起動します。
selector
selector:
matchLabels:
app: flask-hello
Deploymentが管理するPodを識別する条件です。
app=flask-hello
というラベルを持つPodを管理します。
template
template:
Deploymentが作成するPodのテンプレートです。
labels
labels:
app: flask-hello
作成されるPodへラベルを付与します。
app=flask-hello
後でServiceがこのラベルを利用してPodを探します。
containers
containers:
Pod内で起動するコンテナ一覧です。
image
image: flask-hello:v1
利用するDockerイメージです。
例:
docker build -t flask-hello:v1 .
imagePullPolicy
imagePullPolicy: IfNotPresent
イメージ取得方法を指定します。
| 設定 | 説明 |
|---|---|
| Always | 毎回取得 |
| IfNotPresent | なければ取得 |
| Never | 絶対取得しない |
containerPort
containerPort: 5000
アプリケーションが使用するポートです。
例えばFlaskなら
app.run(host="0.0.0.0", port=5000)
Deployment作成の流れ
kubectl apply -f deployment.yaml
実行すると
kubectl apply
↓
Deployment作成
↓
ReplicaSet作成
↓
Podを3個作成
↓
各Podでコンテナ起動
となります。
ReplicaSetとは
Deploymentの裏側で動く仕組みです。
Deployment
↓
ReplicaSet
↓
Pod
ReplicaSetが
replicas: 3
を維持します。
Pod確認
kubectl get pods
例
NAME READY STATUS
flask-hello-xxxx 1/1 Running
flask-hello-yyyy 1/1 Running
flask-hello-zzzz 1/1 Running
Podが壊れたら?
kubectl delete pod flask-hello-xxxxx
すると
Pod削除
↓
Deployment検知
↓
ReplicaSet検知
↓
新Pod作成
されます。
これが Kubernetes の自己修復(Self Healing)機能です。
Serviceとは
Serviceは
- Podの固定入口
- 負荷分散
- Service Discovery
を提供します。
ブラウザ
↓
Service
↓ ↓ ↓
Pod Pod Pod
なぜServiceが必要か
Deploymentだけだと
PodA
PodB
PodC
が作られます。
しかしPodには内部IPが付きます。
10.1.0.5
10.1.0.6
10.1.0.7
問題は
Pod再作成
↓
IP変更
が発生することです。
そのため
ブラウザ
↓
Podへ直接アクセス
は実用的ではありません。
service.yaml
apiVersion: v1
kind: Service
metadata:
name: flask-hello-service
spec:
selector:
app: flask-hello
ports:
- port: 80
targetPort: 5000
type: LoadBalancer
selector
selector:
app: flask-hello
Serviceは
app=flask-hello
のラベルを持つPodを自動発見します。
port
port: 80
Service側ポート
targetPort
targetPort: 5000
Pod側ポート
通信イメージ
http://Service:80
↓
Flask:5000
type
type: LoadBalancer
Service公開方法です。
ClusterIP
type: ClusterIP
クラスター内部のみ通信可能
Pod ←→ Pod
NodePort
type: NodePort
ノードのポートを公開
NodeIP:30000~
でアクセス可能
LoadBalancer
type: LoadBalancer
クラウド環境向け
AWSなら
Service
↓
AWS ELB
が自動作成されます。
EKSでは通常これを利用します。
Service作成
kubectl apply -f service.yaml
流れ
Service作成
↓
selector確認
↓
app=flask-hello Pod検索
↓
Service登録
そのほかのコマンド
port-forward
ローカルPCから簡単にアクセスする方法
kubectl port-forward service/flask-hello-service 8080:80
アクセス
localhost:8080
↓
Service:80
↓
Pod:5000
スケールアウト
Pod数を増やす
kubectl scale deployment flask-hello --replicas=10
Pod 3台
↓
Pod 10台
Deployment確認
kubectl get deployments
| 項目 | 意味 |
|---|---|
| READY | 起動中Pod数 |
| UP-TO-DATE | 最新設定適用済Pod数 |
| AVAILABLE | 利用可能Pod数 |
Deployment詳細確認
kubectl describe deployment flask-hello
確認できる内容
- replicas
- image
- selector
- events
- 更新履歴
YAML出力
kubectl get deployment flask-hello -o yaml
現在の設定を確認できます。
ローリングアップデート
例えば
image: flask-hello:v2
へ変更して
kubectl apply -f deployment.yaml
すると
旧Pod
↓
新Pod
↓
旧Pod削除
が順番に行われます。
サービス停止なしで更新できます。
再起動
kubectl rollout restart deployment flask-hello
Pod再作成
↓
新Pod起動
↓
旧Pod削除
ロールバック
履歴確認
kubectl rollout history deployment flask-hello
戻す
kubectl rollout undo deployment flask-hello
ログ確認
kubectl logs flask-hello-xxxxx
コンテナ標準出力を確認できます。
Pod内部へ入る
kubectl exec -it flask-hello-xxxxx -- sh
Ubuntu系なら
kubectl exec -it flask-hello-xxxxx -- bash
も利用可能です。
イベント確認
kubectl get events
エラー解析で非常によく使います。
例
ImagePullBackOff
CrashLoopBackOff
FailedScheduling
などの原因を確認できます。
リソース削除
Service削除
kubectl delete service flask-hello-service
Deployment削除
kubectl delete deployment flask-hello
内部的には
Deployment削除
↓
ReplicaSet削除
↓
Pod削除
となります。
Kubernetes全体像
Browser
│
▼
Service(LoadBalancer)
│
┌────────┼────────┐
▼ ▼ ▼
Pod1 Pod2 Pod3
│ │ │
▼ ▼ ▼
flask-hello flask-hello flask-hello
│
▼
Container
Deployment
│
▼
ReplicaSet
│
▼
Podを3台維持
この構成が Kubernetes の最も基本的な 「Deployment + Service」構成 であり、EKS・AKS・GKE などのマネージドKubernetesでも同じ考え方で動作します。
==============================================================================================================================================================================================
Pod
kind: Pod
Podとは
Kubernetesでコンテナを実行する最小単位です。
Dockerでは
docker run nginx
でコンテナを直接起動します。
一方Kubernetesでは
Pod
↓
Container
という構造になります。
つまり、
Docker
└─ Container
Kubernetes
└─ Pod
└─ Container
です。
なぜPodが必要なのか
Kubernetesはコンテナだけでなく、
- ネットワーク
- ストレージ
- 設定
- ライフサイクル管理
もまとめて管理したいからです。
そのため
Container
ではなく
Pod
という概念を導入しています。
Podの中身
最も単純なPod
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
spec:
containers:
- name: nginx
image: nginx
構造
nginx-pod
└─ nginx Container
Podは複数コンテナを持てる
実は
1 Pod = 1 Container
ではありません。
例
kind: Pod
spec:
containers:
- name: nginx
image: nginx
- name: log-agent
image: fluentd
構造
Pod
├─ nginx
└─ fluentd
例えば
Webサーバ
+
ログ収集コンテナ
を1つのPodに入れることがあります。
Pod内のコンテナは何を共有する?
同じPod内のコンテナは
ネットワーク共有
同じIPアドレスを持ちます。
Pod IP
10.244.1.20
nginx
fluentd
そのため
localhost
で通信できます。
例
nginx
↓ localhost:9000
fluentd
ストレージ共有
Volumeを共有できます。
Pod
├─ nginx
├─ fluentd
└─ shared-volume
kubectl applyしたときに起こること
例えば
kubectl apply -f pod.yaml
を実行すると
① kubectl
kubectl
が
Podを作ってください
というリクエストを送ります。
② API Server
API Server
が受け取ります。
③ etcd保存
Pod情報が保存されます。
etcd
Pod
name=nginx-pod
image=nginx
④ Scheduler
Schedulerが監視しています。
Pod発見
↓
どのNodeで動かす?
を判断します。
例
Node-1
CPU 50%
Node-2
CPU 10%
なら
Node-2
を選ぶことがあります。
⑤ kubelet
選ばれたNodeの
kubelet
が
Podを起動しろ
と指示を受けます。
⑥ containerd
containerdが
nginxイメージ取得
↓
コンテナ起動
します。
⑦ Running
最終的に
kubectl get pods
で
nginx-pod Running
になります。
Podのライフサイクル
Pending
まだ起動していない
Scheduler待ち
ContainerCreating
コンテナ作成中
Image Pull中
など
Running
正常起動
Succeeded
正常終了
Jobでよく見る
Failed
異常終了
CrashLoopBackOff
起動
↓
クラッシュ
↓
再起動
↓
クラッシュ
を繰り返している
PodのIP
Podには自動的にIPが付きます。
例
nginx-pod
↓
10.244.1.10
しかし
Pod削除
↓
再作成
すると
10.244.1.10
↓
10.244.1.32
に変わることがあります。
だからServiceが必要になります。
なぜ本番でPodを直接作らないのか
例えば
kind: Pod
だけで作ると
Pod死亡
↓
終わり
です。
再作成されません。
例
nginx-pod
↓
Node障害
↓
消滅
そのため本番では
kind: Deployment
を使います。
Deploymentなら
Pod死亡
↓
ReplicaSet検知
↓
新しいPod作成
となります。
PodとDeploymentの違い
Pod
Pod
└─ Container
特徴
単発
自己修復なし
スケールなし
Deployment
Deployment
└─ ReplicaSet
├─ Pod
├─ Pod
└─ Pod
特徴
自己修復あり
スケール可能
Rolling Update可能
実務での使い分け
Podを直接使うケース
学習
検証
デバッグ
一時的な動作確認
例
kubectl run nginx --image=nginx
Deploymentを使うケース
Flask
FastAPI
Django
Node.js
Spring Boot
Nginx
などのWebアプリ
StatefulSetを使うケース
MySQL
PostgreSQL
Redis
MongoDB
Kafka
などのDB
まとめ
Pod
↓
Kubernetesでコンテナを動かす最小単位
kubectl apply
↓
API Server
↓
etcd
↓
Scheduler
↓
kubelet
↓
containerd
↓
Container起動
Pod
└─ Container
ですが、
本番では通常
Deployment
StatefulSet
DaemonSet
の配下でPodを管理します。
==============================================================================================================================================================================================
Kubernetesの kind とは
KubernetesのJSON/YAMLでは、
apiVersion: apps/v1
kind: Deployment
のように kind を指定します。
これは
Kubernetesに何を作るのか
を表しています。
Kubernetesの基本構造
例えば
kubectl apply -f deployment.json
すると、
kubectl
↓
API Server
↓
etcdへ保存
↓
対応するControllerが検知
↓
実際のリソース作成
となります。
Namespace
kind: Namespace
役割
Kubernetes内の論理的な区画を作る。
例
kind: Namespace
metadata:
name: dev
apply時
Namespace作成
↓
etcd保存
終了。
Podは作られません。
イメージ
Cluster
dev
├─ mysql
├─ flask
└─ redis
prod
├─ mysql
├─ flask
└─ redis
Secret
kind: Secret
役割
パスワードやAPIキーを保存する。
例
kind: Secret
stringData:
password: secret123
apply時
Secret作成
↓
etcd保存
終了。
Pod起動時
env:
- valueFrom:
secretKeyRef:
で読み込まれる。
よく保存するもの
DBパスワード
APIキー
TLS証明書
JWT秘密鍵
ConfigMap
kind: ConfigMap
役割
設定ファイルを保存する。
例
kind: ConfigMap
data:
app.conf: |
port=8080
apply時
ConfigMap作成
↓
etcd保存
Pod起動時
volumeMounts
または
envFrom
で読み込まれる。
よく保存するもの
SQL
設定ファイル
ログ設定
アプリ設定
Pod
kind: Pod
kind: Pod
spec:
containers:
- name: nginx
image: nginx
- name: log-agent
image: fluentd
役割
コンテナを起動する最小単位。
例
kind: Pod
apply時
Pod作成
↓
Scheduler
↓
Node決定
↓
kubelet
↓
containerd
↓
コンテナ起動
実際
Pod
└─ Container
注意
本番ではあまり直接作らない。ReplicaSet検知できないから
通常は
Deployment
StatefulSet
DaemonSet
を使う。
Service
kind: Service
役割
Podへの入口。
なぜ必要?
PodのIPは変わる。
10.1.1.1
↓
再起動
↓
10.1.1.8
Serviceを使うと
mysql-service
で固定アクセス可能。
apply時
Service作成
↓
CoreDNS登録
↓
Endpoint Controller監視
Pod起動後
Service
↓
Endpoint
↓
Pod
が紐付く。
Serviceの種類
ClusterIP
type: ClusterIP
内部通信専用。
NodePort
type: NodePort
NodeIP:30080
でアクセス。
LoadBalancer
type: LoadBalancer
AWSなら
ELB
ALB
NLB
が作られる。
Headless Service
clusterIP: None
StatefulSet向け。
Deployment
kind: Deployment
役割
ステートレスアプリを管理。
例
replicas: 2
apply時
Deployment作成
↓
ReplicaSet作成
↓
Pod作成
構造
Deployment
└─ ReplicaSet
├─ Pod
└─ Pod
Podが死んだら
Pod死亡
↓
ReplicaSet検知
↓
再作成
よく使うもの
Flask
FastAPI
Nginx
Node.js
Spring Boot
StatefulSet
kind: StatefulSet
役割
状態を持つアプリを管理。
Deploymentとの違い
Deployment
app-random1
app-random2
StatefulSet
mysql-0
mysql-1
固定名。
apply時
StatefulSet作成
↓
Pod作成
↓
PVC作成
↓
Volume割当
主な用途
MySQL
PostgreSQL
MongoDB
Redis Cluster
Kafka
PersistentVolume (PV)
kind: PersistentVolume
役割
実際のストレージ。
apply時
PV作成
AWSなら
EBS
EFS
FSx
と紐付く。
PersistentVolumeClaim (PVC)
kind: PersistentVolumeClaim
役割
ストレージ要求。
例
storage: 10Gi
apply時
PVC作成
↓
PV検索
↓
自動接続
イメージ
Pod
↓
PVC
↓
PV
↓
EBS
Job
kind: Job
役割
一度だけ実行する。
例
DB移行
バックアップ
集計
apply時
Job作成
↓
Pod起動
↓
処理実行
↓
成功
↓
終了
CronJob
kind: CronJob
役割
定期実行。
例
schedule: "0 0 * * *"
apply時
CronJob登録
実行時
毎日0時
↓
Job作成
↓
Pod起動
↓
処理実行
Ingress
kind: Ingress
役割
HTTPルーティング。
例
example.com
↓
Flask Service
api.example.com
↓
API Service
apply時
Ingress作成
↓
Ingress Controller検知
↓
Nginx設定生成
DaemonSet
kind: DaemonSet
役割
全Nodeに1個ずつPodを配置。
apply時
Node数確認
↓
各NodeへPod配置
主な用途
Fluentd
Datadog Agent
Prometheus Node Exporter
CloudWatch Agent
実務で最もよく使うKind
| Kind | 用途 |
|---|---|
| Namespace | 環境分離 |
| Secret | パスワード |
| ConfigMap | 設定ファイル |
| Service | 通信入口 |
| Deployment | Webアプリ |
| StatefulSet | DB |
| PVC | 永続ストレージ |
| Ingress | HTTP公開 |
| Job | 一回実行 |
| CronJob | 定期実行 |
| DaemonSet | 監視Agent |
このあたりの kind を理解すると、EKSやGKEの実務案件の8〜9割程度のKubernetes構成は読めるようになります。
Controllerとの対応表
| Kind | Controller |
|---|---|
| Deployment | Deployment Controller |
| ReplicaSet | ReplicaSet Controller |
| StatefulSet | StatefulSet Controller |
| DaemonSet | DaemonSet Controller |
| Job | Job Controller |
| CronJob | CronJob Controller |
| Service | Endpoint Controller |
| Namespace | API Serverのみ |
| Secret | API Serverのみ |
| ConfigMap | API Serverのみ |
Controllerは常にetcdを監視しており、
希望状態(Desired State)
と
現在状態(Current State)
の差分を埋め続けます。
これがKubernetesの基本思想である
宣言的管理(Declarative Management)
です。
StatefulSet と Deployment の違い
一言で言うと、
Deployment
→ ステートレスなアプリ向け
StatefulSet
→ 状態(データ)を持つアプリ向け
です。
ステートレスとステートフル
ステートレス
サーバがデータを持たない。
例
Flask
FastAPI
Nginx
Node.js
Spring Boot
例えば
Flask Pod-1
が消えても
Flask Pod-2
が同じ処理をできます。
ステートフル
サーバがデータを持つ。
例
MySQL
PostgreSQL
MongoDB
Redis
Kafka
例えば
mysql-0
に保存したデータは
mysql-1
にはありません。
そのため個体を識別する必要があります。
Deployment
構造
Deployment
└─ ReplicaSet
├─ Pod
├─ Pod
└─ Pod
Pod名
Deploymentはランダムになります。
例
flask-74d5c8f7f-p9mqx
flask-74d5c8f7f-v42jp
再作成されると
flask-74d5c8f7f-ab123
になることもあります。
スケール
replicas: 3
なら
Pod × 3
を維持します。
Podが落ちたら
Pod死亡
↓
ReplicaSet検知
↓
新しいPod作成
Serviceとの相性
Service
↓
負荷分散
↓
Pod1
Pod2
Pod3
が得意です。
StatefulSet
構造
StatefulSet
├─ mysql-0
├─ mysql-1
└─ mysql-2
Pod名
固定です。
mysql-0
mysql-1
mysql-2
再起動しても
mysql-0
は
mysql-0
のままです。
なぜ固定名が必要?
例えば
mysql-primary
mysql-replica
のような構成では
mysql-0
がPrimary
mysql-1
がReplica
として動作します。
名前が変わると困ります。
永続ストレージ
Deployment
Pod削除
↓
データ消える
ことが多いです。
StatefulSet
mysql-0
↓
PVC
↓
PV
を持ちます。
例
mysql-0
└─ mysql-data-mysql-0
mysql-1
└─ mysql-data-mysql-1
Podが再作成されても
mysql-0
は
mysql-data-mysql-0
を再利用します。
起動順序
Deployment
Pod1
Pod2
Pod3
を並列作成
StatefulSet
mysql-0
↓
起動完了
↓
mysql-1
↓
起動完了
↓
mysql-2
順番に作成
削除順序
Deployment
全部同時
StatefulSet
mysql-2
↓
mysql-1
↓
mysql-0
逆順で削除
DNS
Deployment と StatefulSet の DNS の違い
Kubernetesでは、Service が DNS 名を提供します。
Deployment の場合
Deploymentで作られるPodは、再作成されるたびに名前が変わります。
例
flask-6f4f9c8f7b-abcde
flask-6f4f9c8f7b-fghij
そのため、
flask-6f4f9c8f7b-abcde
のようなPod名を前提に通信してはいけません。
代わりに Service を作ります。
kind: Service
metadata:
name: flask-service
するとDNSで
flask-service
へアクセスできます。
通信すると、
flask-service
↓
Pod1
Pod2
Pod3
のどれかにロードバランシングされます。
Deploymentのイメージ
Client
↓
flask-service
↓
┌─────┬─────┬─────┐
↓ ↓ ↓
Pod1 Pod2 Pod3
どのPodに接続されるかは分かりません。
つまり
flask-service
しか知らなくてよい仕組みです。
StatefulSet の場合
StatefulSetは
mysql-0
mysql-1
mysql-2
のようにPod名が固定されます。
再起動しても
mysql-0
は常に
mysql-0
です。
Headless Service
通常のService
clusterIP: 10.0.0.100
↓
mysql-service
にアクセスするとロードバランシングされます。
Headless Service
clusterIP: None
にすると、
mysql-service
はロードバランサーを持ちません。
代わりに各PodのDNSが作られます。
例
kind: Service
metadata:
name: mysql-service
spec:
clusterIP: None
kind: StatefulSet
metadata:
name: mysql
するとDNSは
mysql-0.mysql-service
mysql-1.mysql-service
mysql-2.mysql-service
になります。
実際の名前の構造
<Pod名>.<Service名>
なので
mysql-0.mysql-service
は
Pod名 = mysql-0
Service名 = mysql-service
です。
実際にはさらに
mysql-0.mysql-service.default.svc.cluster.local
という完全なDNS名になります。
なぜ StatefulSet で必要なのか
MySQLクラスタを考えます。
mysql-0 = Primary
mysql-1 = Replica
mysql-2 = Replica
アプリ側で
mysql-0.mysql-service
へ接続すれば、
常にPrimaryへ接続できます。
Deploymentだと
mysql-service
しかなく、
Pod1
Pod2
Pod3
のどれに接続されるか分からないため、
データベースには向きません。
まとめ
| 項目 | Deployment | StatefulSet |
|---|---|---|
| Pod名 | 毎回変わる | 固定 |
| DNS | Serviceのみ | PodごとのDNSあり |
| 接続先 | どのPodか不明 | 特定Podを指定可能 |
| ロードバランシング | あり | Headless Serviceならなし |
| 用途 | Flask、API、Webサーバ | MySQL、PostgreSQL、Kafka、Redis |
覚え方は、
Deployment
→ 「同じPodを何個も並べる」
StatefulSet
→ 「1台1台に名前が付いている」
です。
そのため StatefulSet では
mysql-0.mysql-service
のような個別DNSが利用でき、MySQLやKafkaのような状態を持つアプリを運用できます。
YAML比較
Deployment
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 3
StatefulSet
apiVersion: apps/v1
kind: StatefulSet
spec:
serviceName: mysql-service
replicas: 3
StatefulSetにはDNS名を作るために
serviceName:
が必須です。
Kubernetes内部での違い
Deployment
kubectl apply
↓
Deployment作成
↓
Deployment Controller
↓
ReplicaSet作成
↓
Pod作成
StatefulSet
kubectl apply
↓
StatefulSet作成
↓
StatefulSet Controller
↓
PVC作成
↓
Pod作成
実務での使い分け
Deployment
Flask
FastAPI
Django
Node.js
Nginx
Apache
StatefulSet
MySQL
PostgreSQL
MongoDB
Redis Cluster
Kafka
Elasticsearch/OpenSearch
ZooKeeper
まとめ
| 項目 | Deployment | StatefulSet |
|---|---|---|
| 用途 | Webアプリ | DB |
| Pod名 | ランダム | 固定 |
| ストレージ | 任意 | ほぼ必須 |
| DNS | Service経由 | 個別DNSあり |
| 起動順序 | 並列 | 順番 |
| 削除順序 | 並列 | 逆順 |
| データ保持 | 弱い | 強い |
| 主な用途 | Flask, FastAPI, Nginx | MySQL, PostgreSQL, Kafka |
PVCとの関係
StatefulSetはPodごとに専用PVCを持ちます。
mysql-0
└─ PVC:mysql-data-mysql-0
mysql-1
└─ PVC:mysql-data-mysql-1
そのため
mysql-0削除
↓
mysql-0再作成
されても
mysql-data-mysql-0
が再利用されます。
結果として
Podは消えても
データは消えない
というDB向けの仕組みになっています。
実践
MysqlPod2つ、そのMysqlPodからデータを取得してブラウザに表示するFlaskPod2つを作っていきます。
Kubernetes構成
MySQL Pod × 2
Flask Pod × 2
Flask → MySQL Service → MySQL Pod
1. ファイル構成
k8s-mysql-flask/
├─ app.py
├─ Dockerfile
├─ requirements.txt
├─ namespace.json
├─ mysql-secret.json
├─ mysql-init-configmap.json
├─ mysql-service.json
├─ mysql-statefulset.json
├─ flask-service.json
└─ flask-deployment.json
2. Flaskアプリ
app.py
from flask import Flask
import mysql.connector
import os
app = Flask(__name__)
MYSQL_HOST = os.environ.get("MYSQL_HOST", "mysql-service")
MYSQL_USER = os.environ.get("MYSQL_USER", "root")
MYSQL_PASSWORD = os.environ.get("MYSQL_PASSWORD", "password")
MYSQL_DATABASE = os.environ.get("MYSQL_DATABASE", "sampledb")
@app.route("/")
def index():
conn = mysql.connector.connect(
host=MYSQL_HOST,
user=MYSQL_USER,
password=MYSQL_PASSWORD,
database=MYSQL_DATABASE
)
cursor = conn.cursor()
cursor.execute("SELECT id, name FROM users")
rows = cursor.fetchall()
cursor.close()
conn.close()
html = "<h1>Users from MySQL</h1><ul>"
for row in rows:
html += f"<li>{row[0]} : {row[1]}</li>"
html += "</ul>"
return html
@app.route("/health")
def health():
return "OK"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
requirements.txt
flask
mysql-connector-python
Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
3. Dockerイメージ作成
docker build -t flask-mysql-app:v1 .
4. Kubernetes JSONファイル
namespace.json
{
"apiVersion": "v1",
"kind": "Namespace",
"metadata": {
"name": "mysql-flask-demo"
}
}
mysql-secret.json
{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "mysql-secret",
"namespace": "mysql-flask-demo"
},
"type": "Opaque",
"stringData": {
"MYSQL_ROOT_PASSWORD": "password"
}
}
mysql-statefulset.jsonの下記で使用される
"env": [
{
"name": "MYSQL_ROOT_PASSWORD",
"valueFrom": {
"secretKeyRef": {
"name": "mysql-secret",
"key": "MYSQL_ROOT_PASSWORD"
}
}
}
],
mysql-init-configmap.json
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "mysql-init-config",
"namespace": "mysql-flask-demo"
},
"data": {
"init.sql": "CREATE DATABASE IF NOT EXISTS sampledb;\nUSE sampledb;\nCREATE TABLE IF NOT EXISTS users (\n id INT AUTO_INCREMENT PRIMARY KEY,\n name VARCHAR(255) NOT NULL\n);\nINSERT INTO users (name) VALUES ('Taro'), ('Hanako'), ('Jiro');\n"
}
}
mysql-statefulset.jsonの下記で使用される
"volumes": [
{
"name": "mysql-init",
"configMap": {
"name": "mysql-init-config"
}
}
]
mysql-service.json
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "mysql-service",
"namespace": "mysql-flask-demo"
},
"spec": {
"clusterIP": "None",
"selector": {
"app": "mysql"
},
"ports": [
{
"name": "mysql",
"port": 3306,
"targetPort": 3306
}
]
}
}
mysql-statefulset.jsonの下記で使用される
{
"apiVersion": "apps/v1",
"kind": "StatefulSet",
"metadata": {
"name": "mysql",
"namespace": "mysql-flask-demo"
},
"spec": {
"serviceName": "mysql-service",
"replicas": 2,
mysql-statefulset.json
{
"apiVersion": "apps/v1",
"kind": "StatefulSet",
"metadata": {
"name": "mysql",
"namespace": "mysql-flask-demo"
},
"spec": {
"serviceName": "mysql-service",
"replicas": 2,
"selector": {
"matchLabels": {
"app": "mysql"
}
},
"template": {
"metadata": {
"labels": {
"app": "mysql"
}
},
"spec": {
"containers": [
{
"name": "mysql",
"image": "mysql:8.0",
"ports": [
{
"containerPort": 3306,
"name": "mysql"
}
],
"env": [
{
"name": "MYSQL_ROOT_PASSWORD",
"valueFrom": {
"secretKeyRef": {
"name": "mysql-secret",
"key": "MYSQL_ROOT_PASSWORD"
}
}
}
],
"volumeMounts": [
{
"name": "mysql-data",
"mountPath": "/var/lib/mysql"
},
{
"name": "mysql-init",
"mountPath": "/docker-entrypoint-initdb.d"
}
]
}
],
"volumes": [
{
"name": "mysql-init",
"configMap": {
"name": "mysql-init-config"
}
}
]
}
},
"volumeClaimTemplates": [
{
"metadata": {
"name": "mysql-data"
},
"spec": {
"accessModes": ["ReadWriteOnce"],
"resources": {
"requests": {
"storage": "1Gi"
}
}
}
}
]
}
}
{
"apiVersion": "apps/v1", // StatefulSetは apps/v1 API を使う
"kind": "StatefulSet", // StatefulSetリソースを作成する
"metadata": {
"name": "mysql", // StatefulSetの名前を mysql にする
"namespace": "mysql-flask-demo" // mysql-flask-demo Namespace内に作成する
},
"spec": {
"serviceName": "mysql-service", // StatefulSetが使うHeadless Service名
"replicas": 2, // MySQL Podを2個起動する
"selector": {
"matchLabels": {
"app": "mysql" // app=mysql のPodをこのStatefulSetの管理対象にする
}
},
"template": {
"metadata": {
"labels": {
"app": "mysql" // 作成されるPodに app=mysql ラベルを付ける
}
},
"spec": {
"containers": [
{
"name": "mysql", // コンテナ名を mysql にする
"image": "mysql:8.0", // MySQL 8.0 のDockerイメージを使う
"ports": [
{
"containerPort": 3306, // コンテナ内の3306番ポートを使う
"name": "mysql" // ポート名を mysql にする
}
],
"env": [
{
"name": "MYSQL_ROOT_PASSWORD", // MySQL rootパスワード用の環境変数
"valueFrom": {
"secretKeyRef": {
"name": "mysql-secret", // mysql-secret というSecretを参照する
"key": "MYSQL_ROOT_PASSWORD" // Secret内の MYSQL_ROOT_PASSWORD を使う
}
}
}
],
"volumeMounts": [
{
"name": "mysql-data", // mysql-data ボリュームを使う
"mountPath": "/var/lib/mysql" // MySQLのデータ保存場所にマウントする
},
{
"name": "mysql-init", // mysql-init ボリュームを使う
"mountPath": "/docker-entrypoint-initdb.d" // 初期SQL自動実行用ディレクトリにマウントする
}
]
}
],
"volumes": [
{
"name": "mysql-init", // mysql-init というボリュームを定義する
"configMap": {
"name": "mysql-init-config" // mysql-init-config ConfigMapをファイルとして使う
}
}
]
}
},
"volumeClaimTemplates": [
{
"metadata": {
"name": "mysql-data" // Podごとの永続ボリューム名を mysql-data にする
},
"spec": {
"accessModes": ["ReadWriteOnce"], // 1つのNodeから読み書き可能なディスクにする
"resources": {
"requests": {
"storage": "1Gi" // Podごとに1Giの永続ストレージを要求する
}
}
}
}
]
}
}
flask-service.json
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "flask-service",
"namespace": "mysql-flask-demo"
},
"spec": {
"type": "NodePort",
"selector": {
"app": "flask"
},
"ports": [
{
"port": 80,
"targetPort": 5000,
"nodePort": 30080
}
]
}
}
{
"apiVersion": "v1", // Serviceは Kubernetes Core API の v1 を使う
"kind": "Service", // Serviceリソースを作成する
"metadata": {
"name": "flask-service", // Service名を flask-service にする
"namespace": "mysql-flask-demo" // mysql-flask-demo Namespace内に作成する
},
"spec": {
"type": "NodePort", // クラスタ外部から NodeIP:nodePort でアクセスできるServiceにする
"selector": {
"app": "flask" // app=flask ラベルを持つPodへ通信を流す
},
"ports": [
{
"port": 80, // Service自身が受け付けるポート番号
"targetPort": 5000, // 転送先のFlask Pod内ポート番号
"nodePort": 30080 // 外部からアクセスするためのNode側ポート番号
}
]
}
}
ブラウザ / curl
↓
NodeIP:30080
↓
flask-service:80
↓
Flask Pod:5000
flask-deployment.json
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "flask-deployment",
"namespace": "mysql-flask-demo"
},
"spec": {
"replicas": 2,
"selector": {
"matchLabels": {
"app": "flask"
}
},
"template": {
"metadata": {
"labels": {
"app": "flask"
}
},
"spec": {
"containers": [
{
"name": "flask",
"image": "flask-mysql-app:v1",
"imagePullPolicy": "IfNotPresent",
"ports": [
{
"containerPort": 5000
}
],
"env": [
{
"name": "MYSQL_HOST",
"value": "mysql-service"
},
{
"name": "MYSQL_USER",
"value": "root"
},
{
"name": "MYSQL_PASSWORD",
"valueFrom": {
"secretKeyRef": {
"name": "mysql-secret",
"key": "MYSQL_ROOT_PASSWORD"
}
}
},
{
"name": "MYSQL_DATABASE",
"value": "sampledb"
}
]
}
]
}
}
}
}
{
"apiVersion": "apps/v1", // Deploymentは apps/v1 API を使う
"kind": "Deployment", // Deploymentリソースを作成する
"metadata": {
"name": "flask-deployment", // Deployment名を flask-deployment にする
"namespace": "mysql-flask-demo" // mysql-flask-demo Namespace内に作成する
},
"spec": {
"replicas": 2, // Flask Podを2個起動する
"selector": {
"matchLabels": {
"app": "flask" // app=flask のPodをこのDeploymentの管理対象にする
}
},
"template": {
"metadata": {
"labels": {
"app": "flask" // 作成されるPodに app=flask ラベルを付ける
}
},
"spec": {
"containers": [
{
"name": "flask", // コンテナ名を flask にする
"image": "flask-mysql-app:v1", // 使用するDockerイメージ
"imagePullPolicy": "IfNotPresent", // ローカルにイメージがあればそれを使う
"ports": [
{
"containerPort": 5000 // Flaskアプリが待ち受けるコンテナ内ポート
}
],
"env": [
{
"name": "MYSQL_HOST", // MySQL接続先ホスト名
"value": "mysql-service" // Kubernetes Service名でMySQLへ接続する
},
{
"name": "MYSQL_USER", // MySQL接続ユーザー名
"value": "root" // rootユーザーで接続する
},
{
"name": "MYSQL_PASSWORD", // MySQL接続パスワード
"valueFrom": {
"secretKeyRef": {
"name": "mysql-secret", // mysql-secret Secretを参照する
"key": "MYSQL_ROOT_PASSWORD" // Secret内のMYSQL_ROOT_PASSWORDを使う
}
}
},
{
"name": "MYSQL_DATABASE", // 接続するデータベース名
"value": "sampledb" // sampledbへ接続する
}
]
}
]
}
}
}
}
5. kubectlコマンド
リソース作成
kubectl apply -f namespace.json
kubectl apply -f mysql-secret.json
kubectl apply -f mysql-init-configmap.json
kubectl apply -f mysql-service.json
kubectl apply -f mysql-statefulset.json
kubectl apply -f flask-service.json
kubectl apply -f flask-deployment.json
状態確認
kubectl get pods -n mysql-flask-demo
kubectl get svc -n mysql-flask-demo
kubectl get statefulset -n mysql-flask-demo
kubectl get deployment -n mysql-flask-demo
kubectl get pvc -n mysql-flask-demo
Flaskへアクセス
Minikube
minikube service flask-service -n mysql-flask-demo
または
curl http://localhost:30080
NodePort環境
http://<NodeIP>:30080
ログ確認
kubectl logs -n mysql-flask-demo deployment/flask-deployment
kubectl logs -n mysql-flask-demo mysql-0
kubectl logs -n mysql-flask-demo mysql-1
MySQLへ接続
kubectl exec -it -n mysql-flask-demo mysql-0 -- mysql -uroot -ppassword
SQL実行
USE sampledb;
SELECT * FROM users;
削除
kubectl delete namespace mysql-flask-demo
注意
この構成では
mysql-0
mysql-1
がそれぞれ独立したMySQLです。
レプリケーションは構成していないため、
mysql-0にINSERT
↓
mysql-1には反映されない
状態になります。
実運用では通常、
MySQL Primary
↓
MySQL Replica