はじめに
Kubernetesのデプロイ戦略として、DeploymentとServiceだけでカナリアデプロイを動かしてみます。
Kubernetesでの代表的なデプロイ戦略については、下記でざっくりまとめています。

カナリアデプロイとは
カナリアデプロイは、少数の新しいバージョンをデプロイし、問題がなければ徐々に新しいバージョンを増やすデプロイ方法です。
メリット | デメリット |
---|---|
・ダウンタイムがゼロ ・すぐに旧バージョンに戻せる ・新しいバージョンを本番環境で試せる ・様子を見ながら新しいバージョンに移行できる | ・ロールアウトまで時間がかかる ・モニタリングの手間がかかる |
実際に試してみる
実際にカナリアデプロイをやってみます。
カナリアデプロイを実現する方法はいくつかあると思いますが、今回はDeploymentとServiceで実装します。
現行のバージョンと新しいバージョンの共有のラベルに対して、Serviceからトラフィックを流し、新しいバージョンのレプリカ数を増やしていくことで実装します。
作成するマニフェスト
今回、作成するマニフェストは下記の通りです。
.
├── configmap-v1.yml # v1のhtml用
├── configmap-v2.yml # v2のhtml用
├── deployment-v1.yml # v1のDeployment
├── deployment-v2.yml # v2のDeployment
└── service.yml # アプリのService
v1アプリのデプロイ
まずは、v1のアプリをデプロイします。
nginxで表示するHTMLファイルを、ConfigMapを使ってマウントします。
configmap-v1.yml
は下記の通りです。
apiVersion: v1
kind: ConfigMap
metadata:
name: index-html-v1
data:
index.html: |-
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body style="background-color: blue">
<h2>
v1 app.
</h2>
</body>
</html>
v1アプリは、v1用のConfigMapをマウントしたものになります。
deployment-v1.yml
は、下記の通りです。
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-v1
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp # アプリ共通のラベル
version: v1 # バージョンを表すラベル
spec:
containers:
- name: web
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: html-volume
mountPath: /usr/share/nginx/html
volumes:
- name: html-volume
configMap:
name: index-html-v1
アプリのServiceは、共通のラベルをセレクタとして使用します。バージョンのラベルはセレクタとして使用しません。
service.yml
は下記の通りです。
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
ports:
- name: http
port: 80
selector:
app: myapp # アプリ共通のラベル
type: NodePort
ConfigMap、Deployment、Serviceそれぞれ作成します。
kubectl apply -f configmap-v1.yml,deployment-v1.yml,service.yml
それぞれ作成されたのが確認できます。
❯ kubectl get pod,svc,cm
NAME READY STATUS RESTARTS AGE
pod/myapp-v1-556b75687-65ckg 1/1 Running 0 11s
pod/myapp-v1-556b75687-6mf28 1/1 Running 0 11s
pod/myapp-v1-556b75687-h8s26 1/1 Running 0 11s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/myapp-service NodePort 10.100.240.251 <none> 80:32207/TCP 11s
NAME DATA AGE
configmap/index-html-v1 1 11s
curl
でNodePortを指定して確認してみると、v1のアプリが動作していることが確認できます。何度実行してもv1のアプリからしかレスポンスは返ってきません。
❯ curl localhost:32207
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body style="background-color: blue">
<h2>
v1 app.
</h2>
</body>
</html>
v2アプリのデプロイ
v2のアプリの準備をしていきます。
v2のConfigMapのマニフェストconfigmap-v1.yml
は下記の通りです。
apiVersion: v1
kind: ConfigMap
metadata:
name: index-html-v2
data:
index.html: |-
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body style="background-color: green">
<h2>
v2 app.
</h2>
</body>
</html>
v2のDeploymentのマニフェストdeployment-v2.yaml
は下記の通りです。
v2のアプリはv2のConfigMapをマウントしており、レプリカ数は少なく、v2を表すラベルが追加されています。
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-v2
spec:
replicas: 1 # 最初はレプリカ数を少なく
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp # アプリ共通のラベル
version: v2 # バージョンを表すラベル
spec:
containers:
- name: web
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: html-volume
mountPath: /usr/share/nginx/html
volumes:
- name: html-volume
configMap:
name: index-html-v2
ConfigMapとDeploymentを作成します。
kubectl apply -f configmap-v2.yml,deployment-v2.yml
現行のバージョン(v1)のPodが3つ、新しいバージョン(v2)のPodが1つで、同じServiceからトラフィックが流れてくるようになりました。
❯ kubectl get pod,svc,cm
NAME READY STATUS RESTARTS AGE
pod/myapp-v1-556b75687-65ckg 1/1 Running 0 2m39s
pod/myapp-v1-556b75687-6mf28 1/1 Running 0 2m39s
pod/myapp-v1-556b75687-h8s26 1/1 Running 0 2m39s
pod/myapp-v2-6d55d79f9f-2hg7q 1/1 Running 0 9s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/myapp-service NodePort 10.100.240.251 <none> 80:32207/TCP 2m39s
NAME DATA AGE
configmap/index-html-v1 1 2m39s
configmap/index-html-v2 1 9s
動作確認
curl
で動作確認してみると、数回に一度ぐらいの頻度でv2アプリからのレスポンスが返ってきます。
❯ curl localhost:32207
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body style="background-color: blue">
<h2>
v1 app.
</h2>
</body>
</html>
❯ curl localhost:32207
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body style="background-color: blue">
<h2>
v1 app.
</h2>
</body>
</html>
❯ curl localhost:32207
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body style="background-color: blue">
<h2>
v1 app.
</h2>
</body>
</html>
❯ curl localhost:32207
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body style="background-color: green">
<h2>
v2 app.
</h2>
</body>
</html>
この状況で新しいバージョンの動作を確認します。
バージョンの入れ替え
少ないレプリカ数で新しいバージョンの動作に自信が持てたら、レプリカ数を増やして、新しいバージョンのみにしていきます。
徐々に増やしていくこともできますが、今回は一気にレプリカ数を逆転させていきます。
kubectl scale --replicas=3 deploy myapp-v2
kubectl scale --replicas=0 deploy myapp-v1
新しいバージョンのみになりました。
❯ kubectl get deploy,pod
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/myapp-v1 0/0 0 0 9m49s
deployment.apps/myapp-v2 3/3 3 3 7m19s
NAME READY STATUS RESTARTS AGE
pod/myapp-v2-6d55d79f9f-2hg7q 1/1 Running 0 7m19s
pod/myapp-v2-6d55d79f9f-blrv5 1/1 Running 0 64s
pod/myapp-v2-6d55d79f9f-xp544 1/1 Running 0 64s
curl
で動作確認しても、v1からのレスポンスは返ってこなくなっています。
❯ curl localhost:32207
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body style="background-color: green">
<h2>
v2 app.
</h2>
</body>
</html>
❯ curl localhost:32207
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
</head>
<body style="background-color: green">
<h2>
v2 app.
</h2>
</body>
</html>%
v1アプリの削除
最後に、古いバージョン(v1)のアプリはもう必要ないので、削除して完了です。
kubectl delete deploy myapp-v1