みなさん、プレビュー環境してますか?どうも、かたいなかです。
以前、記事や登壇でIstioベースのPreview環境の構築方法をご紹介しました。
外向けに発表したものの、Istioの運用工数や学習コストがネックとなってしまい、実際の転職会議の開発環境の導入にはいたっていませんでした。
最近になってGateway APIの実装例も増えてきて、Istio以外にもプレビュー環境でのヘッダを元にしたルーティングの実現において、現実的な選択肢となりそうなツールが増えてきました。そこで、Gateway APIのEnvoyによる実装であるEnvoy Gatewayを用いて、サービスメッシュを使用しないプレビュー環境の構築を試してみたため、この記事では構成例をご紹介します。
なお、今回の記事の中ではプレビュー環境の説明等について前回の記事と同様の説明を再度する箇所があるため、前回の記事をすでに読んでいただいた方には冗長に感じられる部分もありますがご了承ください。
プレビュー環境とは(おさらい)
ここでのプレビュー環境とは、Pull Requestがマージされる前に、そのブランチからビルドされたアプリケーションを、ブランチ名などを元にしたURLで動作確認できるようにしたものです。
Wantedly社やメルカリ社で実現されているのが有名です。
www.wantedly.com engineering.mercari.com
プレビュー環境を作る上で比較的難しいのが、BFF等を挟んで後ろにいるバックエンドのサーバでのプレビュー環境の実現です。後ろにいるサーバを呼び出すのは手前にいる別のサーバであり、URLの情報は失われているため、URLをもとにそのままルーティングするわけにはいきません。
そのため、ヘッダ伝播等のしくみを使ってどのサーバにリクエストを送るべきかの情報を引き渡していく必要があります。今回の記事では、転職会議ではDatadog APMを使用していることを念頭に、Datadog APMとot-baggage-branch
ヘッダを用いたヘッダ伝播の構成についても紹介します。
プレビュー環境の構成要素(おさらい)
PRごとの環境を作成するのに一般的に以下の2つが必要だと考えています。
- PRごとのアプリケーションやルーティングの設定のデプロイ
- ヘッダ伝播およびヘッダによるルーティング
それぞれ説明していきます。
PRごとのアプリケーションやルーティングの設定のデプロイ
プレビュー環境作成にあたって、まずはPRごとにアプリケーションをビルドしてデプロイできるようにする必要があります。
また、後述するヘッダによるルーティングの設定をPRごとに適用するため、ルーティングの設定に関してもPRごとに適用できる必要があるでしょう。
例えば、Kubernetes上であればArgoCDのApplicationSetやmercari社が発表したKube Tempuraを使うとPRごとのデプロイを実現できます。また、もっと単純にPRでのCI実行時にステージング環境等にデプロイしてしまうことでも実現できるでしょう。
ヘッダ伝播とヘッダによるルーティング
PR単位で動作確認を行いたいのは、クライアントから直接アクセスされるサーバだけではありません。BFF等の後ろにいるサーバも対象になります。
呼び出し階層の下流にいるサーバに対してのプレビュー環境を実現するには、リクエストがどのURLに対するものなのかの情報が、下流のサーバまで渡されなければなりません。
このような情報を受け渡すには、アプリケーションに受け取ったヘッダを下流に伝播させる必要があります。
実現方法としては、W3CからBaggageヘッダによる仕様が提案されています。それとは別にDatadog APMでも ot-baggage-<name>
というヘッダで情報を伝播させていくことができます。
ot-baggage-branch: pr1
これを用いることで、Nginx等のプロキシでPRごとのURLを ot-baggage-branch
ヘッダに変換してやれば、あとはDatadog APMが仕込まれている各マイクロサービスが ot-baggage-branch
ヘッダを下流に伝播させていってくれます。そして、このヘッダをもとにサービスメッシュやロードバランサ等でルーティングしてやることで、バックエンドのサーバでもアクセスしたURLに応じてプレビュー環境用のサーバにリクエストが届くようになります。
余談ですが、モノリシックなアプリケーションにはこのようなヘッダ伝播に関する考慮は不要です。ロードバランサ等の設定で適切にプレビュー環境ごとのURLでアクセスできるようにしてやれば良いです。ヘッダ伝播は呼び出されたURLの情報をマイクロサービスの下流まで届け、ルーティングに用いるために必要な設定だからです。
補足: ヘッダによるルーティングとGateway API
伝播されてきたヘッダをルーティングするために、使えるツールとしては様々なプロキシやロードバランサの実装が考えられますが、プレビュー環境を作る上ではGateway APIの実装が都合が良いことが多いです。
プレビュー環境では、PRごとにバラバラにルーティング設定がデプロイされることになるため、デフォルトのルートとPRごとにデプロイされたルートとでどれを優先するかが問題になります。
このとき、Gateway APIでは複数のルーティングの設定が同じホスト名を対象にしていたときに優先順位がどうなるかが仕様によって決まっているため、仕様を満たしているかだけを確認すればよいです。以下がそのような仕様の例です。
Datadog APMとEnvoy Gatewayを使用した実装
この記事ではDatadog APM, Envoy Gateway, ArgoCDを使用し、サービスメッシュに依存しない方法でのプレビュー環境の実現方法を紹介します。
具体的には以下のような構成要素からプレビュー環境を組み立てていきます。
- ArgoCD ApplicationSetでPRごとにアプリケーションやルーティングをデプロイ
- Envoy Gatewayがヘッダによりルーティング
- Datadog APMが各マイクロサービスで下流にヘッダを伝播
ot-baggage-branch
ヘッダ挿入用プロキシ
- GitHub - katainaka0503/grpc-pr-env-test-kubernetes
- GitHub - katainaka0503/grpc-pr-env-test-frontend
- GitHub - katainaka0503/grpc-pr-env-test-backend
ArgoCD ApplicationSetでPRごとにアプリケーションやルーティングをデプロイ
ArgoCD ApplicationSetのPull Request Generatorを使うとPRごとに同じような設定を複製し、Kubernetesクラスタにデプロイすることができます。今回はこれを用いてアプリケーションおよびEnvoy Gatewayを用いたヘッダでのルーティング設定をデプロイします。
Envoy Gatewayがヘッダによりルーティング
Envoy Gatewayにより、ot-baggage-branch
ヘッダで指定されたブランチからデプロイされている場合はそちらに、それ以外の場合はメインブランチのアプリケーションにルーティングされるように設定します。複数のアプリケーションで同じブランチ名を使っていれば、それぞれのアプリケーションがPRからデプロイされた状態で動作確認を行うことも可能です。
まずは、マイクロサービス間の通信がEnvoy Gatewayを通過するようにします。
公式ドキュメントに従ってEnvoyGatewayをインストールし、以下のような設定でGatewayを作成します。
apiVersion: gateway.networking.k8s.io/v1beta1 kind: Gateway metadata: name: eg spec: gatewayClassName: eg listeners: - name: http protocol: HTTP port: 80
すると、envoy-gateway-system
というnamespaceに envoy-default-eg-XXXXXX
というような名前の Serviceが生えます。
このServiceに通信を向けてやることでEnvoy Gatewayで通信をルーティングさせることができます。今回は、以下のようなExternalName Serviceを作成してやることで、アプリケーションからの通信先をこのServiceに向けることとしました。
apiVersion: v1 kind: Service metadata: name: frontend-gateway spec: type: ExternalName externalName: envoy-default-eg-XXXXXX.envoy-gateway-system.svc.cluster.local ## GatewayのServiceのFQDN
この状態で、以下のようなルーティング設定をPRごとにApplicationSetで適用することで、ot-baggage-branch
ヘッダでルーティングできるようにします。
apiVersion: gateway.networking.k8s.io/v1alpha2 kind: GRPCRoute metadata: name: "${ARGOCD_ENV_BRANCH_NAME}-${ARGOCD_ENV_APP_NAME}" spec: parentRefs: - name: eg hostnames: - "${ARGOCD_ENV_APP_NAME}-gateway.default.svc.cluster.local" - "${ARGOCD_ENV_APP_NAME}-gateway.default" - "${ARGOCD_ENV_APP_NAME}-gateway" rules: - matches: - headers: - type: Exact name: ot-baggage-branch value: "${ARGOCD_ENV_BRANCH_NAME}" backendRefs: - name: "${ARGOCD_ENV_BRANCH_NAME}-${ARGOCD_ENV_APP_NAME}" port: 80
今回は、サンプルアプリケーションでの通信がgrpcによって行われている関係で GRPCRoute
を使っていますが、普通のHTTPで通信したい場合は HTTPRoute
を使えばよいです。
なお、このようなkustomizeで管理されたYAMLの中で環境変数を用いる方法についてはkencharosさんの記事を参考にさせていただきました。
また、今回はEnvoy Gatewayを採用しましたが、将来的にはEKSからALBの設定を管理するためのLoad Balancer ControllerがGateway APIに対応すると、ALBによってルーティングが実現できて嬉しそうだと思っています。
Datadog APMが各マイクロサービスで下流にヘッダを伝播
アプリケーションにはDatadog APMを組み込み、ot-baggage-branch
ヘッダが伝播されるようにしています。これにより、後ろのマイクロサービスでも特定のブランチからデプロイされたアプリケーションにルーティングさせることができます。
今回のgoのgrpcサーバを使った例ではサーバ側、クライアント側それぞれ以下のような実装を入れることで自然とBaggageヘッダが伝播されるようになります。
// import import ( ... grpctrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/google.golang.org/grpc" ... ) // サーバクライアント共通 tracer.Start() defer tracer.Stop() // サーバ側 s := grpc.NewServer(grpc.UnaryInterceptor(grpctrace.UnaryServerInterceptor()), grpc.StreamInterceptor(grpctrace.StreamServerInterceptor())) // クライアント側 conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(grpctrace.UnaryClientInterceptor()), grpc.WithStreamInterceptor(grpctrace.StreamClientInterceptor()))
ot-baggage-branch
ヘッダ挿入用プロキシ
プレビュー環境にアクセスしたときに、まずトラフィックが流されるNginxのプロキシで、URLから ot-baggage-branch
ヘッダに変換します。これによりユーザはURLによって簡単に動作確認する対象のPRを切り替えられるようになっています。
server { datadog_resource_name "pr-env-proxy"; listen 80 http2; # 正規表現でbaggageヘッダとして使う値を取得 server_name ~^(?<branch>.+)\.katainaka\.org$; location / { grpc_pass grpc://frontend-gateway; grpc_set_header ot-baggage-branch ${branch}; } }
なお、dd-traceが ot-baggage-branch
ヘッダを認識してくれるのは上流からトレース情報とともに流れてきたときのみのようなので、NginxにDatadogのmoduleを仕込みました。
デモ
今回の検証用アプリケーションは、フロントエンドサーバが名前の文字列 (mainブランチでは world
)をバックエンドサーバに送信し、バックエンドサーバは挨拶の文字列(mainブランチでは Hello
)を付け加えてフロントに返すというものです。フロントエンドサーバはバックエンドから返された文字列をそのままクライアントに返します。
なお、検証はローカルクラスタ上で行うため、/etc/hosts
で特定のドメイン名をローカルホストに向けて検証しています。
まずはPRを作成していない状態でアクセスしてみます。
すると、Hello world
という文字列が出力されました。フロントエンド及びバックエンドのサーバが両方ともmainブランチからビルドされた状態だとこのような実装になっています。
次にバックエンドサーバでHello
の文字をHi
に修正してPRを作成し、イメージがビルドおよびデプロイされるまでしばらく待ちます。その状態でPRのブランチに対応したURLを使ってアクセスします。すると、以下のように確かに挨拶の文字列が Hello
から Hi
に書き換わっており、PRからビルドされたイメージに対してトラフィックが流れていることがわかります。
最後にフロントエンドサーバでworld
をkatainaka
に、バックエンドサーバで Hello
をYo
に修正し、それぞれ yo-katainaka
というブランチ名でPRを作ってみます。すると、以下のようにフロントエンド、バックエンドそれぞれの修正が反映されていることがわかります。複数サーバを同時に検証できるのは、API仕様の変更の際などにはやめに結合して検証するときなどに便利そうです。
まとめ
プレビュー環境を実現する上でIstioは大きな武器になりうるのですが、現実的な運用コストや学習コストの高さに尻込みしてしまっていました。最近になってGateway APIの実装が増えてきたことで、プレビュー環境の実現可能性が高まってきたように感じています。
将来的にはLoad Balancer ControllerでのGateway API対応などによって、更にEKSで構築したシステムでのプレビュー環境が現実的になってくることを期待したいです。
参考
- 実装例のコード
使用したツール等
参考にした記事