難しいのは付ける瞬間ではなく、付けた後の暮らしだ
一言でいうと
注入されたプロキシのリソース・起動順序・終了方法は、Podのアノテーションいくつかで決まります。そのアノテーションが実際にどのフィールドになるかは、クラスターに載せる前にistioctl kube-injectで確認できます。
なぜ付けたあとのほうが難しいのか
サイドカーの注入は、学ぶのが簡単です。ネームスペースにラベルを付けると、Podごとにコンテナが1つ増えます。問題はその先です。プロキシはタダではなく、アプリケーションと一緒に起動して一緒に終了しなければならず、ワークロードによっては邪魔になります。
まずリソースから見ていきましょう。デフォルトのプロキシは、CPU 100m、メモリ128Miをリクエストし、上限はCPU 2コア、メモリ1Giです。Pod1つで見れば小さいですが、Podが2,000個なら、リクエストだけでCPU 200コアを確保します。逆に、リクエストを下げすぎると、トラフィックが集中したときにプロキシのほうが先に遅くなり、アプリケーションのレイテンシに見える障害になります。このつまみは、ワークロードごとに違うべきなので、Podのアノテーションで調整します。
2つ目は起動順序です。コンテナは原則として同時に起動します。アプリケーションが速くてプロキシが遅いと、アプリケーションの最初の数秒間に出ていくリクエストが、プロキシなしでiptablesルールにだけかかって、そのまま失敗します。デプロイ直後にだけ現れる5xxが、この形です。holdApplicationUntilProxyStartsを有効にすると、インジェクターがプロキシをコンテナリストの先頭に置いて、postStartフックを付け、プロキシが準備できるまで次のコンテナの起動を待たせます。
3つ目が、最も古い落とし穴です。JobのPodにプロキシが通常のコンテナとして入ると、本体の作業が終わっても、プロキシは生き続けます。Podは完了に移れず、バッチパイプラインがそこで止まります。以前は、作業スクリプトの最後にプロキシの終了エンドポイントを呼び出す方法で回避していました。作業が失敗するとその行が実行されないという欠陥を抱えたままです。今は、Kubernetesが再起動ポリシーを持つ初期化コンテナをサポートしているので、プロキシをそちらに移します。先に起動し、Podが生きている間は生き続け、本体が終わると一緒に片付けられます。
アノテーションがどのフィールドになるのか
| アノテーション | 注入結果で変わる場所 |
|---|---|
sidecar.istio.io/proxyCPU、proxyMemory |
istio-proxyのresources.requests |
sidecar.istio.io/proxyCPULimit、proxyMemoryLimit |
istio-proxyのresources.limits |
proxy.istio.io/config |
istio-proxyの環境変数PROXY_CONFIG(JSONに変換される) |
proxy.istio.io/configのholdApplicationUntilProxyStarts |
コンテナの順序 + lifecycle.postStartフック |
traffic.sidecar.istio.io/excludeOutboundPorts |
istio-initのコマンドライン引数 |
sidecar.istio.io/inject: "false" |
プロキシと初期化コンテナがまったく入らない |
sidecar.istio.io/nativeSidecar: "true" |
プロキシがinitContainersに移り、restartPolicy: Alwaysを受け取る |
ここで重要な性質が1つあります。proxy.istio.io/configは、値が文字列で、その中にYAMLを入れます。人が書く形式と、プロキシが読む形式が違うということなので、打ち間違いがあっても、マニフェストの文法は通ります。結果の環境変数を直接開いて見る習慣が必要な理由です。
アノテーションを付ける場所もよく間違えます。注入はPodを対象にするので、アノテーションはPodテンプレートに付ける必要があります。Deploymentのメタデータに付けると、文法は合っていても、何も起こりません。
現場での姿
最もよく見るのは、「デプロイした直後の数秒間だけエラーが出る」という報告です。ログには接続拒否だけが残り、再現できません。起動順序の保証を有効にすると、なくなります。この設定がデフォルトではない理由は、Podの起動がそれだけ遅くなるからなので、どこで有効にするかは、サービスの性格によって決めます。
2つ目は、「夜間バッチが昨日から終わらない」というものです。PodはRunningで、アプリケーションのログは正常終了を出力しています。プロキシだけが生きています。こうしたPodが積み重なると、バッチキューが詰まり、原因がわからなければ、人がPodを手で削除する運用が定着してしまいます。
3つ目は、データベース接続です。プロキシがプロトコルを誤認すると(ポート名がなかったり、ルールに合っていなかったりすると)、接続が切れたり、おかしなほど遅くなったりします。そういうときに臨時で使うつまみが、出ていくポートの除外です。根本的な解決は、ポート名をプロトコルに合わせて付けることですが、障害の最中は、このアノテーション1つが時間を稼いでくれます。
このラボ環境の限界
ラボのPodでは、本物のプロキシを起動できません。そのため、起動順序の保証が実際に最初のリクエストを救う場面、JobのPodが完了に移れない場面、プロキシがメモリをどれだけ使うかは、見られません。このラボが扱うのは、宣言がどんなPod仕様になるかです。ありがたいことに、この段階で捕まえられるミスが、現場の事故の大きな部分を占めます。アノテーションを見当違いの場所に付けた、値の形式が間違っていた、有効にしたつもりのものが有効になっていなかった、というケースです。
次のラボですること
アノテーションが1つもないデフォルトの注入結果を、まず読んで、アノテーションを1つずつ加えながら、結果のマニフェストのどのフィールドが変わるかを確認します。リソース、プロキシ設定、起動順序の保証、出ていくポートの除外、注入の除外を順に見て、Jobを2つの方式で注入して、プロキシがどちらのリストに入るかを比較します。最後に、8枚を一度にもう一度注入して、結果を表にまとめるスクリプトを作ります。