コンポーネントを一つずつ止めたら、止まったものと持ちこたえたもの
目標
本物のCilium 1.20.1で、cilium agent・operator・cilium-envoy・hubble-relayがそれぞれ何を担当しているのかを、1つずつ停止して確認します。まず、cluster-pool IPAMとCiliumIdentityがどこに記録されるのかを読み、コンポーネントがない間に新しいPod・L7リクエスト・フロー照会・既存のトラフィックがどうなるのかを記録してから、元に戻します。
なぜ重要なのか
「Ciliumが落ちた」という報告は、どのコンポーネントが止まったのかによってまったく別の障害になります。operatorはクラスターで1回行えばよい仕事を担当するので、しばらくなくても転送やポリシーの判定には関わりません。envoyがなければ、L7ルールがかかった経路だけが止まります。relayがなければ、複数のノードを1か所から照会する経路だけが切れます。agentがなければ、すでにロードされたデータパスは動き続けますが、新しいPodはネットワークを受け取れないため、スケジュールが止まります。
コンポーネントを停止するステップでは、観測を記録したあと必ず元に戻します(agentだけはステップ6で停止し、ステップ7で元に戻します)。復旧後の全体採点は、そのときの記録を、Podのuid・アイデンティティ・フローの時刻・復旧したPodの作成時刻と突き合わせるので、記録は必ず停止している間に作ってください。
環境の準備に約5分かかります。ステップ6とステップ7の間はagentがないため、agentのコマンドは動作しません。セッションが終わると、/root/cca-partsのファイルは消えます。
ステップ
kubectl apply -f /opt/fixtures/cca-components/workload.yamlで、cca-partsにapp(レプリカ2)・api-l7・clientを起動してください。/root/cca-parts/ipam.jsonに記録します。記録する項目は、ipam_mode・cluster_pool・mask_size(cilium-configのipam、cluster-pool-ipv4-cidr、cluster-pool-ipv4-mask-sizeで、mask_sizeは数値)、node_pod_cidrs(CiliumNodeのspec.ipam.podCIDRsのリスト)、status_pool・capacity(agentのcilium-dbg statusのIPAMの行で、allocated fromの後ろの範囲と、/の後ろの数値)、router_ip(VMのcilium_hostデバイスのIPv4)、pods(cca-partsのPod 4つの名前からIPへの辞書)です。- appのレプリカ2つのPodとclientのCiliumEndpointのアイデンティティ番号を比較し、appのCiliumIdentityオブジェクトを読んでください。
/root/cca-parts/identity.jsonに、app_identity、app_endpoints(appのPod名2つ)、client_identity、security_labels(そのCiliumIdentityのsecurity-labelsを키=값形式の文字列(プレースホルダーはキーと値です)にして並べ替えたリスト)、allocation_mode(cilium-configのidentity-allocation-mode)を記録します。 - cilium-operatorをreplicas 0に下げ、operatorのPodがすべて消えるまで待ってください。その状態で、cca-partsにPod
born-no-operator(イメージはclientと同じcurl、ラベルはborn=no-operator、コマンドはsleep 86400)を作成し、Readyになったら/root/cca-parts/operator-down.jsonに、operator_replicas(そのときのDeploymentのspec.replicas、数値)、pod_uid、pod_ip、identity(そのPodのCiliumEndpointのアイデンティティ)を記録します。記録したら、operatorを1に戻し、availableになるまで待ちます。 - cca-partsにCiliumNetworkPolicy
l7-getを作成してください。endpointSelectorはapp=api-l7、ingressは1項目で、fromEndpointsはapp=client、toPortsにはTCPの"8080"とrules.http[{method: GET, path: /allowed}]を指定します。clientからapi-l7の/allowedが200、/secretが403になることを確認したあと、kube-systemのcilium-envoy DaemonSetに、存在しないnodeSelectorcca-lab/envoy: "off"を追加して、envoyのPodを取り除きます。その間にclientからapi-l7の/allowed(5秒の制限付き)とappのService(http://app:8080/)にリクエストを送り、agent内のhubbleでclient→api-l7のフローを読み取って、/root/cca-parts/envoy-down.jsonに、l7_code、plain_code(HTTPコードの文字列で、応答がない場合は"000")、flows(client→api-l7のフローのcompact形式の行のリスト)を記録します。そのあと、そのnodeSelectorのキーを削除してenvoyを元に戻し、/allowedが再び200になるまで待ちます。 - kube-systemのhubble-relayをreplicas 0に下げてください。VMのシェルで
hubble status --server <hubble-relay 서비스 ClusterIP>:80(プレースホルダーはhubble-relayのServiceのClusterIPです)が失敗するときのエラーを1行、agentの中でhubble observe --last 5 -o compactのフローの行とhubble statusのCurrent/Max Flowsの行を読み取って、/root/cca-parts/relay-down.jsonに、relay_error、local_flows(リスト)、local_statusとして記録します。そのあと、relayを1に戻し、relay経由のstatusが成功するまで待ちます。 - kube-systemのcilium DaemonSetにnodeSelector
cca-lab/agent: "off"を追加して、agentのPodを取り除いてください(このステップでは元に戻しません)。Podが消えたらノードのtaintを確認し、clientからappのServiceにリクエストを送ったあと、cca-partsにcurlのPodorphan(コマンドはsleep 86400)を作成して、FailedSchedulingイベントを待ちます。/root/cca-parts/agent-down.jsonに、existing_code(client→appのコードの文字列)、taint(ノードにかかったciliumのtaintのkey:effect)、orphan_uid、orphan_phase、reason・message(orphanのFailedSchedulingイベント)を記録します。 - cilium DaemonSetからnodeSelectorのキー
cca-lab/agentを削除して、agentを元に戻してください。agentがReadyになり、ノードのagent-not-ready taintが消え、orphanがスケジュール(PodScheduled=True)されるまでポーリングしたあと、/root/cca-parts/restore.jsonに、taint_removed(true/false)、orphan_scheduled_at(orphanのPodScheduled conditionのlastTransitionTime)、agent_pod(新しいagent Podの名前)を記録します。 /root/cca-parts/report.txtに、키=값形式の行(プレースホルダーはキーと値です)を8行書きます。項目は、ipam_mode、node_pod_cidr、identity_allocation_mode、without_operator_new_pod(born-no-operatorの現在のphase)、without_envoy_l7・without_envoy_plain(envoy-down.jsonの2つのコード)、without_agent_existing・without_agent_new_pod(agent-down.jsonのexisting_codeとorphan_phase)です。値は、記録ファイルおよび現在の設定と一致している必要があります。
参考
- コンポーネントの概要(agent・operator・Hubble relay): https://docs.cilium.io/en/v1.20/overview/component-overview/
- Cilium Operatorの役割と、ないときに起きること: https://docs.cilium.io/en/v1.20/internals/cilium_operator/
- Cluster Scope IPAM(cluster-pool): https://docs.cilium.io/en/v1.20/network/concepts/ipam/cluster-pool/
- Envoy DaemonSetモード: https://docs.cilium.io/en/v1.20/security/network/proxy/envoy/
- ノードのtaintと管理されていないPod: https://docs.cilium.io/en/v1.20/installation/taints/
- agentのコマンド:
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg status/hubble observe
Podのアドレスはどこから切り出されたものか
kubectl apply -f /opt/fixtures/cca-components/workload.yamlで、cca-partsにapp(レプリカ2)・api-l7・clientを起動してください。/root/cca-parts/ipam.jsonに記録します。記録する項目は、ipam_mode・cluster_pool・mask_size(cilium-configのipam、cluster-pool-ipv4-cidr、cluster-pool-ipv4-mask-sizeで、mask_sizeは数値)、node_pod_cidrs(CiliumNodeのspec.ipam.podCIDRsのリスト)、status_pool・capacity(agentのcilium-dbg statusのIPAMの行で、allocated fromの後ろの範囲と、/の後ろの数値)、router_ip(VMのcilium_hostデバイスのIPv4)、pods(cca-partsのPod 4つの名前からIPへの辞書)です。
cluster-poolモードでは、クラスター全体のプールをノードごとに一定の大きさに切り分けてCiliumNodeに記録しておき、各ノードのagentがその断片の中からPodのアドレスを割り当てます。プール・断片・実際のアドレスが互いに包含関係にあるかを確認してください。ノードのゲートウェイの役割を果たすcilium_hostも、同じ断片からアドレスを受け取ります。
2つのレプリカは1つのアイデンティティを共有する
appのレプリカ2つのPodとclientのCiliumEndpointのアイデンティティ番号を比較し、appのCiliumIdentityオブジェクトを読んでください。/root/cca-parts/identity.jsonに、app_identity、app_endpoints(appのPod名2つ)、client_identity、security_labels(そのCiliumIdentityのsecurity-labelsを키=값形式の文字列(プレースホルダーはキーと値です)にして並べ替えたリスト)、allocation_mode(cilium-configのidentity-allocation-mode)を記録します。
アイデンティティはPodごとではなく、セキュリティ関連のラベルの集合ごとに1つです。crd割り当て方式では、その集合がCiliumIdentityというクラスタースコープのオブジェクトとして保存され、名前がそのまま番号になります。Pod名に付くハッシュのような値は、セキュリティラベルに含まれない点を確認してみてください。
operatorがない間に生まれたPod
cilium-operatorをreplicas 0に下げ、operatorのPodがすべて消えるまで待ってください。その状態で、cca-partsにPod born-no-operator(イメージはclientと同じcurl、ラベルはborn=no-operator、コマンドはsleep 86400)を作成し、Readyになったら/root/cca-parts/operator-down.jsonに、operator_replicas(そのときのDeploymentのspec.replicas、数値)、pod_uid、pod_ip、identity(そのPodのCiliumEndpointのアイデンティティ)を記録します。記録したら、operatorを1に戻し、availableになるまで待ちます。
公式ドキュメントは、operatorを「ノードごとではなくクラスターで1回行えばよい仕事」を担当するコンポーネントとして説明しており、転送やポリシー判定の経路には含まれないとしています。すでにノードにpodCIDRの断片があるとき、アドレスは誰が割り当てるのか、初めて見るラベルの組み合わせのアイデンティティオブジェクトは誰が作るのかを観測してみてください。新しいアイデンティティ番号でkubectl get ciliumidentityを確認してください。
envoyを取り除いたらL7の経路だけが止まった
cca-partsにCiliumNetworkPolicy l7-getを作成してください。endpointSelectorはapp=api-l7、ingressは1項目で、fromEndpointsはapp=client、toPortsにはTCPの"8080"とrules.http [{method: GET, path: /allowed}]を指定します。clientからapi-l7の/allowedが200、/secretが403になることを確認したあと、kube-systemのcilium-envoy DaemonSetに、存在しないnodeSelector cca-lab/envoy: "off"を追加して、envoyのPodを取り除きます。その間にclientからapi-l7の/allowed(5秒の制限付き)とappのService(http://app:8080/)にリクエストを送り、agent内のhubbleでclient→api-l7のフローを読み取って、/root/cca-parts/envoy-down.jsonに、l7_code、plain_code(HTTPコードの文字列で、応答がない場合は"000")、flows(client→api-l7のフローのcompact形式の行のリスト)を記録します。そのあと、そのnodeSelectorのキーを削除してenvoyを元に戻し、/allowedが再び200になるまで待ちます。
サイドカーのないCiliumでは、HTTPルールはノードのenvoyが判定します。eBPFはL3/L4の判定のあと、その接続をプロキシへ渡しますが、受け取るenvoyがなければどうなるでしょうか。L7ルールがないServiceはプロキシを経由しません。nodeSelectorのキーに/が含まれる場合、JSON patchのパスでは~1と書きます。フローはhubble observe --since <시각> --namespace cca-parts -o compact(プレースホルダーは時刻です)で読んでください。
relayが落ちても、ノードはフローを集め続けていた
kube-systemのhubble-relayをreplicas 0に下げてください。VMのシェルでhubble status --server <hubble-relay 서비스 ClusterIP>:80(プレースホルダーはhubble-relayのServiceのClusterIPです)が失敗するときのエラーを1行、agentの中でhubble observe --last 5 -o compactのフローの行とhubble statusのCurrent/Max Flowsの行を読み取って、/root/cca-parts/relay-down.jsonに、relay_error、local_flows(リスト)、local_statusとして記録します。そのあと、relayを1に戻し、relay経由のstatusが成功するまで待ちます。
Hubbleは、agentごとにフローをリングバッファに集め、relayは複数のノードのバッファを1か所から照会できるようにする中継役です。どちらが止まると何を失うのかを比べてください。失敗したコマンドのエラーは標準エラー出力に出るので、2>&1で受け取ります。
agentを取り除いても既存のリクエストは通り続ける
kube-systemのcilium DaemonSetにnodeSelector cca-lab/agent: "off"を追加して、agentのPodを取り除いてください(このステップでは元に戻しません)。Podが消えたらノードのtaintを確認し、clientからappのServiceにリクエストを送ったあと、cca-partsにcurlのPod orphan(コマンドはsleep 86400)を作成して、FailedSchedulingイベントを待ちます。/root/cca-parts/agent-down.jsonに、existing_code(client→appのコードの文字列)、taint(ノードにかかったciliumのtaintのkey:effect)、orphan_uid、orphan_phase、reason・message(orphanのFailedSchedulingイベント)を記録します。
agentは、BPFプログラムとマップをロードして更新する側であり、パケットを直接運ぶプロセスではありません。agentがない間、すでにロードされたものはどうなるでしょうか。新しいPodにはCNIがネットワークを接続できないので、operatorがノードにtaintを付けてスケジュールを止めます。イベントはkubectl get events --field-selector involvedObject.name=orphanで確認します。
agentが戻るとtaintが外れる
cilium DaemonSetからnodeSelectorのキーcca-lab/agentを削除して、agentを元に戻してください。agentがReadyになり、ノードのagent-not-ready taintが消え、orphanがスケジュール(PodScheduled=True)されるまでポーリングしたあと、/root/cca-parts/restore.jsonに、taint_removed(true/false)、orphan_scheduled_at(orphanのPodScheduled conditionのlastTransitionTime)、agent_pod(新しいagent Podの名前)を記録します。
taintを付けるのも外すのも、operatorがagent Podの状態を見て行います。スケジュールされたあと、コンテナがRunningになるまではさらに時間がかかることがあるので(サンドボックスのリトライ)、このステップはスケジュールまで待ってください。agentが入れ替わった直後は、hubble-relayもピアに再接続する間、しばらくReadyが外れます。最後の全体採点の前に、orphanがRunningかどうかも一度確認してみてください。
誰が止まると何が止まるのか
/root/cca-parts/report.txtに、키=값形式の行(プレースホルダーはキーと値です)を8行書きます。項目は、ipam_mode、node_pod_cidr、identity_allocation_mode、without_operator_new_pod(born-no-operatorの現在のphase)、without_envoy_l7・without_envoy_plain(envoy-down.jsonの2つのコード)、without_agent_existing・without_agent_new_pod(agent-down.jsonのexisting_codeとorphan_phase)です。値は、記録ファイルおよび現在の設定と一致している必要があります。
レポートは、コンポーネントごとに「ない間も続いたこと」と「止まったこと」を1行ずつ対応させた表です。記録ファイルはjqで、設定はcilium-configとCiliumNodeから読んでください。