LIVESENSE ENGINEER BLOG

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

Pull Requestをすぐ動作確認! マイクロサービスでのプレビュー環境の作り方

こんにちは、かたいなかです。

最近、マイクロサービスアーキテクチャを採用した環境でプレビュー環境の実現方法についていくつかのパターンを比較し整理する機会がありました。

今回の記事では、プレビュー環境を構築するための要件をなるべく特定の技術に依存せずに紹介したあとで、ArgoCD、Istio、OpenTelemetryを使用した実装例をご紹介します。

目次

プレビュー環境とは

ここでのプレビュー環境とは、Pull Requestがマージされる前にPRからビルドされたアプリケーションを特定のURLで動作確認できるようにしたものです。レビュー等を待つことなくステージングのデータベースや他のマイクロサービスと結合した状態で動作確認できるようにすることで、開発速度の向上が期待できます。

Wantedly社メルカリ社で実現されているのが有名です。

www.wantedly.com engineering.mercari.com

プレビュー環境を作る上で比較的難しいのが、BFF等を挟んで後ろにいるバックエンドのサーバでのプレビュー環境の実現です。後ろにいるサーバではURLをもとにそのままルーティングするわけにはいかないので、ヘッダ伝播等のしくみを使ってどのサーバにリクエストを送るべきかの情報を引き渡していく必要があります。今回の記事ではそういったヘッダ伝播についても解説します。

プレビュー環境の構成要素

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ヘッダによる仕様が提案されています。Baggageヘッダは以下のようなヘッダです。

baggage: branch=pr1

このBaggageヘッダの伝播についてはOpenTelemetry SDKによる実装が提供されています

これを用いて、Nginx等のプロキシでPRごとのURLをBaggageヘッダに変換しつつ、Baggageヘッダを伝播させます。そして、このヘッダをもとにサービスメッシュやロードバランサ等でルーティングしてやることで、バックエンドのサーバでもアクセスしたURLに応じてプレビュー環境用のサーバにリクエストが届くようになります。

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

Baggage以外の方法としてはカスタムヘッダを伝播するように自分たちで実装する事も考えられます。

なお、モノリシックなアプリケーションにはこの設定は不要であり、ロードバランサ等の設定で適切にプレビュー環境ごとのURLでアクセスできるようにしてやれば良いです。この設定はマイクロサービスでマイクロサービス間の通信をルーティングするために必要になる設定だからです。

実装例

この記事では実装例としてArgoCD、Istio、OpenTelemetryを使用した実装例を紹介します。

ArgoCDがPRごとにアプリケーションやルーティングの設定を適用する部分を担当し、IstioとOpenTelemetryがヘッダ伝播およびヘッダによるルーティングを担当します。

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

ArgoCD ApplicationSet

ArgoCD ApplicationSetのPull Request Generatorを使うとPRごとに同じような設定を複製しKubernetesクラスタにデプロイすることができます。今回はこれを用いてアプリケーションおよびIstioを用いたヘッダでのルーティング設定をデプロイします。

Istio

Istioにより、Baggageヘッダで指定されたブランチからデプロイされている場合はそちらに、それ以外の場合はステージングのアプリケーションにルーティングされるように設定します。同じブランチ名を用いることで複数のアプリケーションがPRからデプロイされた状態で動作確認を行うことも可能です。

Istioで設定をデプロイするにあたって、通常のVirtualServiceでは設定をブランチごとに別々にデプロイできないため、カスタムコントローラで設定をマージしてやるなどの対応が必要になります。今回はIstioの最近のバージョンで利用できるようになった、Kubernetes Gateway APIの HttpRoute リソースを用いることでそのあたりの問題を回避しています。

具体的には以下のような設定をApplicationSetで適用することでBaggageヘッダでルーティングできるようにします。

apiVersion: gateway.networking.k8s.io/v1alpha2
kind: HTTPRoute
metadata:
  name: {{ .Release.Name }}
spec:
  parentRefs:
    - kind: Mesh
      name: istio
  hostnames: ["{{ .Values.app }}.default.svc.cluster.local"]
  rules:
  - matches:
      - headers:
          - type: RegularExpression
            name: baggage
            value: ^(.*\s*;)?branch\s*=\s*{{ .Values.branch }}(\s*;.*)?$   
    backendRefs:
      - name: {{ .Values.branch }}-{{ .Values.app }}
        port: 80

OpenTelemetry

アプリケーションにはOpenTelemetry SDKを組み込み、Baggageヘッダが伝播されるようにしています。これにより、後ろのマイクロサービスでも特定のブランチからデプロイされたアプリケーションにルーティングさせることができます。

今回のgoのgrpcサーバを使った例ではサーバ側、クライアント側それぞれ以下のような実装を入れることで自然とBaggageヘッダが伝播されるようになります。

## サーバ側
server := grpc.NewServer(grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
        grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()))


## クライアント側
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
        grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()))

Baggageヘッダ挿入用Proxy

NginxのプロキシでURLからBaggageヘッダに変換しています。これによりユーザからはURLによって簡単にアクセスを切り替えられるようになっています。

server {
    ... 省略
    server_name  ~^(?<branch>.+)\.katainaka\.org$;

    proxy_set_header baggage            $branch;
    ... 省略
}

動作確認

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

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

なお、検証はローカルクラスタ上で行うため、HTTP2の authority ヘッダ(HTTP1のHostヘッダにあたるもの)を指定して、port forwardingによりBaggageヘッダ挿入用プロキシに接続することで特定のURLでアクセスした状態を再現しています。

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

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

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

まとめ

今回は、ヘッダ伝播を使用してマイクロサービス環境でプレビュー環境を構築する方法をご紹介しました。今回紹介した実装例以外にも実現方法はいくつでも考えられますが、伝播されたヘッダをもちいてルーティングするというアイデアは広く使えるものではないでしょうか。

この記事が誰かの役に立てば幸いです。

補足: 実装例で考慮していないこと

画像等のCORS

画像等でCORSでのアクセスを行っている場合、プレビュー環境ごとにURLが変わることで問題が発生する場合があります。その場合はCORS用のヘッダを返すプロキシ等を用意することで回避する必要があります。

DBのアクセス権限

アプリケーションに付与しているDBのアクセス権限が強すぎる場合、予期せず破壊的なSQLを実行し、ステージングのDBを破壊してしまう可能性があります。

PRのApprove前にSQLを実行することが可能になることを考えて適切な権限を設定するとよいでしょう。

参考