adtech studio

[Kubernetes] オンプレでも GKE Like な Ingress を使うために 自作 Ingress Controller を実装してみた

masaya By masaya

Kubernetes OSS ネットワーク 検証 開発

こんにちは。
CIA の青山 真也です。

この記事は Kubernetes Advent Calendar 1日目の記事になります。

ちなみに CyberAgent Advent Calndar 1日目の記事も「GKE 互換のオンプレコンテナ基盤 AKE (Adtech Container Engine) 誕生秘話とアーキテクチャ完全公開!」というテーマで書いており、まるで夏休み最終日の気分です。
弊社のオンプレコンテナ基盤 AKE の話を詳細にまとめてありますので、こちらも是非お読み下さい。

今回は、SSL 終端や L7 パスベースルーティング及び L7 ロードバランシングを行ってくれる Ingress リソースの話をします。
GKE の Ingress が最高すぎるんですが、GKE の Ingress に負けない使い勝手になりました!

 

はじめに

みなさんは GKE 等で Ingress リソースを作成したことがある方が多いかと思います。

詳しくは後ほど話しますが、GKE で Ingress リソースを作ると非常に簡単に Ingress リソースを作ることができ、しかも分かりやすいと言った特徴があります。
AWS などでも alb-ingress-controller 等で ALB 連携できる Ingress Controller などが用意されています。

しかし、OpenStack 環境やベアメタル環境などでは Ingress を使う場合には手間が掛かる(しかも初心者には分かりづらい)nginx-ingress-controller や nghttpx-ingress-controller を利用する必要があります。

今回は、OpenStack 環境やベアメタル環境などの Kubernetes でも GKE Like に Ingress を利用できる方法をお伝えできればと思います。

 

Ingress の基礎知識

Ingress は L7 LoadBalancing を行なうリソースです。

たとえばこのような Deployment と、

このような Service を事前に作っておきます。

 

Ingress では NodePort の Service を登録しておく必要があります。

その後、下記のような Ingress リソースを登録することで L7 LoadBalancer が作成され、L7 LoadBalancer から svc1 に対して転送されるような形になります。
余談ですが、spec.backend はデフォルトの転送先になります。また、SSL 終端を行なう場合には spec.tls を設定しておいて下さい。

 

図にするとこのような感じです。(/path2 宛を svc2 に転送する設定も入っている場合)

仕組みとしては、L7 LoadBalancer から Service に向けて転送するような形になっています。
GKE の場合と Nginx Ingress Controller の場合で結構仕組みはことなりますが、今の時点では結果的には L7 LoadBalancer から Service に向けて転送するということだけ抑えておいて下さい。

 

Ingress と Ingress Controller の違い

Ingress と Ingress Controller という似たような名前が登場してきていますが、これら 2 つは全くの別物です。

  • Ingress
    • Kubernetes のリソースの1種
    • ただの定義にすぎないため登録しただけでは何も起こらない(イメージ的には Custom Resource Definision に近い)
  • Ingress Controller
    • Ingress リソースの定義を読んで L7 ロードバランサを用意する

通常はこの Ingress リソースを登録しても Kubernetes に登録自体はされますが、何も起こりません。

GKE ではマネージドマスターで見えなくなっていますが、デフォルトで Ingress Controller が配備されているため GCLB の L7 LoadBalancer が作成されているだけです。

 

GKE の Ingress

GKE の Ingress の場合、Google Cloud LoadBalancer 上に「特定のパスから全 Kubernetes Node の NodePort へと転送する設定」が登録されます。
また、SSL 終端なども GCLB 上で行われます。

Ingress の場合には DSR 構成ではないため、SNAT と同じようにクライアントの IP Address が欠損してしまいますが、HTTP Header の X-Forwarded-For にクライアントの IP Address があるため、必要に応じて利用して下さい。

 

厳密には、GCLB > NodePort Service > Pod という経路をたどるため、GCLB が Kubernetes Node に割り振った後、Kubernetes Node から Kubernetes Node へと転送される可能性がある点に注意して下さい。
より低レイテンシを求める場合には、NodePort Service を作成する際に spec.externalTrafficPolicy を Local に設定して下さい。
この設定を行った場合には、NodePort Service が受けたトラフィックは必ず自分の Pod へと転送するようになります。
但し注意点が 2 つあります。

1つ目は、Kubernetes Node 上に転送できる Pod が無い場合は 別の Kubernetes Node に転送するといった柔軟な対応はしてくれません。spec.externalTrafficPolicy を Local に設定した場合は、必ず 各ノードに Pod が居るようにするか、NodePort に GCLB の転送設定を書き換えて下さい。

2つ目は、Ingress には関係ありませんが、spec.externalTrafficPolicy を Local にしていても、内部のコンテナから NodePort を叩くと、別の Kubernetes Node にもバランシングされる点に注意して下さい。仕様なのかはわかりませんが、iptables のエントリ的に ClusterIP 相当のチェインに吸い込まれるようです。

さすがは GKE といったところでしょうか。非常に分かりやすく理に適った構成だと思います。

 

Nginx Ingress Controller

Nginx Ingress Controller では、L7 LoadBalancer の代わりに Nginx Pod を作成し、Nginx で SSL 終端やパスベースのルーティングを行います。

また、Nginx Pod を作成すると書きましたが、Nginx Ingress Controller = Nginx Pod となります。そのため、負荷分散を行なう場合には Nginx Ingress Controller (Nginx Pod)を Deployment などでスケールさせることで実現します。
展開方法については詳しくはここでは書きませんが、各リポジトリの examples を見ると良いかと思います。

さらに、GKE の Ingress では/path1 に来たリクエストを svc1 の NodePort に転送していましたが、Nginx Ingress Controller では、/path1 に来たリクエストを直接 Pod に転送してくれるといった違いもあります。

この状態だと Nginx Pod に来たリクエストを分散対象の Pod にL7 LoadBalancing や SSL 終端・パスベースルーティングなどを行ってくれることはわかると思います。

しかし、このままではグローバルなエンドポイントを持たないため、nginx Ingress Controller の Pod に向けた “type LoadBalancer” Service を作成して上げる必要があります。
“type LoadBalancer” が利用できない場合などは、nginx Ingress Controller の Pod に向けた “type NodePort” などを作成することでも代用は可能です。
また、クラスタ内部でしか使わない場合には “type ClusterIP” でも問題ありません。

 

整理すると、L4 LoadBalancer > Nginx Ingress Controller Pod > 各 Pod という経路を辿ります。

 

Nginx Ingress Controller が GKE には勝てない部分

Nginx Ingress の使いづらいポイントはいくつかあります。

前述の通り、エンドポイントは Nginx Ingress Controller Pod への Service を作ることで実現します。
そのため、kubectl get ingress などした場合に、IP Address などは書かれておらず、自分自身で管理しなければなりません。
言い換えるならば、Ingress リソースを作成した後は、その Ingress リソース用に Nginx Ingress Controller Deployment を作成し、Service で紐付けてあげる作業が必要です。
さらに、Ingress 上には Service の IP が登録されるわけではないので、確認したい場合には対応する Service を探さなければならないと言った手間が発生します。

 

例えば、GKE 上ではこのような表示になるのですが、

 

Nginx Ingress の場合にはこのような表示となってしまい、Address を確認することができません。
これは Nginx Ingress を利用している場合には、Ingress リソースの Status が更新される処理が存在しないためです。

 

 

また、GKE の場合には複数の Ingress リソースを作成した場合に、それぞれの Ingress リソース毎に GCLB が作成されます。
これらは別のエンドポイントとなるため分離することが可能です。

 

 

一方で nginx ingress Controller の場合には、Ingress リソースに何も指定しないで複数作成した場合、Ingress Controller はどちらの Ingress リソースの定義も吸収します。したがって、上記の様なことを行おうと思っていたにも関わらず、両方共 エンドポイントを分けることは可能だとしても、中身は全く同じになってしまいます。

 

Nginx Ingress Controller を上記のように分離したい場合には、Ingress Controller と Ingress リソースの紐付けを行ってあげる必要があります。

Ingress Controller 側には –ingress-class HOGE の起動オプションを渡して起動します。また、Ingress リソース側に関しては annotation で kubernetes.io/ingress.class: HOGE を指定して作成します。
この作業をすることにより、Ingress Controller が管理する Ingress リソースを判別することができるようになるため、分離を行なうことが可能です。

 

自作 Ingress Controller

前述のおさらいですが、Nginx Ingress Controller を利用する場合、GKE に比べて大きく 3 つの使い勝手が悪い部分があると考えられます。

1.Ingress リソースの状態を確認しても Status は何も分からない

2.Ingress リソースを作成した後に、Nginx Ingress Controller Pod を立てて Service も作成する必要がある

3.Ingress リソースの分離性を保つために Ingress リソースと Nginx Ingress Controller に識別子を設定する必要がある

 

今回はこの問題を解消するべく、Ingress Controller を自作しました。
GKE の方が良いとは言わせません。オンプレも頑張ってるんだぞー。

Deployment Controller など様々なコントローラ系のソースを参考に作成しました。
Controller 系統を自作する場合には、イベントハンドラの機構などは用意されてはいますが、実装量は結構あるため type LoadBalancer のようにお気軽に作れるものではありませんでした。

イベントハンドラとしては、特定の Kubernetes API Resource に対して「追加」「変更」「削除」の処理が行われたタイミングで呼ばれます。
Deployment Controller であれば、追加時に ReplicaSet の追加を行なう・変更時に ReplicaSet から ReplicaSet へのローリングアップデートを実施、削除時は自身の保持する ReplcaSet を削除するといった処理を行なう必要があります。

 

また、各関数には追加削除時はそのリソースの情報、変更時は変更前後のリソースの情報が渡ってくる形になります。
今回の GKE Like な Ingress Controller では この obj interfece{} は obj.(*ext.Ingress) として Ingress リソースとして扱うことができる構造体として渡ってきています。

 

GKE Like な自作 Ingress Controller の仕組みはわりとシンプルな構成になっており、Nginx Ingress Controller を上記のように手動で使う部分を全て自動化する Controller として実装しています。

onAdd が呼び出された際には、下記の 4 つの工程を行います。

  • Ingress リソースの annotation に “kubernetes.io/ingress.class” = NAMESPACE_INGRESSNAME を埋め込む
  • Ingress リソースに対応する Ingress Controller の Deployment を作成(–ingress-class = NAMESPACE_INGRESSNAME)
  • 上記の Deployment に対する “type Loadbalancer” Service を作成
  • Ingress リソースの Status に LB の IP と ClusterIP を反映

リソースの作成には kubernetes/go-client を利用しています。
例えば、Ingress の場合にはここにCreate()、Delete()、Update()、UpdateState() といったインターフェースが定義されています。

また、全てのリソースに共通することですが、onAdd 関数内で複数回リソースを更新するとエラーが発生するため、1回だけにする必要があります。
さらに、各リソースの Status に関しては Update() 関数ではなく UpdateStatus() 関数を利用しないと更新があっても無視される点に注意して下さい。
今回の場合だと、Ingress リソースの Status.LoadBalancer.Ingress 構造体を更新することで、LoadBalancer の IP と Cluster IP を登録しています。

onUpdate が呼び出された場合には特別な作業は行いません。作り込み次第ですが、 IP 変更時の変更処理程度だと思います。
Ingress リソースの更新に関しては、Nginx Ingress Controller の Deployment が変更を反映してくれるため、この部分に複雑な処理が必要ないのは多いなメリットだと思います。

onDelete 時には Deployment と Service を削除します。

 

以上の実装をすることで、GKE Like な Ingress Controller を作成することが出来ました。
GKE と違う部分としては、Internal から Ingress を叩く場合には ClusterIP を利用したほうが良いため、Ingress の IP にも一応入れています。

 

裏側的には Deployment と Service が配置されています。当然 annotation と ingress-class は指定された状態でデプロイされています。

 

物理 Ingress Controller

Nginx Ingress Controller を利用する形で GKE ライクな Ingress Controller を自作しましたが、結局のところパフォーマンスを最大化するには L7 を捌ける Hardware LoadBalancer が欲しいなと思います。
現状の弊社の LoadBalancer では L7 を全て捌き切るのば難しいため、実装を見送りましたが、Hardware LoadBalancer を使った Ingress も実現することは容易なため、機会や利用感を見つつ検討したいと思っています。

 

Nginx / Nghttpx Ingress Controller の違い

Nginx を利用しているか Nghttpx を利用しているかの違いがありますが、Nghttpx Ingress Controller は Nginx Ingress Controller を参考に作られているため、基本的には同じだと思います。
Nghttpx の方は ZLab 様が公開してくれており、HTTP/2 に対応しているため、gRPC にも利用できるといった特徴があります。
また、今回実装した GKE Like Ingress Controller はどちらも選択できるようにしています。

 

以上、Ingress Controller を作ってみたお話でした。
KubeCon から帰ってきたら、じきに OSS で公開しようかな―と思っています。