Skip to main content

K3sでcert-managerの証明書が自動更新されない。HTTP-01チャレンジをServiceLBが奪っていた

1. 症状 — cert-managerのCertificateが「Ready: False」から動かない

k3s cert-manager 証明書 更新されない という状態にハマったとき、多くのチームがまず疑うのはDNS設定かcert-managerのバージョンだ。しかし実際の犯人はもっと足元にある。

section01

cert-managerでLet's Encrypt証明書を自動更新している構成で、kubectl get certificateがReady: Falseのまま変わらないという報告は珍しくない。cert-managerのGitHub Issueでは、k3s + Traefik環境でHTTP-01チャレンジ用のPodが「Progressing」のまま進まず、アクセスすると503 Service Unavailableが返るという事例が報告されている。ログにはpropagation check failedというエラーが残り、Certificate → CertificateRequest → Order → Challengeというリソースの依存チェーンのどこかで詰まっていることが分かる。

原因の切り分けは、この4つのリソースをkubectl describeで順番に追うところから始まる。だが、Traefikの設定を何度見直しても異常が見つからないケースが少なくない。それもそのはずで、多くの場合、問題はTraefikでもcert-manager自体でもなく、K3sが標準搭載しているServiceLBというコンポーネントにある。こうした「設定はしたはずなのに、実は別のコンポーネントに専有されていた」というK3s特有の落とし穴は、クラスタの状態を普段からどこまで可視化できているかで発見のスピードが大きく変わる。

2. 原因1 — ServiceLBがport 80/443を専有し、チャレンジPodへの到達性を壊す

section02

K3sには、LoadBalancer型のServiceを使えるようにする「ServiceLB」(旧称Klipper LB)という軽量なロードバランサー実装がデフォルトで組み込まれている。K3s公式ドキュメントによると、ServiceLBはLoadBalancer型ServiceごとにDaemonSetを作成し、クラスタ内の各ノードにPodをデプロイして、hostPortでそのサービスポートを直接専有する。

デフォルト構成では、K3s同梱のTraefikがLoadBalancerサービスとしてport 80と443をこの仕組みで専有する。結果として、同じノード上で同じポート番号を要求する別のLoadBalancerサービスやHostPort Podは競合し、Pendingのまま残ることになる。

k3s-io/k3sのDiscussionが解説しているとおり、ServiceLBのPod自体は実はトラフィックを処理していない。各ノードにiptablesルールを設定し、外部IPからClusterIPへのマッピングとノードポートからのフォワーディングをカーネルレベルで行っているだけだ。つまり「見えている構成」と「実際にパケットを処理している層」が一致しないため、port 80への到達性トラブルは通常のPodログやTraefikの設定確認だけでは発見しにくい。

cert-managerのHTTP-01チャレンジは、cert-manager公式ドキュメントにあるとおり、既存のIngressやGateway設定を利用して一時的なacmesolver Podへのルーティングを行う。この経路のどこかでport 80への到達性が失われていれば、チャレンジは静かに失敗し続ける。Let's EncryptのHTTP-01チャレンジ仕様では、検証は必ずport 80で行うと定められており、任意のポートを指定することはACME標準上許可されていない。つまり、port 80の到達性が壊れている限り、代替ポートで回避することもできない。

3. 原因2 — ingressClassNameの不一致でTraefikがチャレンジ用Ingressを拾わない

section03

もう一つ見落とされがちなのが、cert-managerが自動生成するACMEチャレンジ用IngressのingressClassNameだ。Kubernetes公式ドキュメントが説明するとおり、Ingressリソースのspec.ingressClassNameは、どのIngressコントローラーがそのリソースを処理するかを決めるフィールドで、デフォルトのIngressClassが定義されていない環境では動作がコントローラーの実装依存になる。

k3s-io/k3sのGitHub Issueでは、cert-managerが作成したチャレンジ用IngressにingressClassName: traefikが設定されると、K3s同梱のTraefikがそのリソースを認識しないケースが報告されている。回避策はIngressリソースからingressClassNameを手動で削除することだが、これは応急処置に過ぎず、次回の証明書更新時に同じIngressが再生成されれば問題は再発する。

さらにTraefik Labsのコミュニティフォーラムでは、/.well-known/acme-challenge/へのリクエストにTraefikが正しく応答せず、IngressRouteのバックエンドサービスへそのまま転送してしまう事例や、HTTP→HTTPSリダイレクトが有効な場合にチャレンジ自体がリダイレクトされて失敗する事例も報告されている。原因1(ServiceLBのポート専有)と原因2(ingressClassNameの不一致)は独立した問題だが、実際の障害では両方が重なって発生することも多く、片方だけ直しても解決しないケースがあることに注意したい。マネージドK8sサービスの中には、こうしたIngressコントローラーとロードバランサーの整合性を導入時点でテンプレート化しているものもあり、Kuboもその一つだ。

4. 対処法 — ノード固定・ingressClassName調整・disable servicelbの使い分け

section04

応急処置としては、詰まっているCertificate・CertificateRequest・Order・Challengeのリソースを削除して再作成し、その際にingressClassNameの指定を見直すことで一時的に復旧できる。ただしこれは対症療法であり、次の更新サイクルで再発するリスクが残る。

恒久対策としては2つの方向性がある。一つは、K3s公式ドキュメントにあるとおり、enablelbラベルとlbpoolラベルを使ってServiceLBの対象ノードを固定し、ポート競合が起きにくい構成に変える方法だ。もう一つは、--disable=servicelbでServiceLB自体を無効化し、MetalLBのような、bare-metal/オンプレ環境向けに設計された本格的なロードバランサー実装に切り替える方法だ。MetalLBはBGPやL2モードで既存のネットワーク機器と連携し、クラウド環境と同等のロードバランシングをオンプレミスでも実現する。

どちらを選ぶかは、クラスタの規模と運用体制次第だ。小規模なクラスタでノード数が少なく、複雑なネットワーク要件がなければノード固定で十分なことが多い。一方、複数のLoadBalancerサービスを恒常的に運用する本番クラスタでは、MetalLB導入によって根本的にポート競合のリスクを排除したほうが運用は安定する。

こうした「デフォルトで動いているつもりが、実は特定のコンポーネントに専有されていた」という設定不整合は、K3sを本番運用する上で繰り返し遭遇するパターンだ。KuboのCaptain UIでは、こうしたクラスタ内の設定状態を可視化できるため、ServiceLBのポート専有やIngressClassの不一致のような「見えない専有者」を事前に発見しやすくなる。

5. まとめ — K3sの「省略された当たり前」が本番障害の火種になる

K3sは軽量なKubernetesディストリビューションとして、ServiceLBやTraefikを標準搭載することで初期構築の手間を大幅に減らしてくれる。しかしその「省略された当たり前」が可視化されないまま本番運用に持ち込まれると、今回のcert-manager証明書更新失敗のような、原因の特定に時間がかかる障害につながる。

証明書が更新されないという症状に直面したら、cert-manager自体やDNS設定だけでなく、ServiceLBのポート専有とIngressClassの整合性という、K3s特有のレイヤーも合わせて確認してほしい。

EKSやAKSのような高コストなマネージドK8sに悩まされることなく、こうした運用の落とし穴を事前に減らしたいのであれば、K3sベースのKuboが一つの選択肢になる。Let's Encrypt証明書の自動管理を標準搭載しつつ、Captain UIで構成の見落としを可視化できる点は、本記事のような障害を未然に防ぐ助けになるはずだ。同じ落とし穴を踏みたくないチームは、お問い合わせから一度相談してみるとよいだろう。

Related articles

← Back to all posts