LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

〜運用しやすいプレビュー環境を求めて〜 Gateway APIで作るサービスメッシュレスなプレビュー環境

みなさん、プレビュー環境してますか?どうも、かたいなかです。

以前、記事や登壇でIstioベースのPreview環境の構築方法をご紹介しました。

made.livesense.co.jp

外向けに発表したものの、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ごとにアプリケーションをビルドしてデプロイできるようにする必要があります。

https://cacoo.com/diagrams/m0f0lxsmdYwiIrPN-0C65C.png

また、後述するヘッダによるルーティングの設定を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に応じてプレビュー環境用のサーバにリクエストが届くようになります。

https://cacoo.com/diagrams/m0f0lxsmdYwiIrPN-95C28.png

余談ですが、モノリシックなアプリケーションにはこのようなヘッダ伝播に関する考慮は不要です。ロードバランサ等の設定で適切にプレビュー環境ごとのURLでアクセスできるようにしてやれば良いです。ヘッダ伝播は呼び出されたURLの情報をマイクロサービスの下流まで届け、ルーティングに用いるために必要な設定だからです。

補足: ヘッダによるルーティングとGateway API

伝播されてきたヘッダをルーティングするために、使えるツールとしては様々なプロキシやロードバランサの実装が考えられますが、プレビュー環境を作る上ではGateway APIの実装が都合が良いことが多いです。

プレビュー環境では、PRごとにバラバラにルーティング設定がデプロイされることになるため、デフォルトのルートとPRごとにデプロイされたルートとでどれを優先するかが問題になります。

このとき、Gateway APIでは複数のルーティングの設定が同じホスト名を対象にしていたときに優先順位がどうなるかが仕様によって決まっているため、仕様を満たしているかだけを確認すればよいです。以下がそのような仕様の例です。

gateway-api.sigs.k8s.io

Datadog APMとEnvoy Gatewayを使用した実装

この記事ではDatadog APM, Envoy Gateway, ArgoCDを使用し、サービスメッシュに依存しない方法でのプレビュー環境の実現方法を紹介します。

具体的には以下のような構成要素からプレビュー環境を組み立てていきます。

  • ArgoCD ApplicationSetでPRごとにアプリケーションやルーティングをデプロイ
  • Envoy Gatewayがヘッダによりルーティング
  • Datadog APMが各マイクロサービスで下流にヘッダを伝播
  • ot-baggage-branchヘッダ挿入用プロキシ

https://cacoo.com/diagrams/m0f0lxsmdYwiIrPN-C43F5.png

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さんの記事を参考にさせていただきました。

kencharos.hatenablog.com

また、今回はEnvoy Gatewayを採用しましたが、将来的にはEKSからALBの設定を管理するためのLoad Balancer ControllerがGateway APIに対応すると、ALBによってルーティングが実現できて嬉しそうだと思っています。

github.com

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を仕込みました。

github.com

デモ

今回の検証用アプリケーションは、フロントエンドサーバが名前の文字列 (mainブランチでは world)をバックエンドサーバに送信し、バックエンドサーバは挨拶の文字列(mainブランチでは Hello)を付け加えてフロントに返すというものです。フロントエンドサーバはバックエンドから返された文字列をそのままクライアントに返します。

https://cacoo.com/diagrams/m0f0lxsmdYwiIrPN-9764C.png

なお、検証はローカルクラスタ上で行うため、/etc/hosts で特定のドメイン名をローカルホストに向けて検証しています。

まずはPRを作成していない状態でアクセスしてみます。

すると、Hello world という文字列が出力されました。フロントエンド及びバックエンドのサーバが両方ともmainブランチからビルドされた状態だとこのような実装になっています。

次にバックエンドサーバでHelloの文字をHiに修正してPRを作成し、イメージがビルドおよびデプロイされるまでしばらく待ちます。その状態でPRのブランチに対応したURLを使ってアクセスします。すると、以下のように確かに挨拶の文字列が Hello から Hi に書き換わっており、PRからビルドされたイメージに対してトラフィックが流れていることがわかります。

最後にフロントエンドサーバでworldkatainakaに、バックエンドサーバで HelloYoに修正し、それぞれ yo-katainaka というブランチ名でPRを作ってみます。すると、以下のようにフロントエンド、バックエンドそれぞれの修正が反映されていることがわかります。複数サーバを同時に検証できるのは、API仕様の変更の際などにはやめに結合して検証するときなどに便利そうです。

まとめ

プレビュー環境を実現する上でIstioは大きな武器になりうるのですが、現実的な運用コストや学習コストの高さに尻込みしてしまっていました。最近になってGateway APIの実装が増えてきたことで、プレビュー環境の実現可能性が高まってきたように感じています。

将来的にはLoad Balancer ControllerでのGateway API対応などによって、更にEKSで構築したシステムでのプレビュー環境が現実的になってくることを期待したいです。

参考