k3sでサービスを公開し始めると、最初はTraefikのIngressだけだったものに、TLS証明書、不正アクセス対策、ログイン認証が順に積み重なっていきます。
個々のツールを導入する記事は数多くあります。しかし、実際に運用して分かったのは、大事なのは設定項目を暗記することではなく、「1本のリクエストがどの順番で処理されるか」を1枚の絵として持っておくことでした。
この絵がないと、障害が起きたときに、アプリ、Traefik、認証基盤、セキュリティ対策のどこを調べればよいのか分からなくなります。
この記事では、私の個人k3sクラスタで使っている構成をもとに、Traefik、cert-manager、CrowdSec、OAuth2 Proxyの責務を、リクエストが通る順番に整理します。
YAMLの書き方ではなく、障害時にどの層から確認するかが中心です。
先に結論
リクエストは、運用上は次のような順番で処理されると考えると分かりやすくなります。
[ブラウザ]
│
│ ① DNSでホスト名をIPアドレスへ変換
▼
[Traefik EntryPoint]
│
│ ② TLS終端
│ cert-managerが発行・更新した証明書をTraefikが使用
▼
[Router]
│
│ ③ Host・Pathなどのルールに一致するRouterを選択
▼
[共通Middleware]
│
│ ④ CrowdSec bouncerで送信元IPを判定
▼
[認証Middleware:必要なサービスのみ]
│
│ ⑤ OAuth2 Proxyを通じてログイン状態を確認
▼
[Service]
│
▼
[Pod]
│
│ ⑥ アプリが初めてリクエストを処理
この図は、Traefik内部の実装を厳密に表したものではなく、障害を切り分けるための運用モデルです。
特に重要なのは、次の3点です。
- cert-managerはTLS通信を処理するのではなく、証明書を発行・更新する
- CrowdSecはRouter選択後、Serviceへ転送される前の共通Middlewareとして動く
- OAuth2 Proxyによる認証は、必要なRouterだけに追加する
設計の原則は、アプリに関係しない仕事を、アプリへ到達する前に終わらせることです。
TLS、不正な送信元IPの遮断、共通ログインをアプリごとに実装しないことで、新しいサービスを公開するときの設定をIngressと必要なMiddlewareの指定に集約できます。
① DNS:まずTraefikまで届いているか
ブラウザが最初に行うのは、ホスト名からIPアドレスを調べることです。
ここで間違っていると、リクエストはTraefikにすら届きません。
DNSの障害では、次のような症状が出ます。
- 名前解決できない
- 古いIPアドレスへ接続される
- 一部のDNSサーバーからだけ接続できない
- IPv4では届くがIPv6では届かない
サービスが見えないと、すぐIngressやPodを調べたくなります。しかし、Traefikのアクセスログに何も記録されていないなら、その前段で止まっている可能性があります。
まず「Traefikまでリクエストが到達しているか」を確認するのが出発点です。
② TLS:cert-managerとTraefikの役割を分ける
私の環境では、公開サービスの証明書をcert-managerで発行・更新しています。
ここで混同しやすいのが、cert-managerとTraefikの役割です。
- cert-manager:証明書を取得し、Kubernetes Secretへ保存・更新する
- Traefik:そのSecret内の証明書を使ってTLSを終端する
つまり、ブラウザとTLS通信をしているのはTraefikです。
cert-managerは、そのときにTraefikが提示する証明書を用意しています。
私は証明書関連の定義をGit上でまとめて管理し、どのドメインにCertificateリソースが存在するかを確認しやすくしています。証明書をアプリ固有の設定ではなく、クラスタの玄関が持つ在庫として扱う考え方です。
証明書エラーが起きた場合は、次の順で確認します。
- DNSが正しいIPアドレスを指しているか
- Ingressが参照するTLS Secret名が正しいか
- CertificateリソースがReadyになっているか
- ACME ChallengeやOrderが失敗していないか
- Traefikが意図した証明書を選択しているか
「証明書がある」ことと、「Ingressがその証明書を使っている」ことは別です。
③ Router:どのServiceへ送るかを決める
TLS終端後、TraefikはHostやPathなどのルールを使って、リクエストを処理するRouterを選びます。
たとえば、次のような違いです。
blog.example.com → ブログのService
grafana.example.com → 監視画面のService
この層で問題があると、特定のドメインだけ404になることがあります。
最初に確認するのは次の項目です。
- IngressのHost名
- IngressClass
- Service名
- Serviceのポート
- TraefikがRouterを正しく読み込んでいるか
注意したいのは、404がTraefikから返っているのか、アプリから返っているのかです。
Traefikのアクセスログに記録されるRouter名やService名を見ると、Routerに一致しなかったのか、アプリまで到達した後に404になったのかを分けられます。
④ CrowdSec:共通Middlewareとして遮断する
CrowdSecの判定は、TraefikのMiddlewareとして共通適用しています。
EntryPointに共通Middlewareを設定すると、そのEntryPointを使うRouterに自動的に適用できます。厳密にはRouterのルールに一致した後でMiddlewareが実行されますが、運用上はどの公開サービスにも共通する玄関の検問として扱えます。
この位置に置く理由は、アプリごとにCrowdSecの設定を追加すると、いつか設定漏れが起きるからです。
玄関側へ共通適用すれば、新しいサービスを公開した時点から同じ判定を通せます。
共通Middlewareは障害も共有する
この構成には大きな注意点があります。
私が以前使っていた構成では、bouncerがリクエストごとにCrowdSec LAPIへ問い合わせていました。その状態でLAPIへの接続に失敗すると、技術的なエラーを遮断として扱い、複数のドメインが一斉に403になることがありました。
これはCrowdSec全体の共通仕様ではなく、使用するbouncer、バージョン、モード、キャッシュ設定によって変わります。
そのため、運用前に次の点を確認しておく必要があります。
- LAPIへ到達できない場合に許可するのか、遮断するのか
- 判定結果をローカルにキャッシュするのか
- streamモードなど、LAPIへの問い合わせを毎回行わない方式を使えるか
- 起動直後や再同期失敗時に、どのような動作をするか
セキュリティ製品では、障害時に通信を通すfail-openと、通信を止めるfail-closedのどちらを選ぶかが重要です。
fail-closedは安全側に倒れますが、共有Middlewareとして使う場合は、障害の影響も全サービスへ広がります。
⑤ OAuth2 Proxy:認証をアプリへ実装させない
管理画面や内部ツールには、認証機能がなかったり、製品ごとにログイン方法が異なったりします。
私はこのようなサービスを、TraefikのForwardAuth MiddlewareとOAuth2 Proxyを使って保護しています。
処理の流れは概ね次のようになります。
Traefik
│
│ 認証済みか確認
▼
OAuth2 Proxy
│
├─ 認証済み → リクエストを許可
│
└─ 未認証 → サインイン経路を通じてIdPへ誘導
ForwardAuthは、Traefikから外部の認証サービスへ問い合わせ、リクエストを通してよいか確認する仕組みです。
未認証時のIdPへのリダイレクトは、ForwardAuth単体ではなく、OAuth2 Proxyのサインインエンドポイントやエラー処理を含む認証経路全体で実現します。
この構成の利点は、アプリに認証機能がなくても、同じログイン基盤で保護できることです。
一方、ForwardAuthはあくまでアプリへ入る前の門です。
門を通った後の機能制限や権限管理は、アプリ側に残ります。
たとえば、ForwardAuthを通過できる人が、アプリ内ですべての操作を許可されるとは限りません。逆に、アプリ側に権限管理がない場合は、門を通過した人に管理操作まで許可することになります。
認証と認可は分けて考える必要があります。
⑥ ServiceとPod:ここまで来て初めてアプリを疑う
ブログなど、インターネット上の全員に公開するコンテンツには、通常、共通認証を付けません。
一方、管理用UIには認証を付けます。
判断基準は単純です。
インターネット上の誰が見てもよい画面か
公開コンテンツはCrowdSecまで、管理画面はOAuth2 Proxyの後ろに置きます。
ServiceとPodまでリクエストが届いているなら、そこで初めてアプリ側を調べます。
- Podが起動しているか
- readiness probeが成功しているか
- ServiceにEndpointが存在するか
- ServiceのtargetPortが正しいか
- アプリが想定したアドレスとポートで待ち受けているか
502や503ではTraefikを疑いたくなりますが、実際にはServiceの接続先が存在しない、PodがReadyではない、ポートがずれているといった原因も多くあります。
症状からの逆引き表
この構成で障害を調べるときは、次の表を上から確認します。
| 症状 | 主に疑う層 | 最初に確認するもの |
|---|---|---|
| 名前解決できない、違うIPへ接続される | ① DNS | A・AAAAレコード、DNSキャッシュ |
| 証明書警告、別ドメインの証明書が出る | ② TLS | Certificate、TLS Secret、Ingressの参照先 |
| 公開・管理を含む複数ドメインが一斉に403 | ④ 共通Middleware | bouncerのログ、LAPIやキャッシュへの接続 |
| 特定ドメインだけTraefikの404 | ③ Router | Host・Path、IngressClass、Routerの読み込み状況 |
| ログインがループする、401が続く | ⑤ 認証 | redirect URL、Cookie、Forwarded Header、IdPの状態 |
| 502または503 | ⑥ Service・Pod | Endpoint、targetPort、readiness、Podログ |
| 一部の利用者だけ403 | ④ CrowdSec | decision、送信元IPの認識、プロキシ経由時のIP判定 |
| Traefikのアクセスログに記録がない | ①より前 | DNS、Firewall、LoadBalancer、到達経路 |
この表で一番重要なのは、複数サービスが同時に落ちた場合は、個々のアプリより共有層を先に疑うことです。
アプリをいくつ増やしても、DNS、Traefik、共通Middleware、認証基盤は共有されています。
そのため、横断的な障害では、Podを一つずつ再起動するより先に、共有する玄関を確認した方が早く原因へ到達できます。
私が使っている切り分けの順番
新しいサービスが見えない場合、私は次の順番で確認します。
- DNSが意図したIPを返しているか
- TLS証明書が正しいか
- HTTPステータスが何か
- TraefikのアクセスログにRouter名とService名が出ているか
- 共通Middlewareが遮断していないか
- 認証対象ならOAuth2 Proxyまで到達しているか
- ServiceにEndpointが存在するか
- PodがReadyで、アプリが正常に応答するか
この順番の利点は、外側から内側へ一方向に調べられることです。
いきなりPodログから調べると、実際にはDNSで止まっているのに、アプリ側を延々と調査することになります。
この構成の弱点
横断層の障害が広く波及する
共通化は、設定漏れを減らせる一方で、障害の影響範囲を広げます。
特に次のコンポーネントは、多くのサービスに影響します。
- DNS
- Traefik
- 共通セキュリティMiddleware
- OAuth2 Proxy
- IdP
共通化するときは、設定方法だけでなく、そのコンポーネントが止まった場合に何が見えなくなるかまで整理しておく必要があります。
k3s同梱のTraefikはk3sの更新と関係する
私の環境では、k3sに同梱されているTraefikを利用しています。
構成が単純になる一方で、TraefikやHelm Chartのバージョン変更はk3sのリリースと関係します。
Traefikだけを独立して細かく更新したい場合は、k3s同梱版を無効化し、自分でデプロイする方法もあります。
私は現時点では、自由度よりも同梱構成の単純さを優先しています。
送信元IPの判定を間違えると遮断が機能しない
Traefikの前に別のロードバランサーやリバースプロキシがある場合、CrowdSecが見るIPアドレスが実際の利用者ではなく、直前のプロキシになることがあります。
逆に、信頼していない相手から送られたX-Forwarded-Forをそのまま信用すると、送信元IPを偽装される可能性があります。
CrowdSecを導入しただけで安心せず、Traefikがどの接続元やヘッダーを信頼する設定になっているかを確認する必要があります。
向いている人・向かない人
この構成が向いているのは、公開するサービスが増え、次のような悩みが出てきた人です。
- TLS設定の重複を減らしたい
- 管理画面のログインを統一したい
- セキュリティMiddlewareの付け忘れを防ぎたい
- 障害の切り分け方を標準化したい
一方、公開するサービスが1〜2個だけなら、ここまでの構成は過剰かもしれません。
アプリ内蔵の認証と、単純なリバースプロキシだけで十分な場合もあります。
重要なのは、ツールを増やすことではありません。
サービス数と運用負荷に合った玄関を作ることです。
まとめ
Traefikをクラスタの玄関にすると、TLS、送信元IPの判定、共通認証、ルーティングを、アプリの外側へ集約できます。
その一方で、共有層の障害は複数のサービスへ同時に波及します。
そのため、設定ファイルだけでなく、次の流れを1枚の図として持っておくことが重要です。
DNS
↓
TLS
↓
Router
↓
共通Middleware
↓
認証Middleware
↓
Service
↓
Pod
障害が起きたときは、内側のPodからではなく、外側のDNSから順に確認します。
この順序が頭に入っているだけで、「どこが悪いのか分からない」という時間を大きく減らせます。
TraefikからPodまでの通信も暗号化したい場合は、次の記事で構成を紹介しています。