K3sクラスタのネットワーキングをCiliumのeBPFベースのkube-proxy置き換え(kube-proxy replacement)に切り替えようとして、公式ドキュメント通りに設定したのに、なぜかNodePort経由の通信だけが届かなくなった――そんな経験はないだろうか。ClusterIP経由の通信は正常なのに、外部からNodePortで叩くと接続が返ってこない。この記事では、K3s特有の前提条件がCiliumの汎用手順とどこで食い違うのかを、実際に詰まったポイントに沿って整理する。

1. なぜK3sでCiliumのkube-proxy replacementを試すのか
K3sは軽量Kubernetesディストリビューションとして、デフォルトでFlannel(CNI)、組み込みのkube-proxy、そしてServiceLB(旧Klipper LoadBalancer)を標準搭載している。これは「追加設定なしですぐ動く」というK3sの強みだが、本番運用でより高度な可観測性やネットワークポリシー、パフォーマンスを求める段階になると、CNCFのGraduatedプロジェクトであるCiliumへの置き換えを検討するチームが増えている。
Ciliumはカーネル内で動作するeBPFを使い、Service負荷分散をiptablesのような線形ルール処理ではなくハッシュテーブルで行う。これによりServiceやエンドポイントの数に関わらずO(1)に近いルックアップ時間を実現できるとされる(The New Stackの解説記事)。CiliumはCNCFの中でもKubernetes、OpenTelemetryに次ぐ活発さを持つプロジェクトとされ、2023年10月にCNCF Graduatedステータスを取得している(Isovalentの公式ブログ、CloudNativeNowの報道)。
こうした背景から、「Flannelと組み込みkube-proxyをCiliumのkube-proxy replacementに置き換える」という構成変更は、K3sユーザーにとって現実的な選択肢になっている。問題は、Ciliumの公式手順自体は汎用的なKubernetesクラスタ(kubeadmベース)を前提に書かれており、K3s特有の初期設定をそのまま残したままでは、設定が「中途半端に二重」の状態になりやすいことだ。

こうした高度なCNI統合の検証自体にかかる時間は決して小さくない。こうした検証コストそのものをマネージドで肩代わりする選択肢があることも、構成変更を検討する際の判断材料になる。
2. 公式手順通りに導入したら、NodePortだけ繋がらなくなった
Cilium公式のK3sインストールガイドでは、マスターノードを以下のように起動することが案内されている(Cilium公式ドキュメント)。
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC='--flannel-backend=none --disable-network-policy' sh -
さらにkube-proxyそのものを置き換える場合は、--disable-kube-proxy フラグを追加する。公式ドキュメントでは、設定ファイル方式として以下も案内されている。
# /etc/rancher/k3s/config.yaml
flannel-backend: "none"
disable-network-policy: true
disable-kube-proxy: true
ここまでは多くの解説記事でも紹介されている標準手順だ。しかし、この状態でCiliumをHelmでインストールし、ClusterIP経由の疎通は確認できても、NodePort経由だけアクセスできないという事象に遭遇するケースが報告されている。Cilium公式のトラブルシューティング文脈でも、ネットワークインターフェースに紐づくIPアドレスの扱いによってNodePortが機能しないパターンが実際にIssueとして記録されている(cilium/cilium Issue #12317)。
K3s特有の環境では、この事象の背景に次の3つの要因が複合的に絡んでいることが多い。
要因1: ServiceLB(旧Klipper)が残ったままになっている
K3sはデフォルトでServiceLBと呼ばれる簡易ロードバランサーを提供しており、LoadBalancerタイプのServiceに対してhostPortを使ったDaemonSetを各ノードに展開する(SUSE公式のK3sネットワーキングドキュメント)。--flannel-backend=none や --disable-kube-proxy を指定しても、ServiceLBは自動的には無効化されない。この状態でCiliumのeBPFロードバランシングとServiceLBのhostPortバインドが同じノード上で競合し、期待したNodePortにトラフィックが届かなくなることがある。
要因2: Traefikのデフォルト構成がServiceLBに依存している
K3sはデフォルトでTraefik Ingressコントローラーをデプロイし、ポート80/443を使うLoadBalancer Serviceを公開する。このLoadBalancer Serviceの経路も標準ではServiceLBに依存しているため、要因1の問題を解消しないままTraefikだけ有効にしておくと、Ingress経由の通信も不安定になる。

要因3: APIサーバーへの接続情報(k8sServiceHost/k8sServicePort)の未設定
kube-proxyを取り除いた構成では、kube-proxyが担っていた「APIサーバーServiceのプロキシ」機能も失われる。Cilium公式ドキュメントは、この状態でCiliumエージェントがAPIサーバーに到達できるよう、k8sServiceHost と k8sServicePort を明示的に指定する必要があると説明している(Cilium公式: Kubernetes Without kube-proxy)。K3sのクイックスタート手順ではこの設定が省略されがちで、結果としてCiliumエージェントの初期化が不完全なまま起動し、Service連携全体が不安定になるケースがある。
同ドキュメントはさらに、既存のkube-proxyとCiliumのNATテーブルが互いを認識できないため、両者が一時的にでも共存すると接続が切断される可能性があると明記している。K3sのように複数のネットワークコンポーネントがデフォルトで同梱されているディストリビューションでは、この「一時的な共存」が起きやすい。
3. K3s向けの具体的な設定差分
上記3つの要因を踏まえ、K3s環境でCiliumのkube-proxy replacementを安定稼働させるための設定を整理する。
マスターノードの起動オプション
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="\
--flannel-backend=none \
--disable-network-policy \
--disable-kube-proxy \
--disable=servicelb \
--disable=traefik" sh -
Cilium公式手順に加えて、--disable=servicelb を明示的に追加するのがポイントだ。Traefikを引き続き使う場合は --disable=traefik を外し、後述のNodePort検証を終えてから有効化するとトラブルシュートが切り分けやすい。
Cilium Helmインストール時の必須パラメータ
helm repo add cilium https://helm.cilium.io/
helm repo update
helm install cilium cilium/cilium \
--namespace kube-system \
--set kubeProxyReplacement=true \
--set k8sServiceHost=<API_SERVER_IP> \
--set k8sServicePort=6443 \
--set ipam.operator.clusterPoolIPv4PodCIDRList="10.42.0.0/16"
<API_SERVER_IP> には、各ワーカーノードから到達可能なAPIサーバーのIPアドレス(マスターノードのIPまたはHA構成ならロードバランサーVIP)を指定する。K3sのデフォルトPod CIDR(10.42.0.0/16)と一致させることも忘れてはならない。この値はCilium公式のK3sインストールガイドでも明示されている(Cilium公式ドキュメント: Installation Using K3s)。
NodePort疎通確認
# Ciliumのサービス一覧にNodePortが登録されているか確認
cilium service list
# 実際にNodePortへアクセスして確認
curl http://<NODE_IP>:<NODE_PORT>
cilium service list の出力にNodePortエントリが見当たらない場合、ServiceLBのhostPortバインドが競合している可能性が高い。kubectl get pods -n kube-system -l app=svclb でServiceLB由来のPodが残っていないかを確認するとよい。

CiliumのService実装がこうして安定すると、こうしたネットワーク基盤の標準化はKubo CloudのようなPure Kubernetesを前提としたマネージド環境でも同じ考え方で再現できる。Kubo はK3sベースでありながら標準Kubernetes API互換を維持しているため、CiliumのようなCNCF Graduatedプロジェクトをロックインなくそのまま組み込める。
4. externalTrafficPolicyとTraefikを組み合わせる際の注意点
ServiceLBとTraefikを無効化してCilium主体の構成に移行した後、改めてIngress経由のトラフィックを扱う場合は externalTrafficPolicy の設定にも注意が必要だ。Kubernetes公式ドキュメントによれば、externalTrafficPolicy: Cluster(デフォルト)はクラスタ内のどのノードのPodにもトラフィックを転送できる一方でホップが増える可能性があり、externalTrafficPolicy: Local は同一ノード上のPodにのみ転送することでクライアントIPを保持できる(Kubernetes公式: Virtual IPs and Service Proxies)。
K3s公式ドキュメントは、NAT環境(パブリッククラウドなど)で externalTrafficPolicy: Local を使ってクライアント送信元IPを保持したい場合、各ノードに node-external-ip を設定しないことを推奨している(SUSE公式K3sドキュメント)。CiliumのeBPFロードバランサーに置き換えた後も、この制約自体はKubernetesのService仕様に基づくものであるため解消されない点に注意したい。
こうした細かい設定差分を一つずつ洗い出す作業は、オンプレミスで標準K8s運用の経験があるインフラエンジニアでも時間がかかる。Kubo のCaptain UIはクラスタの可視化された管理画面を提供しており、ServiceLBやkube-proxyの状態をブラックボックス化せずに確認できる構成を標準搭載している。
5. 本番投入前のチェックリスト
K3s + Ciliumのkube-proxy replacement構成を本番投入する前に、最低限次の項目を確認したい。
- ServiceLBが完全に無効化されているか:
kubectl get pods -n kube-system -l app=svclbで何も返らないことを確認 - Cilium Helm値にk8sServiceHost/k8sServicePortが設定されているか:
helm get values cilium -n kube-systemで確認 cilium service listにNodePortエントリが登録されているか- Pod CIDRがK3sのデフォルト(10.42.0.0/16)または変更後の値と一致しているか
externalTrafficPolicyの設定意図(Cluster/Local)がIngress/LoadBalancer構成と整合しているか- 移行作業中にkube-proxyとCiliumが一時的にでも共存していないか(Cilium公式ドキュメントが警告する通り、NATテーブルの不整合による接続断を避けるため)
これらを一つずつ検証する作業は、構成管理の属人化を招きやすい部分でもある。AI-Driven Deploymentを特徴とするKuboであれば、こうした検証・設定差分の洗い出しをAIに任せた上で、月額48,000円〜というコストで本格的なK3sベースのKubernetesクラスタを運用できる。
まとめ
K3sでCiliumのkube-proxy置き換えを試すと、公式手順だけでは見落としがちなK3s特有の前提(ServiceLBの残存、Traefikの依存関係、k8sServiceHost/k8sServicePortの未設定)によってNodePortが届かなくなることがある。原因は「Ciliumの手順が間違っている」のではなく、「K3sがデフォルトで同梱しているネットワークコンポーネントを明示的に無効化し切れていない」ことにある。ServiceLBとTraefikの無効化、Cilium Helm値の明示的な設定、そしてNodePort疎通確認を一つずつ積み重ねることで、K3sでも標準Kubernetesクラスタと同じeBPFベースのネットワーキングを安定運用できる。
マネージドK8sの世界では、EKSやAKSの高コストと複雑さ、ベンダーロックインに悩む必要はもうない。K3sベースのKuboなら、Ciliumのような本格的なCNCFプロジェクトの機能をそのまま活かしながら、圧倒的なコスト効率で運用できる。