LinkerdのGateway API実装(HTTPRoute)を触ってみる
この記事は Kubernetes Advent Calendar 2023 の 15日目(シリーズ2)の記事です。
Gateway APIとは
Gateway API は、Ingress API のより表現力豊かな次世代バージョンに似ていると考えることができます。
「表現力豊か」であることによってIngressの課題を克服することを目指しています。
- クラスターへ関与する役割(インフラ構築・クラスター運用・アプリケーション開発)ごとに関心事を分離できていない
- 固有の実装にアノテーションが多用されて互換性が低い
Ingressの進化版であるため元々はnorth/south(クラスター外から内へ、内から外へ)トラフィックの管理を目的としていましたが、サービスメッシュユーザーからの関心の高まりによってeast/west(クラスター内のワークロード間)トラフィックにも利用できるようにする仕様策定が進んでいます。
Kubernetes Advent Calendar 2023 の 1日目の記事にて、Gateway APIの最新動向について総括されています。
今回はLinkerdによる実装を通じて、Gateway APIのeast/westユースケースを体験してみます。
下記リポジトリを手元にcloneして同じ内容を追体験することもできます。
Linkerdすこし紹介
LinkerdはKubernetes向けサービスメッシュのひとつです。
この分野で有名なプロダクトといえばIstioですが、LinkerdはIstioと同じくCNCFのプロジェクトで、Istioより2年早くサービスメッシュの中で最初にgraduated projectに認定されています。
Linkerdは設計原則に表される通り「シンプルであること」を追及しているのが特徴です。
- シンプルにせよ。Linkerdは認知的オーバーヘッドが低く、運用がシンプルである必要があります。運用者は最小限の魔法で、そのコンポーネントが明確であり、その動作が理解可能かつ予測可能であると思える必要があります。
- リソース要件を最小化せよ。Linkerdは特にデータプレーン層(Podに注入されるサイドカープロキシ)において、パフォーマンスとリソースのコストを可能な限り最小限に抑える必要があります。
- ただ機能せよ。Linkerdは既存のアプリケーションを壊してはならず、また、使い始めたり単純なことを実行したりするために複雑な構成※を必要とするべきではありません。
セットアップ
事前準備としてLinkerdがインストールされたローカルクラスターを用意します。
Linkerd CLIに加えて、以下のツールを使います。
- https://github.com/k3d-io/k3d
- https://github.com/casey/just
- https://github.com/smallstep/cli
- https://github.com/helm/helm
- https://github.com/helmfile/helmfile
全部いっぺんにインストールする
aquaがインストールされていれば、リポジトリのルートでaqua i
を実行すればすべてのツールがインストールできます。
クラスター作成
just create-cluster
k3dで作ったクラスターにはtraefik Ingressコントローラーがついてくるため、Ingressオブジェクトを作ることでPodにリクエストを送るときkubectl port-forward
しなくて済みます。
Ingressにはlocalhost:54321
でリクエストできるようになっています。
CRDインストール
LinkerdはCRDの取り扱いについて、Helmのベストプラクティスで推奨される二通りの方法のうちCRDのチャートを分離する方法を取っています。
just apply linkerd-crds
Linkerdインストール
just ca
just issuer
just apply linkerd-control-plane
Viz extensionインストール
just apply linkerd-viz
HTTPRouteリソース
east/westユースケースにおいてはクラスターへの入口を表現するGateway
やGatewayClass
リソースは利用されないため、主にServiceへのルーティングを定義するXRoute
リソースが主役になります。
※XRoute
というリソースがあるのではなく、HTTPRoute
/ TLSRoute
/ TCPRoute
/ UDPRoute
があります。
この中でLinkerdが実装しているHTTPRoute
を触ってみます。
Linkerdをインストールすると、2種類のAPIグループでHTTPRoute CRDが使えるようになります。
gateway.networking.k8s.io
policy.linkerd.io
前者はGateway APIのアップストリーム準拠、後者はGateway Enhancement Proposals(GEPs)は出ているが、まだ仕様が固まっていない機能が先んじて盛り込まれた実験版になります。
LinkerdのHTTPRouteを触ってみる
チュートリアルに沿って試していきます。
過程で作成するリソースや実行するコマンドについて、より簡単に繰り返し試せるように、多少読み替えつつlinkerd-installationリポジトリに納めています。
以下、このリポジトリを使ってセットアップした前提で進めていきます。
Fault injectionをやってみる
なにができるのか?
HTTPRouteリソースを使用してトラフィックの一部を特定のバックエンドにリダイレクトすることで、アプリケーションに障害を簡単に注入できます。
サンプルアプリケーションのデプロイ
以下の構成をもつサンプルを利用し、books
バックエンドへのアクセスに障害を注入してみます。
just booksapp
authors
サービスはデフォルトで一定の失敗が発生するように設定されています(FAILURE_RATE
環境変数)が、これを常に成功するように変更します。
env:
- name: DATABASE_URL
value: sqlite3:db/db.sqlite3
- name: BOOKS_SITE
value: http://books:7002
- name: FAILURE_RATE
value: "0.5"
kubectl patch
で環境変数を削除することで常に成功するようになります。
kubectl -n booksapp patch deploy authors \
--type='json' \
-p='[{"op":"remove", "path":"/spec/template/spec/containers/0/env/2"}]'
patchしてしばらくするとsuccess rateが100%になることを確認できます。
linkerd viz -n booksapp stat deploy
NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN
authors 1/1 100.00% 4.3rps 3ms 42ms 82ms 8
books 1/1 100.00% 5.7rps 6ms 70ms 94ms 8
traffic 1/1 100.00% 0.3rps 1ms 1ms 1ms 2
webapp 3/3 100.00% 5.9rps 21ms 64ms 93ms 10
ダッシュボードでも同様に確認できます(linkerd-viz.local:54321
でアクセスできます)。
障害を再現するバックエンドの用意
500を常に返すよう設定されたNGINXをデプロイします。
kubectl apply -f booksapp/failure.yaml
HTTPRouteの作成
kubectl apply -f booksapp/fault-injection-httproute.yaml
books
行きのリクエストのうち、10%のリクエストを500を返すNGINXに送るように定義されています。
エラーの確認
webapp
から発されたリクエストの成功率を確認してみると以下のようになります。
linkerd viz stat -n booksapp deploy --from deploy/webapp
NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN
authors 1/1 95.58% 4.2rps 5ms 45ms 81ms 3
books 1/1 100.00% 5.9rps 8ms 77ms 95ms 6
error-injector 1/1 0.00% 0.6rps 1ms 2ms 2ms 3
books 100%?
コマンドの出力はwebapp
から受け取ったリクエストに対して各Deployment自身が返したレスポンスの成功率なので、
-
authors
: 裏のbooks
に送ったリクエストが一部NGINXに飛ばされるため100%にならない-
books
にリクエストを送る経路はauthors
からだけではないため90%より高い
-
-
books
: ここまで届いたリクエストには正常にレスポンスが返されるため100% -
error-injector
: 常に500を返しているため0%
経路が二股に分かれますが結局はwebapp
から出発するため、webapp
の成功率を確認すると約90%になっています。
linkerd viz stat -n booksapp deploy/webapp
NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN
webapp 3/3 90.07% 8.9rps 19ms 77ms 95ms 10
backendRefsとweightの他の使い道
Fault injection以外のシナリオとして、backendRefs
にbooks
の異なるバージョンを定義すればcanary releaseになります。
タイムアウトを設定してみる
なにができるのか?
Serviceの親を持つアウトバウンドHTTPRoutesに対して、GEP-1742で指定されているタイムアウトをサポートします。
サンプルアプリケーションのデプロイ
ドキュメントには一気通貫の完全なチュートリアルがありませんが、booksappを使って試せます。
Fault Injectionのときと同じにkubectl patch
でauthorsサービスが常に成功する状態を作り、成功率100%であることを確認します。
linkerd viz -n booksapp stat deploy
NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN
authors 1/1 100.00% 6.7rps 3ms 33ms 45ms 6
books 1/1 100.00% 8.8rps 5ms 69ms 94ms 7
error-injector 1/1 100.00% 0.3rps 1ms 1ms 1ms 2
traffic 1/1 100.00% 0.3rps 1ms 1ms 1ms 2
webapp 3/3 100.00% 8.8rps 20ms 73ms 95ms 11
HTTPRouteの作成
authors
へのアクセスにタイムアウトを設けてみます。
kubectl apply booksapp/timeouts-httproute.yaml
request
とbackendRequest
の違いは以下の通りです。
-
request
-
プロキシがリクエストを受信したときに開始され、成功した応答がクライアントに送信されたときに終了します。
- =自分(この場合
authors
)がリクエストを受信してから自分のクライアントに返事するまで
- =自分(この場合
-
-
backendRequest
-
リクエストがバックエンドにディスパッチされたときに開始され、そのバックエンドから応答を受信したときに終了します。
- =自分のバックエンド(この場合
books
)にリクエストしてから自分が返事をもらうまで
- =自分のバックエンド(この場合
-
webapp
からauthors
へのリクエストが一部504で返ってくるようになるため、webapp
の成功率が下がることが確認できます。
linkerd viz -n booksapp stat deploy
NAME MESHED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99 TCP_CONN
authors 1/1 100.00% 0.4rps 1ms 9ms 10ms 6
books 1/1 100.00% 0.4rps 1ms 4ms 4ms 5
error-injector 1/1 100.00% 0.3rps 1ms 2ms 2ms 2
traffic 1/1 100.00% 0.3rps 1ms 2ms 2ms 2
webapp 3/3 11.41% 10.5rps 5ms 10ms 87ms 11
Dynamic request routingをやってみる
なにができるのか?
リクエストヘッダーの内容に基づいてHTTPトラフィックをルーティングできます。これは、A/B テストやその他のトラフィック管理戦略の実行に役立ちます。
「その他のトラフィック管理戦略」の一つはcanary releaseで、weight
による無差別な振り分けと違い、狙ったリクエストを流し分けることができます。
ここではpodinfoアプリケーションを利用して1つのフロントエンドと2つのバックエンドが存在する環境を作り、バックエンドへのリクエストをHTTPRouteによって送り分けます。
サンプルアプリケーションのデプロイ
just apply backend-a
just apply backend-b
just apply frontend
backend aとbackend bはリクエストが届いたとき自分の名前を答えるように設定されています。
frontendはbackend-a-podinfo
Serviceにリクエストを送るように設定されています。
リクエストを送ってみる
この段階ではbackend bにリクエストが届くことはありません。
curl -sX POST podinfo.local:54321/echo | grep -o 'PODINFO_UI_MESSAGE=. backend'
PODINFO_UI_MESSAGE=A backend
HTTPRouteの作成
kubectl apply -f podinfo/httproute.yaml
HTTPRouteにより、backend-a-podinfo
Service行きのリクエストに分岐を追加しています。
x-request-id
ヘッダーの値がalternative
である場合、リクエストはbackend-a-podinfo
ではなくbackend-b-podinfo
に送られるようになります。
リクエストを送ってみる
さっきと同じリクエストは引き続きbackend aに届きます。
curl -sX POST podinfo.local:54321/echo | grep -o 'PODINFO_UI_MESSAGE=. backend'
PODINFO_UI_MESSAGE=A backend
x-request-id: alternative
ヘッダーを付けたリクエストはbackend bに届きます。
curl -sX POST -H 'x-request-id: alternative' \
podinfo.local:54321/echo | grep -o 'PODINFO_UI_MESSAGE=. backend'
PODINFO_UI_MESSAGE=B backend
x-request-id
ヘッダーがあっても値が違うとbackend aに届きます。
curl -sX POST -H 'x-request-id: foo' \
podinfo.local:54321/echo | grep -o 'PODINFO_UI_MESSAGE=. backend'
PODINFO_UI_MESSAGE=A backend
Authorization policyを定義してみる
なにができるのか?
Serviceレベルで認可を強制するだけでなく、個々のHTTPルートに対してより詳細な認可ポリシーを構成することもできます。
今回はHTTPRoute
以外のカスタムリソースServer
/ AuthorizationPolicy
/ NetworkAuthentication
/ MeshTLSAuthentication
も登場します。
サンプルアプリケーションのデプロイ
booksappをデプロイし、kubectl patch
でauthors
のFAILURE_RATE
環境変数を消しておきます。
アプリの構成をおさらいすると、authors
サービスに対してwebapp
とbooks
はいずれもクライアントの立場になります。
ただしauthors
に送るリクエストの内容はそれぞれ異なり、
books
サービスはGET
リクエストを/authors/:id.json
にのみ送信する必要があります。
一方webapp
サービスは、さらにDELETE
リクエストとPUT
リクエストを/authors
に送信し、POST
リクエストを/authors.json
に送信することもあります。
今はまだサービス間の通信に何のポリシーも課していないため、authors
に対して未認証のすべてのリクエストとprobeのためのリクエストが許可されています。
linkerd viz authz -n booksapp deploy/authors
ROUTE SERVER AUTHORIZATION UNAUTHORIZED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99
default default:all-unauthenticated default/all-unauthenticated 0.0rps 100.00% 6.8rps 3ms 42ms 66ms
probe default:all-unauthenticated default/probe 0.0rps 100.00% 0.3rps 1ms 1ms 1ms
Serverの作成
ServerはselectorにマッチしたPodを指すという点でServiceに似たリソースです。基本的に単体で使うものではなく、HTTPRouteやAuthorizationPolicyから参照されます。
authors
のPodを指すServerを作ります。
kubectl apply -f booksapp/server.yaml
Serverが存在すると、明示的に許可するまで対象Podへのトラフィックは拒否されるため、authors
にはリクエストできない状態になりました。
linkerd viz authz -n booksapp deploy/authors
ROUTE SERVER AUTHORIZATION UNAUTHORIZED SUCCESS RPS LATENCY_P50 LATENCY_P95 LATENCY_P99
default authors-server 5.1rps 0.00% 0.0rps 0ms 0ms 0ms
probe authors-server default/probe 0.0rps 100.00% 0.1rps 3ms 3ms 3ms
default default:all-unauthenticated default/all-unauthenticated 0.0rps 100.00% 0.1rps 1ms 1ms 1ms
probe default:all-unauthenticated default/probe 0.0rps 100.00% 0.1rps 1ms 1ms 1ms
ブラウザでアクセスしてみてもInternal Server Errorが表示されます。
ここからルート別に通信を許可するポリシーを作成していきます。
HTTPRouteの作成
authors
の特定パスへのGETリクストとマッチするHTTPRouteを作ります。
kubectl apply -f booksapp/authors-get-route.yaml
AuthorizationPolicy / MeshTLSAuthenticationの作成
ここまでに定義したServerとルートについて通信を許可するポリシーを作成します。
requiredAuthenticationRefs
で指定した条件を満たす場合、HTTPRouteで定義した経路でauthors
にリクエストを送ることが許可されます。
kubectl apply -f booksapp/authors-get-policy.yaml
ここでの条件はServiceAccountがbooks
かwebapp
であることです。
authorsのreadiness probeを通す
authors
のメインコンテナにはreadiness probeが定義されていますが、現在は通らなくなっています(明示的な許可がないため)。
readinessProbe:
failureThreshold: 3
httpGet:
path: /ping
port: 7002
scheme: HTTP
これを通すためのルートとポリシーを作ります。
まずprobeとマッチするHTTPRouteを定義し、
AuthorizationPolicyでこのルートを許可します。
許可の条件をNetworkAuthenticationで定義します。
Probeの送信元はメッシュされていないkubeletのため、MeshTLSAuthenticationではなくNetworkAuthenticationを使い、ローカルネットワークからの通信を許可します。
kubectl apply -f booksapp/probe-route-policy.yaml
ここまでくるとトップページは正常に見えるようになります。
しかし、著者の削除ボタンを押すとNot found
が表示されます。
一部パスへのGET
を許可するポリシーしかまだ作成していないため、著者の作成や削除といった操作はエラーを引き起こします。
webapp->authors 変更操作の許可
booksappが正常に機能できるように最後のルートとポリシーを定義します。
kubectl apply -f booksapp/authors-modify-route.yaml
authors-server
へのDELETE
/ PUT
/ POST
リクエストにマッチさせています。
kubectl apply -f booksapp/authors-modify-policy.yaml
books
からauthors
へはDELETE
/ PUT
/ POST
を行わないため、今回のAuthorizationPolicyではwebapp
ServiceAccountのみを許可します。
著者を追加してみると、成功するようになりました。
Discussion