PONCOTSU

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

Windows開発機では functions-framework-pythonのローカルエミュレータを動かせない

Cloud Functionsをそれなりに利用するようになると、動かす関数の信頼性・安全性を向上させる、あるいは開発体験を向上させるためにユニットテストをしっかり書くようにする・CIを回すようにするといった段階に進むことが多々あると思います。 そこで活用したいローカルエミュレータなのですが、Python製ライブラリだとWindowsを使う開発者を対象としておらず、開発環境を整えることが不可能だったよ、という話です。

要約

  1. GCP公式がCloud Functionsのローカルエミュレータを配布している
  2. PythonエミュレータWindows機では動かすことができない
  3. 回避策は「テスト対象を絞る」「別の言語でCloud Functionsで動かす関数を実装しなおす」

Cloud Functionsのローカル開発環境に利用するfunctions-frameworkシリーズ

Cloud Functionsを使ったサーバレスアーキテクチャを採用する際、ローカルで事前に検証・ユニットテストなどに活用するように、公式からCloud Functionsのエミュレータが配布されています。 今回はこのPythonで書かれたエミュレータfunctions-framework-python)はWindowsでは動かせられないよ、という趣旨の話です。 なおPythonの他にNode.js・JavaRubyでも同様のライブラリが開発されています。

本題

PythonエミュレータWindows機では動かすことができない

Pythonで書かれたエミュレータfunctions-framework-python)は、内部で Flask が使われています。 Flask とはPythonで書かれたウェブサーバで、他の言語でいうところの Sinatra(Ruby) express.js(Node.js) にあたるウェブフレームワークです(少し語弊ありそう)。 エミュレータでは Flask を動かすためのアプリケーションサーバgunicorn というライブラリを利用しており、この gunicorn が曲者で、unix系OSでしか動かせないライブラリとして開発されていたのです。
なのでWindowsでは gunicorn を動かそうにも動かすことができず、連鎖して Flask が動かず、当然 functions-framework-python も動かないという構造になっていました。

なお、厳密にはpipインストールすらできずつまづく、というところでその先進めない状態になります。

# Windows機でpip installしようとすると...
pipenv install --dev
Installing dependencies from Pipfile.lock (12c19b)...
  ================================ 1/1 - 00:00:00
Ignoring gunicorn: markers 'platform_system != "Windows"' don't match your environment

Stackoverflowでも「そもそもgunicornはWindows用に作られていないよ」というやりとりが...w stackoverflow.com

回避策

さて、課題があっても解決しなければならない・解決するのがエンジニアリング。
採用したいかどうかは置いておいて、採れる回避策を検討しました。

1. テスト対象を絞る

そもそもローカルエミュレータを使いたい理由は主に以下の2点です。

  • ローカルで動作確認をとりたい(HTTPトリガーの関数の場合)
  • CI等で動作確認をとりたい

逆に言うと、ローカルで動作確認をとらないと決めたならエミュレータは不要です。特にPub/Sub経由のイベントトリガーの関数としてCloud Functionsを利用する場合、以下の理由でローカルでの動作確認がしづらかったりします。

  • 最初に実行するmain関数の部分で返り値が不要だったりするので、動作確認がしづらい
  • 同様の理由でユニットテストも書きづらい
  • Pub/Sub側のエミュレータを用意する必要がありかなり環境構築がつらくなる

上記理由からmain関数から呼ぶ独自の関数やクラスの振る舞いを単体テストする、などの対応で納めることが可能な作りならば、「エミュレータは不要」と判断することもできます。

2. コンテナで隠蔽してあげる

Pythonを動かす環境自体をDockerコンテナで隔離してあげれば、WindowsだろうがMacだろうがコンテナの中は同じ環境として扱えるのでこの問題は解消されます。
しかし、開発者にDockerに関する知識・Python特有の仮想環境の知識が求められるようになるため、「関数を書くことだけに集中」という状態はより作りづらくなるなぁという課題も生まれます。

3. 別の言語でCloud Functionsで動かす関数を実装しなおす

いやどうしてもローカルエミュレータで動作確認したいんだ!ということでしたら、やはりWindowsでも環境構築できる言語で再実装・再構築し直す必要があります。 Windowsでも相性がよさそうなのはJavaや.NETなんですかね。試してないのでわからないです。

さいごに

Cloud Functionsに限らずサーバレス関数な技術は使い勝手がよくてとても便利なのですが、テストを書いたり、開発者が同じ開発環境を整える手筈を組もうとすると結構大変です。無邪気にアプリケーションでの利用をするのはなるべく避けるのも運用観点では重要な判断軸なのかなと思ったり。

CloudSQLのメンテナンスに対する運用を考える

CloudSQLを使っていると逃げきれない仕様の一つに定期メンテナンスがあります。 数ヶ月に一度、最大で90秒ほどDBインスタンスが落ちる仕様なため、プロダクション環境などでCloudSQLを利用していると、必然と90秒のメンテナンスによるサービスダウンが発生しかねません。

頑張って高可用クラスタ冗長化)してたり、リードレプリカを作成しても、メンテナンス中はすべて停止してしまうため、サービスダウンは不可避です。

そんなわけでCloudSQLのこの仕様について頭を悩ませているインフラ運用者もきっと多いはず!
ということで2年くらいCloudSQLを運用してみえた運用方法の策定基準をまとめてみます。

前提

  • サーキットブレイカーの導入など、アプリケーションレイヤでのDBインスタンスの停止(GCPが一番推奨している対策方法)に対する処方は抜きとする
    • モノリスなのかマイクロサービス なのかで打ち手は変わってくるし、もちろんSLAにも関連してくるため、方針を決めるための考慮点が多いので今回はスコープアウト
  • アプリケーションに手を入れず、CloudSQLを運用するインフラエンジニアだけでなんとかしなきゃいけないケースを想定
  • 本番サービス運用を想定
  • 特にSLAを重要視して作った運用方針案

3通りの考えられる運用方針

1. 自動メンテナンスのなすがままパターン

方針
CCPが自動で決めたメンテナンスタイミングに従って自動メンテナンス作業をしてもらう方針です。インフラエンジニアのメンテナンス作業はほぼいらないので楽といえば楽ですが、メンテナンス作業時間幅を設定していないと、不意に作業が開始されて期待しないサービスダウンが起きてしまいかねません。

【この方針を採用できるサービス例】

  • いつサービスが落ちても重大な被害がない(SLAを設定していない場合など)
  • 数ヶ月に一度の90秒程度のサービスダウンなら許容できる
    • 日中は困るが、深夜帯ならサービスダウンが一瞬起きても許容できる場合も採用できる

【この方針をとるときに設定しておくべきこと】

  • MUST
    • 特になし
  • 推奨
    • GCPからメンテナンスについての事前通知を受け取る設定を行なう
    • メンテナンス時間幅の設定を行なう

2. 事前に手動でメンテナンス作業を実施するパターン

方針
CloudSQLでは1週間前にメンテナンスするーという旨の通知をもらうことができます(設定してないと通知はこない)。通知を受け取ったら1週間以内にメンテナンス作業を手動で実施することで、意図しないサービスダウンを防ぐことができます。ただしサービスダウン自体は発生するため、あくまで「サービスダウンが起きる時間を固定し、ステークホルダにその旨を知らせることができる」という利点があるのみです。
なお事前通知を受けてから1週間後に自動メンテナンスが走りますが、通知を受けた際に「延期」することも可能です。最大でさらに1週間ほど実施タイミングを伸ばせるので、ステークホルダへの合意・共有に時間を要する組織であればこの設定もしておくとよいです。

【この方針を採用できるサービス例】

  • SLAが定義されており、サービスをダウンさせる場合はステークホルダへの合意が必要なケース
    • 調整にかけられる時間は最大で2週間なので、それ以上に時間が必要な重厚長大な組織の場合は当てはまりません
  • 1〜2週間程度の間でメンテナンスのためにインフラチームの工数を確保できる柔軟なチームの場合

【この方針をとるときに設定しておくべきこと】

  • MUST
    • GCPからメンテナンスについての事前通知を受け取る設定を行なう
  • 推奨
    • メンテナンス作業の延期申請
    • メンテナンス時間幅の設定を行なう

3. 繁忙期だけサービスダウンを拒否し、それ以外の期間では2.で運用するパターン

方針
CloudSQLではメンテナンス作業を拒否することができます。最大で拒否できる期間は90日で、メンテナンス作業時間が設定されていてもこの設定がある場合はメンテナンスが行なわれません。しかしあくまでサービス特性上、繁忙期は1秒でもサービスを落としたくないといったニーズへの対応であり、メンテ工数・メンテナンスによって生じるサービスダウンタイムを増やしたくないという理由で拒否をするは避けた方がよいと個人的には思っています。繁忙期以外の期間では、2.で提案した運用に切り替えます。
注意点としては、年に1度しかこの拒否設定はできないため、年に複数回のキャンペーンを行ない、そのキャンペーン中はサービスを落としたくない、といったニーズには答えられません。
こうなってくるとアプリケーションレイヤに手を入れなければなりませんし、マイクロサービス 前提のアーキテクチャになっていないと実現は難しいです。

【この方針を採用できるサービス例】

  • 季節要因により負荷が高まるサービス
  • キャンペーンを行なう予定のあるサービス

【この方針をとるときに設定しておくべきこと】

  • MUST
    • GCPからメンテナンスについての事前通知を受け取る設定を行なう
    • メンテナンス拒否設定を行なう
  • 推奨
    • メンテナンス時間幅の設定を行なう

所感

すべて泥臭い運用方針でイケてる提案ではないですが、現在のCloudSQLの仕様上、しょうがないのかな?とも思っています。
せめて高可用クラスタやリードレプリカはローリングアップデート方式でメンテナンスしてくれよという気持ちでいますが、実現するでしょうか...

他、「うちはこうしてるよー」というアイデアや、「もっといい方法あるやん」というツッコミ、まってます。

複数コンテナを有するPodのHPA設定の挙動を深ぼる

HPAの設定ではDeployment単位で閾値を指定しているのに、扱うリソースはPodなので少しわかりづらいとおもったので、複数コンテナを有するPodに対してHPA設定をかけた場合の計算ロジック(詳細版)をまとめました。

HPAのreplicaset数算出方法を整理

公式を眺める

複数コンテナを持つPodだと、「Podが利用しているリソース」はどう計算されるでしょうか。
以下、メモリについて考えてみます。
公式を参考にすると、以下のような計算式になる。

HPAの計算方法
HPAの計算方法

数式をみるとウッ頭痛が...!となる人向けに少し解説します。
どちらもシンプルです。

podUsage でやっていることは、Pod内のコンテナが要求しているメモリ量の合計値と、同じくPod内コンテナが使用しているメモリ量の合計値を割ることでPod単位の使用率を求めているだけです。

scalledReplicasets は理想のreplicaset数を意味します。 計算方法は、HPAリソースで指定した使用率(averageUtilization)を分母にし、replicaset分の podUsage の総和を分子とした式を切り上げ(ceil関数)する、といったものになります。

具体例で考える

僕は式を出されても理解できないタイプなので、実際のケースに当てはめて考えてみたいと思います。
まず、

  • アプリケーションPod
    • Javaアプリケーションが載っているコンテナ(要求したメモリ量は 512Mi・使用中のメモリ量は360Mi)
    • cloudproxyコンテナ(要求したメモリ量は32Mi・使用中のメモリ量は12Mi)

を考えてみます。
このPodの合計要求メモリ量は 512 + 36= 548Mi、使用中の合計メモリ量は 360 + 12 = 372Miとなり、使用率に変換すると 372÷548 = 67%となります。( PodUsage )。

現在のreplicaset数は2とし、どのPodも同じメモリ使用率とします。
さらにHPAで設定した期待するメモリ使用率は 60%( averageUtilization )とします。
まず分子の計算をします。replicaset数分のPodのメモリ使用率を足していくと 134%となります。
最後に 134 ÷ 60 = 2.23。小数点切り上げをするので 3つのreplicasetが必要と算出されます。

本来はPodごとに使用率は変わってきますし、メモリ以外にもCPU使用率やカスタムメトリックスなどを使った複合的なスケール基準値を設定していると多少なり複雑になってくるはずです。

Pod内で複数コンテナを運用している場合のHPAの設定方法

要約すると「複数コンテナを有するPodに対してHPA設定をかけるなら、コンテナすべてにlimits設定をかけておきましょう。でなければ正常に機能しません」という些末な話になります。

課題

HPA設定を付与したリソースを確認しようと思いGKEのコンソール画面を覗くと、

HPA cannot read metrics value

どうやらHPAの設定がうまくいっていない様子。

HPAの設定がうまくいっていないケース
HPAの設定がうまくいっていないケース
なぜだろうと思い、クラスタの状態を確認するも、似たような症状になっていて訳が分からなかった。
というのも、他のPodに付与しているHPA設定も、今回エラーを発見したものと同様の設定をかけていたので条件は同じはず…とずっと思っていたからです。

原因と対処

そこでGCPサポートに相談したところ、実は今回エラーになっていたHPA設定の対象となっていたPodはサイドカーを持っており、そのサイドカーのlimits設定がなされていなかったからでした。下記がサイドカーのlimits制限がされてない状態のときのmanifestです。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hoge
  namespace: hoge
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hoge
      tier: backend
  template:
    metadata:
      labels:
        app: hoge
        tier: backend
    spec:
      containers:
      - env:
        - name: SPRING_PROFILES_ACTIVE
          value: prd
        image: 省略
        imagePullPolicy: Always
        livenessProbe:
          httpGet:
            path: /private/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 30
        name: hoge
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /private/health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
        resources:
          limits:
            cpu: 500m
            memory: 768Mi
          requests:
            cpu: 100m
            memory: 512Mi

   # サイドカー
      - name: cloudsql-proxy
        command: 省略
        image: gcr.io/cloudsql-docker/gce-proxy:1.17
        name: cloudsql-proxy
        volumeMounts:
        - mountPath: /secrets/cloudsql
          name: cloudsql-instance-credentials
          readOnly: true
      volumes:
      - name: cloudsql-instance-credentials
        secret:
          secretName: cloudsql-instance-credentials

メインコンテナの hoge の設定があれば大丈夫だと思っていたのが勘違いのもとでした。
Limitsを付与して実行するとうまくメトリクスを取得できていたので、課題は解決しました。

...
- name: cloudsql-proxy
  command: 省略
  image: gcr.io/cloudsql-docker/gce-proxy:1.17
  resources:
    limits:
      memory: 64Mi
    requests:
      memory: 32Mi
...

正常なHPA設定時のマネージドコンソール上の表示
正常なHPA設定時のマネージドコンソール上の表示

今回はメモリに対してオートスケール設定を付与するケースだったので、メモリだけlimits制限をかけています。

DeepL opener PROのおかげで勉強効率が上がった話

先日、DeepL opener PROというchrome拡張機能が公開されたので、さっそく使ってみました。
この記事はこの拡張プラグインを使ってから1週間経った時点での感想記事になります。

DeepL opener PROとは何か

個人で契約したDeepL APIAPIキーを入力すれば使える拡張プラグインで、選択した範囲だけ即時翻訳してくれるツールです。
これまで類似したプラグインは出ていましたが、DeepLの翻訳ページに飛ばすものが多く、体験があまりよくありませんでした。
これはそのページで即座に翻訳結果がみれるのが嬉しいところです。

https://github.com/T3aHat/DeepLopener_PRO/raw/main/gif/text-oriented.gif

作者の記事から引用

嬉しかったこと

総合するとリーチできるドキュメントが増えたにつながります。細かく分解すると以下のとおり。

1. 英文を読むときの心理的コストが減った

最近ではOSSのドキュメントも有名なものだと日本語翻訳されたものが多いですが、それでもまだ英文だけのやつも多いです。構文や使われている単語は大学受験で英語をしっかりやってきたらならそこまで難しくないものですが、いかんせん疲れる。気合を入れて読むことが多い自分にとってはDeepL opener PROはかなりのサポーター君になっていて、「英文読むの疲れたら翻訳しちゃえばいいや」と思える前提をつくれたのは大きく、気兼ねなく英文記事を読む体制が作れたので、地味に嬉しいです。

2. Oreilly Safaribooksを読む速度が上がった

おそらくOreilly側が対策をしているのだろうけども、選択範囲を指定して一部翻訳するといった機能は使えないです。
ただ、ページ全体翻訳機能を使うことはでき、その場合は開いている章単位での翻訳になります。一度の翻訳で150〜200円程度かかりますが、章ごとに読むタイプにとっては1冊を読み切るためにちょうど都合がよく、わりと便利だったりしています。 インフラ系の書籍はバンバン出てるので、積読が増えていましたが、これでインプット速度をあげることができたので今後はもっと読んでいこうと思っています。

つらいこと

同じ箇所を翻訳すると再度課金対象なの?

はい。翻訳したページをリロードすると元に戻り、同じ場所をもう一度翻訳すると翻訳した文字数が加算されます。
なので、大量の文章(あるいはページ)を翻訳した場合はリロードには気をつけてください。

最低でも3130円かかる

基本料金が630円かかります。
さらに100万字ごとに2500円かかります。
なので、3130円は最低でも支払う必要があります。

例えばさきほどのOreillyの章単位での翻訳で200円かかった場合、残り2300円分の翻訳字数が残ることになります。無邪気に翻訳しまくっているとすぐに1万円くらい使っちゃいそうですね。

一応、DeepL API側の機能として、文字数制限をかけることができます。
100万字を超えたら使えなくする、通知を飛ばすといったことが可能なので、超過しそうになったら気付けるのでありがたいです。

kubevalでmanifestのフォーマットをチェック

YAMLベースのドキュメントのコードレビューって、フォーマットのズレや期待していない値かどうかの確認で不備を見逃してしまい、あとあとリリース時の kubectl apply -f manifest.yaml などでうまく実行できずに焦ることが経験上、多いです。

今回はコードレビューをする前に機械的にmanifestをバリデーションしてくれるツールである kubeval について、Macへの導入を試します。

kubevalをインストール

kubeval とはkubectlを使わずにmanifestファイルをバリデーション(規格にあった構成かどうかをチェック)するツールです。Linuxをはじめ、MacWindows用にもパッケージが配布されており、ローカル開発環境などで利用されることが多いツールです。

公式ドキュメント(英語)

今回はMacOSにインストールしていきます。

wget https://github.com/instrumenta/kubeval/releases/latest/download/kubeval-darwin-amd64.tar.gz
tar xf kubeval-darwin-amd64.tar.gz
sudo cp kubeval /usr/local/bin

インストールが済んだら

kubeval --version

Version: 0.15.0
Commit: df50ea7fd4fd202458002a40a6a39ffbb3125bad
Date: 2020-04-14T09:32:50Z

と、バージョンを確認し、うまく起動できたことを確認。

kubevalを使ってみる

試しにサンプルコード(serviceとdeployment)のバリデーションをしてみましょう。 検査対象はこちら。

# manifest.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: adservice
spec:
  selector:
    matchLabels:
      app: adservice
  template:
    metadata:
      labels:
        app: adservice
    spec:
      serviceAccountName: default
      terminationGracePeriodSeconds: 5
      containers:
      - name: server
        image: adservice
        ports:
        - containerPort: 9555
        env:
        - name: PORT
          value: "9555"
        # - name: DISABLE_STATS
        #   value: "1"
        # - name: DISABLE_TRACING
        #   value: "1"
        #- name: JAEGER_SERVICE_ADDR
        #  value: "jaeger-collector:14268"
        resources:
          requests:
            cpu: 200m
            memory: 180Mi
          limits:
            cpu: 300m
            memory: 300Mi
        readinessProbe:
          initialDelaySeconds: 20
          periodSeconds: 15
          exec:
            command: ["/bin/grpc_health_probe", "-addr=:9555"]
        livenessProbe:
          initialDelaySeconds: 20
          periodSeconds: 15
          exec:
            command: ["/bin/grpc_health_probe", "-addr=:9555"]
---
apiVersion: v1
kind: Service
metadata:
  name: adservice
spec:
  type: ClusterIP
  selector:
    app: adservice
  ports:
  - name: grpc
    port: 9555
    targetPort: 9555
kubeval manifest.yaml
PASS - adservice.yaml contains a valid Deployment (adservice)
PASS - adservice.yaml contains a valid Service (adservice)

どちらのkubernetesリソースも PASS となっています。これは問題なしという意味の PASS です。問題がある場合は WARN ERR が表示されます。 試しに以下の間違いを混入させて実行してみます。

# manifest.yaml

...
apiVersion: v1
  kind: Service          ## インデントズレ
metadata:
  name: adservice
spec:
  type: ClusterIP
  selector:
    app: adservice
  ports:
  - name: grpc
    port: 9555
    targetPort: 9555

実行してみます。

kubeval manifest.yaml

ERR  - Failed to decode YAML from adservice.yaml: error converting YAML to JSON: yaml: line 2: mapping values are not allowed in this context

2行目が何かおかしいぞ、とエラーを出してくれています。 kubeval でコードをリポジトリにあげるまえにローカルで確認しておくとmanifestの品質を一定まで維持できるので、使っていきたいやつです。

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をサービス公開する手順として覚えておくとよさそうです。