Ingressを強化したKubernetes Gateway APIを試してみる

| 14 min read
Author: noboru-kudo noboru-kudoの画像

2022-07-13にKubernetesコミュニティ(sig-network)で策定が進められているKubernetes Gateway APIがベータ版になりました。

外部からのトラフィックにはKubernetes built-inのIngressリソースで対応することが多いと思いますが、Ingressには次のような欠点が指摘されてきました。

  • クラスタ管理者、アプリ開発者の関心事が1つのリソース(Ingress)内に混在している(責務が曖昧)
  • カナリアリリース等、Ingressでサポートしていない機能を利用する場合は、実装固有のアノテーションを指定する必要がある
  • HTTP(S)通信しか対応していない(この部分は現時点ではGateway APIでも実験的バージョン)

Kubernetes Gateway API(以下Gateway API)はこのような欠点を解消すべく誕生した新しいAPIです。
現時点ではサポートするプロダクトのほとんどが一部機能に限定されており、商用環境で使うにはかなり勇気が必要ですが、将来的にはIngressに取って変わる可能性があります。
今回はこのGateway APIを実際に試してみたいと思います。

Information

現時点で最も実装が進んでいるのはGCPのGKEで、パブリックプレビュー版として提供されています。
興味のある方は以下公式ドキュメントを参照してください。

Contents

Kubernetes Gateway APIが提供するリソース

#

Gateway APIは、ロールに応じてリソースが定義され、大規模クラスタでの運用に配慮した設計となっています。
次のリソースで構成されています。

リソース名 ロール 内容
GatewayClass インフラプロバイダ Gateway種類の定義
Gateway クラスタ管理者 Gatewayインスタンスの設定
xRoute(複数種類) アプリ管理者 or 開発者 Serviceへのルーティング設定

このようにGateway APIはロール指向でリソースが分割されています。

GatewayClassはクラウドプロバイダや実装ベンダーが提供するもので、利用者が作成することはほとんどないかと思います。
IngressでいうIngressClassと同義になります。

GatewayとxRouteは従来のIngressリソースに対応します。Gateway APIでは運用のしやすさを考慮し、クラスタ管理者とアプリ側で2つに分割されています。

Gatewayはクラスタ管理者が作成します。ここではGatewayインスタンス自体の設定をします。
また、ここで作成したGatewayの利用範囲を制限したり、TLSや共通のネットワーク設定もここで指定します。

xRouteはアプリのルーティングを設定するリソースで、アプリ開発者または管理者が自分のNamespace内に作成します。
xの部分はルーティングの方法に応じて複数定義されます。現時点では、HTTP(S)ベースのHTTPRouteのみがベータ版として提供されています。

HTTP(S)以外のxRoute

TLSのSNIベースのルーティングを定義するTLSRouteや、TCP/UDPプロトコルをサポートするTCP/UDPRouteがあります。
現時点では、両者は実験的(Experimental)バージョンで、デフォルトでは含まれていません(Experimental Channelとして提供)。
これが安定してくるとHTTP以外の様々なプロトコルに対応可能となり、Gateway APIの普及が大きく前進しそうです。

以下公式ドキュメントから、これらのリソースの関連を示したものを抜粋しました。

gateway api resource relations
引用元: https://gateway-api.sigs.k8s.io/concepts/api-overview/#combined-types

以降で実際のGateway APIの利用方法を見ていきます。

IstioでGateway APIをセットアップする

#

今回はKubernetes環境にはローカルで簡単に確認できるminikubeを利用しました[1]

次に、Gateway APIのカスタムリソース(CRD)をセットアップしておきます。現時点で最新のv0.5.0を準備します。

kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v0.5.0/standard-install.yaml

そしてGateway APIをサポートするプロダクトをインストールします。
Gateway APIの実装にはいくつかありますが、ここではIstioを使用することにしました。
Istioは以下公式ドキュメントよりセットアップします。

Caution

IstioのGateway APIサポートは現時点でアルファ版の位置づけです。
まだ安定しているとは言えませんので、実運用を検討する際は最新の実装状況を確認してください。

Istio以外でGateway APIをサポートする製品や実装ステータスは、以下に記載があります。

ここではistioctl(CLIツール)をインストールし、Istio本体をminimalプロファイルでセットアップします。

curl -L https://istio.io/downloadIstio | sh -
# ここでは現時点の最新バージョンの1.14.1をインストール
cd istio-1.14.1
export PATH=$PWD/bin:$PATH
istioctl install --set profile=minimal -y

インストールが終わるとistio-systemというNamespace配下にIstioのPodが実行されます。

kubectl get pod -n istio-system
NAME                      READY   STATUS    RESTARTS   AGE
istiod-859b487f84-48b9w   1/1     Running   0          3m2s

また、この時点でistioというGatewayClassも作成されていました。これを使用してGateway APIを利用できそうです。

kubectl get gatewayclass
NAME    CONTROLLER                    ACCEPTED   AGE
istio   istio.io/gateway-controller   True       1h

セットアップ後は別ターミナルで、minikubeからローカル環境にトンネルを通しておきます。

minikube tunnel

minikubeからローカル環境にトンネルが作成されます。このターミナルはこのままにしておきます。

Gatewayを作成する

#

ここでは以下のような構成を想定します。

sample-structure

fooとbarというチームがそれぞれ開発したアプリを、クラスタ管理者が管理する共通Gatewayで公開するものとします。
また、Gatewayはドメインexample.comのサブドメインを使用するアプリでのみ利用可能とします。

では、Gatewayリソースを作成してみます。
まずは、クラスタ管理者のみがGatewayを作成するものとして専用Namespaceを用意します。

kubectl create ns istio-ingress

ここではistio-ingressとしました。

作成するGatewayリソースは以下のようになります(gateway.yamlとしました)。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
name: gateway
namespace: istio-ingress
spec:
gatewayClassName: istio
listeners:
- name: default
hostname: "*.example.com"
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: All

spec.gatewayClassNameに先程確認したIstioが提供するGatewayClassの名前を指定します。

spec.listenersにはGatewayの設定を記述します。
ここではdefaultという名前で、ホスト名*.example.comのHTTP通信を許可するリスナーを1つ作成しました。

これを反映します。

kubectl apply -f gateway.yaml

Gatewayの状態は以下で確認します。

kubectl get gateway -n istio-ingress
NAME      CLASS   ADDRESS         READY   AGE
gateway   istio   10.102.160.70   True    82s

READY=trueとなり、Istio側の準備が完了しています。
ADDRESSが外部向けのIPアドレスです。
今回はローカル環境ですが、クラウド環境ではこの時点でロードバランサーが配置され、エンドポイントが割り当てられるはずです。

実際のGatewayのPod、Serviceも確認してみます。

kubectl get pod,svc -n istio-ingress
NAME                           READY   STATUS    RESTARTS   AGE
pod/gateway-6cd4768fdf-rcv4b   1/1     Running   0          4m6s

NAME              TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)                        AGE
service/gateway   LoadBalancer   10.102.160.70   10.102.160.70   15021:30671/TCP,80:30970/TCP   4m6s

Gateway Podが起動しています。実態はIstioのEnvoyプロキシーです。
また、LoadBalancerタイプのServiceも作成され、EXTERNAL-IPには先程のGatewayと同じIPアドレスが指定されています。
これを通して外部からのトラフィックを受け付けられる状態となりました。

HTTPRouteでルーティングを定義する

#

HTTPRouteを作成して、アプリのルーティングを定義します。

ここでは予めfoo/barNamespaceを作成し、以下のマニフェストでサンプルアプリをデプロイしておきました。

それぞれリクエストを受け付けると、Pod名を返すだけのシンプルなものです。
このアプリに対してHTTPRouteを作成します。

# foo-route.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: http
namespace: foo
spec:
parentRefs:
- name: gateway
namespace: istio-ingress
sectionName: default
hostnames: ["foo.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /foo
backendRefs:
- name: foo
port: 80
# bar-route.yaml
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: http
namespace: bar
spec:
parentRefs:
- name: gateway
namespace: istio-ingress
sectionName: default
hostnames: ["bar.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /bar
backendRefs:
- name: bar
port: 80

それぞれの基本的な構造は同じです。

まず、spec.parentRefs配下で先程作成したGatewayリソースを指定します。sectionNameにはリスナーの名前を指定します。先程defaultという名前でリスナーを作成しましたので、それを設定します。

spec.hostNamesにはホスト名を指定します。Gateway側でexample.comのサブドメインのみを許可しましたので、それぞれfoo.example.combar.example.comとしました。

最後にspec.rulesです。ここにアプリ向けに作成したServiceリソースへのマッピングを定義します。
この辺りはIngressの指定とほとんど同じです。それぞれのパス(/fooまたは/bar)に対応するServiceへのルーティングを指定します。

これをクラスタに反映します。

kubectl apply -f foo-route.yaml -f bar-route.yaml

HTTPRouteは以下で確認します。

kubectl get httproute -A
NAMESPACE   NAME   HOSTNAMES             AGE
bar         http   ["bar.example.com"]   2m34s
foo         http   ["foo.example.com"]   3m50s

実際のIstioのルーティング設定は、IstioのCLIツールistioctlからも確認できます。

GATEWAY_POD=$(kubectl get pod -n istio-ingress -o jsonpath='{.items[0].metadata.name}')
istioctl proxy-config route ${GATEWAY_POD} -n istio-ingress
NAME        DOMAINS             MATCH                  VIRTUAL SERVICE
http.80     bar.example.com                            http-0-istio-autogenerated-k8s-gateway.bar
http.80     foo.example.com                            http-0-istio-autogenerated-k8s-gateway.foo
            *                   /stats/prometheus*     
            *                   /healthz/ready*        

実際に動作するかをcurlで確認します。今回はDNS設定をしていないので、Hostヘッダを指定します。

GATEWAY_IP=$(kubectl get gateways gateway -n istio-ingress -o jsonpath='{.status.addresses[*].value}')
curl -H Host:foo.example.com ${GATEWAY_IP}/foo
> foo foo-99dcf8b8-jq8gm
curl -H Host:bar.example.com ${GATEWAY_IP}/bar
> bar bar-95c7cdf87-v8sqx

無事Gateway経由でPodにアクセスできました。

HTTPS通信を強制する

#

一般的に外部公開向けのエンドポイントはHTTPS通信を強制したいことが多いと思います。
Ingressだと、ルーティングを記述するIngressリソース側にTLS設定が必要でしたが、Gateway APIではGatewayリソース(つまりクラスタ管理者側)で設定します。

ローカルで簡単に試せる自己署名の証明書を使って試してみます。
まず、opensslで自己署名のワイルドカード証明書を作成し、Secretリソースとして登録します。

openssl req -x509 -nodes -days 30 -subj /CN=*.example.com \
-keyout server.key -out server.crt
kubectl create secret tls self-signed-cert \
--cert server.crt --key server.key -n istio-ingress

Gatewayリソースは以下のように修正します。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
name: gateway
namespace: istio-ingress
spec:
gatewayClassName: istio
listeners:
- name: default
hostname: "*.example.com"
# port: 80
# protocol: HTTP
port: 443
protocol: HTTPS
tls:
certificateRefs:
- kind: Secret
group: ""
name: self-signed-cert
namespace: istio-ingress
allowedRoutes:
namespaces:
from: All

spec.port/protocolには443/HTTPSとし、spec.tls.certificateRefsに先程作成した証明書のSecretリソースを指定します。
アプリ側で作成したHTTPRouteリソースは変更不要です。

これを適用します。

kubectl apply -f gateway.yaml

これだけです。今度はHTTPSプロトコルでアクセスします。

curl -H Host:foo.example.com --resolve "foo.example.com:443:${GATEWAY_IP}" \
--cacert server.crt "https://foo.example.com:443/foo"
> foo foo-99dcf8b8-jq8gm
curl -H Host:bar.example.com --resolve "bar.example.com:443:${GATEWAY_IP}" \
--cacert server.crt "https://bar.example.com:443/bar"
> bar bar-95c7cdf87-v8sqx

先ほどと同じ結果になりました。HTTPRouteリソース(つまりアプリ側)では何もせずともHTTPS化できました。
この構成であれば、更新を忘れがちな証明書もクラスタ管理者で一括管理になります。
非機能要件をアプリ担当者から分離できますので、ある程度大きい規模のクラスタでは理想的だと思います。

カナリアリリースをする

#

Gateway APIではカナリアリリースもサポートします。これをIngressで実現するには、実装固有のアノテーションで指定する必要がありました。

以下のようにfooアプリをv1 -> v2にアップデートするケースを想定します。
なお、シンプルに確認するために、先程のHTTPS通信はHTTPに戻しました。

canary release

今回の主要アクターはアプリ開発者です。HTTPRouteのルーティングを調整することでカナリアリリースを実現します。

まずはv1,v2のアプリを用意します。以下のマニフェストを作成し、デプロイ(Deployment, Service)しておきました。

現時点では100%のトラフィックをv1に向けておきますが、HTTPヘッダでenv=canaryと指定した場合はv2の方にトラフィックを振り向けるようにします。
HTTPRouteは以下のようになります。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: http
namespace: foo
spec:
parentRefs:
- name: gateway
namespace: istio-ingress
sectionName: default
hostnames: ["foo.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /foo
backendRefs:
- name: foo-v1
port: 80
- matches:
- headers:
- name: env
value: canary
path:
type: PathPrefix
value: /foo
backendRefs:
- name: foo-v2
port: 80

spec.rulesに2つのルールを記述しています。1つ目のルールでは先程と同様にパス一致でv1、2つ目のルールではHTTPヘッダ条件(spec.rules.matches.headers)を加えてv2へのルーティングを指定します。
この状態で反映すると、以下のようになります。

# デフォルト -> v1
curl -H Host:foo.example.com ${GATEWAY_IP}/foo
> foo foo-v1-6c476649d9-km6k4
# HTTPヘッダ(env=canary)指定 -> v2
curl -H Host:foo.example.com -H env:canary ${GATEWAY_IP}/foo
> foo foo-v2-9c5644495-vlzvx

期待通り、特定のHTTPヘッダがついている場合のみv2にトラフィックが流れました。

次に、v1へ80%、v2に20%のトラフィックを流すようにします。
HTTPRouteを以下のように変更します。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: http
namespace: foo
spec:
parentRefs:
- name: gateway
namespace: istio-ingress
sectionName: default
hostnames: ["foo.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /foo
backendRefs:
- name: foo-v1
port: 80
weight: 80
- name: foo-v2
port: 80
weight: 20

ルールは1つになり、spec.rules.backendRefsに2つのServiceを記述しました。
それぞれにweightを追加して、トラフィックの割合を指定します。

これを適用して、複数回curlでアクセスしてみます。

for i in $(seq 1 10); do curl -H Host:foo.example.com ${GATEWAY_IP}/foo; done 
foo foo-v1-6c476649d9-km6k4
foo foo-v1-6c476649d9-km6k4
foo foo-v1-6c476649d9-km6k4
foo foo-v2-9c5644495-vlzvx
foo foo-v1-6c476649d9-km6k4
foo foo-v1-6c476649d9-km6k4
foo foo-v1-6c476649d9-km6k4
foo foo-v2-9c5644495-vlzvx
foo foo-v1-6c476649d9-km6k4
foo foo-v1-6c476649d9-km6k4

20%の割合でv2にトラフィックが流れていることが確認できます。
最後にv1側のweightを0、v2を100にすればリリース完了です(もちろんv1側は削除しても構いません)。

apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: http
namespace: foo
spec:
parentRefs:
- name: gateway
namespace: istio-ingress
sectionName: default
hostnames: ["foo.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /foo
backendRefs:
- name: foo-v1
port: 80
weight: 0
- name: foo-v2
port: 80
weight: 100

これで全てのトラフィックはv2側に流れます(自明なので確認結果は省略します)。

Flaggerでカナリアリリースを自動化する

ここでは手動でカナリアリリースを試しましたが、プログレッシブデリバリーツールのFlaggerでもGateway APIの対応が進められています。
実際にカナリアリリースをする際は、Flaggerを利用した方がより確実なリリースを実施できると思います。
今回は試してはいませんが、興味のある方は以下のドキュメントを参照してください。

Flaggerを利用したカナリアリリースは本サイトでも紹介記事がありますので、そちらもぜひご参考ください。

まとめ

#

Gateway APIは責務に応じてリソースが分割されたことで、ある程度の規模のクラスタ構成で運用効率が大きく改善されると思います。
また、カナリアリリース等、Ingressにはない仕様が多く盛り込まれ、よりモダンなAPIになってきた感があります。
今後HTTPS以外のプロトコルについても順次実装が進んでくると、多くのユースケースで適用可能になり、より注目度が高まってくると考えられます。

ただし、Gateway APIのFAQによると、現状Gateway APIでIngressを置き換える予定はないようです。

Q: Will Gateway API replace the Ingress API?
A: No. The Ingress API is GA since Kubernetes 1.19. There are no plans to deprecate this API and we expect most Ingress controllers to support it indefinitely.

引用元: https://gateway-api.sigs.k8s.io/faq/

とはいえ、Gateway APIがベータ版となったことで、今後サポートする製品はもっと増えてくると思います。
そうなってくると、IngressではなくGateway APIを採用する事例が増えてくることは間違いないかと思います。
今後もGateway APIの動向には注目です。


参照資料


  1. minikubeのセットアップはこちらの記事を参照してください。 ↩︎

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)
  2. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  3. GitHub Codespaces を使いはじめる (2022-05-18)
  4. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  5. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  6. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  7. Nuxt3入門(第3回) - ユニバーサルフェッチでデータを取得する (2022-10-06)
  8. 第1回 OpenAPI Generator を使ったコード生成 (2022-06-04)
  9. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  10. Nuxt3入門(第2回) - 簡単なNuxtアプリケーションを作成する (2022-10-02)