Envoy Proxyを使ったファイルベースの動的コンフィグレーションとk8sのライフサイクル

みらい翻訳のカレンダー | Advent Calendar 2022 - Qiita に投稿した2つの記事 (「Envoy Proxyを使ったファイルベースの動的コンフィグレーション」、「Envoy Proxyを使ったファイルベースの動的コンフィグレーション (EKS)」) では、いずれも起動前に Envoy Proxy の構成ファイルをコピーし、また複数の Pod 間で共有するイメージとしていました。

これら 2 つの記事で、Kubernetes の hostPath と Persistent Volumes (Amazon Elastic Block Store (EBS)) の使い方を理解することができます。

しかし、起動時の初期構成ファイルをあらかじめストレージにコピーすることは、運用が煩雑になる可能性があります。

また、これら 2 つの記事では Envoy Proxy のコンテナイメージに shell や mv コマンド等を含むことを想定していました。これはコンテナサイズとセキュリティの両面から問題があります。

この記事では、コンテナのライフサイクルを解説して、課題の解決方法を示します。

postStart と preStop

Kubernetes では、コンテナの起動後と停止前に任意のコマンドの実行が可能です。これを利用することで、Envoy Proxy の構成ファイルを ConfigMap や Secrets からコピーできます。

またこの場合、Envoy Proxy のコンテナイメージに shell や cp コマンド等を含んでいることが前提となります。これは課題であるコンテナサイズの縮小やセキュリティ向上に寄与しないということになります。

さらに、postStart に記述されたコマンドはコンテナの起動と同時並行で実行されます。試してみると、Envoy Proxy の方が構成ファイルのコピーより早く起動して、そのため構成ファイルが見つからず、コンテナの起動に失敗しました。 つまり、この方法は Envoy Proxy の場合には利用できませんでした。

このトピックの詳細については、Kubernetes のドキュメント「コンテナライフサイクルイベントへのハンドラー紐付け」を参照してください。

Init Containers

Kubernetes には、Pod 内で主となるコンテナを起動する前に別のコンテナを順番に起動する「Init コンテナ」機能があります。

initContainers に定義されたコンテナは上から順番に実行され、それぞれのコマンドが完了してから次のコンテナが実行され、initContainers 全てのコマンド完了後、主となるコンテナを起動します。

これを利用することで、この記事の例にある Envoy Proxy や Demo アプリケーションの起動前に必要な構成ファイルを ConfigMap や Secrets からターゲットファイルにコピーできます。

Init コンテナを使用する利点は、Kubernetes のドキュメントで説明されている通りで、主となるコンテナイメージにセットアップ用のツール等を含める必要がなくなり、コンテナサイズの縮小とセキュリティ向上に寄与します。

2 つの記事の説明で残されていた課題の解決策として活用できます。

サンプルコード

サンプルコードは、これまでと同じ GitHub リポジトリ にあります。

emptyDir

今回、構成ファイルをコピーするボリュームに emptyDir (Volumes) を使用します。emptyDir は Pod のノード割り当て時に最初に空で作成され、Pod が実行されている間存在し続けます。

initContainers のコンテナコマンドで ConfigMap に定義された動的コンフィグレーションを emptyDir ボリュームにコピーします。

これまでの記事では永続ボリュームに動的コンフィグレーションファイルをコピーしていました。このため、動的コンフィグレーションが変更された後 Pod を再起動すると変更後のルーティングで起動されました。

これ自体は問題ではありませんが、ローリングアップデートやカナリアの場合には全ての Pod の設定を同時に変更せず、徐々に変更を反映して、場合によっては変更をキャンセルします。

残念ながら永続ボリュームを使用する場合、このような制御はより複雑な運用が必要になってしまいます。

この記事で説明する方法であれば、Pod を再起動すると、ConfigMap から emptyDir に起動時にコピーするため、ConfigMap の構成で再起動されることが保証されます。

同じ Pod を 3 つにスケーリングしている場合、つまり replicas が 3 の場合の動的コンフィグレーションのローリングアップデートを考えてみます。

  1. Pod-1 の emptyDir に構成ファイルを mv して、動的コンフィグレーションを変更
  2. Pod-1 の変更が正常であることを確認
  3. Pod-2 の emptyDir に構成ファイルを mv して、動的コンフィグレーションを変更
  4. Pod-2 の変更が正常であることを確認
  5. Pod-3 の emptyDir に構成ファイルを mv して、動的コンフィグレーションを変更
  6. Pod-3 の変更が正常であることを確認
  7. 動的コンフィグレーションを定義した ConfigMap を更新

もし、途中で不具合が判明した場合は、7 の前であれば変更に失敗した Pod を再起動するだけで前の状態に戻せます。

実行環境

emptyDir はローカル環境の KubernetesAmazon Elastic Kubernetes Service (EKS) もどちらも同様に利用することができます。

EKS を利用する場合、前回の記事同様 spring-boot/eks/terraform ディレクトリで次のコマンドを実行して EKS クラスタを構築します。

terraform init
terraform plan
terraform apply

Helm チャート

Helm チャートは spring-boot/demo2 ディレクトリにあります。

Helm チャートはローカル環境の Kubernetes を使用する場合は次のコマンドでインストールします。

helm install us ./demo2
helm install jp --values=jp-values.yaml ./demo2

EKS の場合は次のコマンドでインストールします。

helm install us --values=eks-demo2-us-values.yaml ./demo2
helm install jp --values=eks-demo2-jp-values.yaml ./demo2

インストール結果は次のコマンドで確認できます。

kubectl get configMap,pod,svc

この記事のキーとなる deployment.yaml の initContainers の設定は次の通りです。

      initContainers:
        - name: init
          image: busybox
          command: ["sh", "-c", "(cp /config/cds/cds.yaml /config/dynamic/cds.yaml; cp /config/lds/lds.yaml /config/dynamic/lds.yaml)"]
          volumeMounts:
            - mountPath: /config/dynamic
              name: dynamic-config
            - mountPath: /config/cds
              name: cds
            - mountPath: /config/lds
              name: lds

Volumes の設定は次の通りです。

      volumes:
        - name: dynamic-config
          emptyDir: {}
        - name: config
          configMap:
            name: {{ include "demo2.fullname" . }}-config
        - name: cds
          configMap:
            name: {{ include "demo2.fullname" . }}-cds
        - name: lds
          configMap:
            name: {{ include "demo2.fullname" . }}-lds

この設定により、Initコンテナの BusyBox は次のようにボリュームがマウントされます。

/config     - dynamic-config           (emptyDir)
/config/cds - {{ demo2.fullname }}-cds (ConfigMap)
/config/lds - {{ demo2.fullname }}-lds (ConfigMap)

そして、cds と lds の ConfigMap は、command に書かれた cp コマンドにより dynamic-config の emptyDir ボリュームにコピーされます。

そして、Envoy Proxy のコンテナの設定は次の通りです。

        - name: {{ .Values.envoyimage.name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: {{ .Values.envoyimage.repository }}:{{ .Values.envoyimage.tag }}
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
            - name: admin
              containerPort: 9901
              protocol: TCP
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          volumeMounts:
            - readOnly: true
              mountPath: /config
              name: config
            - mountPath: /config/dynamic
              name: dynamic-config
          args:
            - "-c"
            - "/config/front-envoy.yaml"

動的コンフィグレーションの変更

EKS を使用している場合は、サービスの EXTERNAL-IP を使ってアクセスしてください。ここではローカル環境の場合の説明をします。

service/us-demo2service/jp-demo2 それぞれのサービスに外部から接続できるように port-forward を実行します。

kubectl port-forward svc/us-demo2 8080:80
kubectl port-forward svc/jp-demo2 8081:80
curl http://localhost:8080
Hello World

curl http://localhost:8080/jp
{"timestamp":"2022-12-28T06:26:45.295+00:00","path":"/jp","status":404,"error":"Not Found","message":null,"requestId":"7c8f19c4-2"}

curl http://localhost:8081
Hello Japan

では、us-demo2 の Envoy Proxy の動的コンフィグレーションを変更します。

注意: この段階ではまだ、Envoy Proxy に shell や mv コマンドが存在していることを想定しています。プロダクション環境でこのような処理を実行するためには、REST API でこの一連の処理をするコンテナを作成して、Pod に追加することをおすすめします。

まず、Pod 名を確認します。

kubectl get pod | grep us-demo2
us-demo2-598f8c6fbc-wj29t   2/2     Running   0          17m

POD_NAME 環境変数に Pod 名を設定します。

export POD_NAME=us-demo2-598f8c6fbc-wj29t

Envoy Proxy のクラスタ設定 (cds.yaml) を更新します。

kubectl cp config/cds/demo2-cds2.yaml $POD_NAME:/config/dynamic/cds2.yaml -c envoy
kubectl exec $POD_NAME -c envoy -- mv -f /config/dynamic/cds2.yaml /config/dynamic/cds.yaml

次にリスナー設定 (lds.yaml) を更新します。

kubectl cp config/lds/demo2-lds2.yaml $POD_NAME:/config/dynamic/lds2.yaml -c envoy
kubectl exec $POD_NAME -c envoy -- mv -f /config/dynamic/lds2.yaml /config/dynamic/lds.yaml

ルーティングの変更を確認します。

curl http://localhost:8080
Hello World

curl http://localhost:8080/jp
Hello Japan

まとめ

Initコンテナによる初期化、emptyDir ボリュームを利用する方法が前回までの記事による方法より運用、セキュリティ上も利点が多くなります。

ファイルベースの動的コンフィグレーションを採用する場合は、この記事の方法を参考にしてください。

参考