PONCOTSU

インフラ領域を主にあれしてます

ServiceとPodだけでほぼダウンタイムゼロのリリースを実現する

ラベルを使ってPodを世代管理しておくと、Serviceを更新するだけでリリース(トラフィックルーティングを切り替える)を行なうことができて便利です。

要約

  • 実装
    • Podにラベルを付与し、バージョン管理を行なう(dockerimageのタグと一致させておくとミスリードを減らせることが多いのでおすすめ)
    • バージョンごとにDeploymentを用意し、複数バージョンがRUNNINGの状態をつくる
    • Serviceでトラフィックを流すPodを切り替える
  • メリット
    • アプリケーションのデプロイとサービス公開の作業を分離できる
    • Service(L4レイヤの設定)を切り替えるだけなので影響範囲・切り替えコストを最小化できる
    • Deploymentの再applyによるダウンタイムを極力ゼロにできる
  • デメリット
    • 一時的にクラスタのリソースを過分に利用してしまう
    • 古いバージョンのお掃除が大変

詳細

全体像

扱うKubernetesリソースが2つ(ServiceとDeployment)だけなので最初にmanifestの全体像を貼ります。 この設定では hoge-0.0.1hoge-0.0.2という2つのDeploymentが用意されており、現在Serviceは hoge-0.0.1に紐づけられているPod( label['version']: v0.0.1) に向けられています。

apiVersion: v1
kind: Service
metadata:
  name: hoge
  labels:
    app: hoge
spec:
  type: ClusterIP
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: hoge
    version: 0.0.1
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hoge-0.0.1
  labels:
    app: hoge
    version: 0.0.1
spec:
  replicas: 10
  selector:
    matchLabels:
      app: hoge
  template:
    metadata:
      labels:
        app: hoge
        version: 0.0.1
    spec:
      containers:
      - name: hoge
        image: tacumaigei/hoge:0.0.1
        ports:
        - containerPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hoge-0.0.2
  labels:
    app: hoge
    version: 0.0.2
spec:
  replicas: 10
  selector:
    matchLabels:
      app: hoge
  template:
    metadata:
      labels:
        app: hoge
        version: 0.0.2
    spec:
      containers:
      - name: hoge
        image: tacumaigei/hoge:0.0.2
        ports:
        - containerPort: 8080

現在のPodの状況はこう(以降、すべて namespaceはdefaultとします)

kubectl get deployment
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
hoge-0.0.1   10/10   10           10          8h
hoge-0.0.2   10/10   10           10          8h

kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
hoge-0.0.1-5d45d5845d-7lqpw   1/1     Running   1          8h
hoge-0.0.1-5d45d5845d-85lxj   1/1     Running   1          8h
hoge-0.0.1-5d45d5845d-98cnl   1/1     Running   1          8h
hoge-0.0.1-5d45d5845d-b96ff   1/1     Running   1          8h
hoge-0.0.1-5d45d5845d-jx7tc   1/1     Running   1          8h
hoge-0.0.1-5d45d5845d-knjng   1/1     Running   1          8h
hoge-0.0.1-5d45d5845d-mzwrq   1/1     Running   1          8h
hoge-0.0.1-5d45d5845d-t2h84   1/1     Running   1          8h
hoge-0.0.1-5d45d5845d-wxpd6   1/1     Running   1          8h
hoge-0.0.1-5d45d5845d-zgnq6   1/1     Running   1          8h
hoge-0.0.2-f579c9474-49bx5    1/1     Running   1          8h
hoge-0.0.2-f579c9474-7lkfz    1/1     Running   1          8h
hoge-0.0.2-f579c9474-9bxd7    1/1     Running   1          8h
hoge-0.0.2-f579c9474-b7qmg    1/1     Running   1          8h
hoge-0.0.2-f579c9474-ctn6v    1/1     Running   1          8h
hoge-0.0.2-f579c9474-jc4x6    1/1     Running   1          8h
hoge-0.0.2-f579c9474-jkfrd    1/1     Running   1          8h
hoge-0.0.2-f579c9474-lrj42    1/1     Running   1          8h
hoge-0.0.2-f579c9474-p2427    1/1     Running   1          8h
hoge-0.0.2-f579c9474-rzv6q    1/1     Running   1          8h

kubectl get svc
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
hoge         ClusterIP   10.96.222.54   <none>        80/TCP    8h
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP   10h

利用しているアプリケーション(dockerfile)は適宜ご自身のアプリケーションと思ってください。この例で利用しているアプリケーションは / にリクエストすると HOGE: Hello World! が返ってくるだけの簡易アプリケーションです(リポジトリ))。

ここで重要なことが2つあります。

  1. Deploymentの名前はバージョンごとに設定すること(今回はhoge-0.0.1 hoge-0.0.2としています)
  2. ServiceのSelectorはPodに対して行われる

1.の設定は複数アプリケーションをバージョン違いで共存させるために必要です。Deploymentに紐づいているPodのlabelだけバージョンを上げてDeployment名を同じままにしておくとDeploymentを更新してしまうことになるため注意です。

2.の設定は理解に注意が必要です。ServiceはあくまでPodを認知する機構であり、Deploymentを認知するわけではないということです。Selectorの設定で Deploymentのラベルであるapp: hoge-0.0.1を設定してもこの名前のPodを探しにいってしまい、見つかりません。 なのでPodのラベルを使って、適切にPodを認知させてください。

Serviceを hoge-v0.0.2に切り替え

ここが本題。 現在 ラベル app:hoge version: 0.0.1 と設定されているPodに向けられているServiceの設定を version: 0.0.2 へ変更します。

apiVersion: v1
kind: Service
metadata:
  name: hoge
  labels:
    app: hoge
spec:
  type: ClusterIP
  ports:
  - name: http
    protocol: TCP
    port: 80
    targetPort: 8080
  selector:
    app: hoge
    version: 0.0.2           # 変更箇所
kubectl apply -f service.yaml

これでDeployment hoge-0.0.2 に切り替わりました。 めちゃくちゃ楽ですね。

アプリケーションのdockerimageが作成されたらdeploymentファイルをクラスタにapply、という一連の処理を自動化しておけば、リリース時にはServiceの切り替え作業だけにすることができ、かつダウンタイムがほぼゼロに収まる運用を実現できます。

検証結果

念のため、ローカルでkindを立ち上げて検証しました。 同じnamespace内にcurlコマンドを叩くためのPodを用意し、Service hoge にリクエストを流し続けている間にcurlを叩き続けた結果がこちらです。

# hogeと同じnamespace内に立てたPodの中
$ for i in {1...100000}; do curl hoge:8080/; done
HOGE: Hello,World!HOGE: Hello,World!HOGE: Hello,World!HOGE: Hello,World!HOGE: Hello,World!HOGE: Hello,World!HOGE: Hello,World!HOGE 2!!!!!!!!: Hello,World!HOGE 2!!!!!!!!: Hello,World!HOGE 2!!!!!!!!: Hello,World!HOGE 2!!!!!!!!: Hello,World!HOGE 2!!!!!!!!: Hello,World!HOGE 2!!!!!!!!:

検証した限りはリクエストの欠損なく切り替えられていることが確認できました。 これはクラスタの負荷状況やリクエスト数によっても変わるところなのでダウンタイムゼロ!と断言することはできない部分ですが、一番コスト低く新しいPodをサービス公開する手順として覚えておくとよさそうです。