この記事は 2021/03/11 に公開された CloudNative Days Spring 2021 ONLINE でのトーク「Pull Request Preview URL - 後ろ側の Microservice の Review 簡単に」を記事に起こしたものです。口頭発表を確認したい方は是非下のリンクを御覧ください。
改めてこんにちは、Wantedly の DX Squad で技術基盤を作っている大坪です。チームと自分の紹介については下のリンクを御覧ください。
TL;DR 今回の発表を簡単に要約すると下のようになります。
Istio + Ambassador を使って 仮想的な Kubernetes の copy を実現 これによって開発体験がガラッと変わる しかしこれだけだとよくわかりません。「なんで copy をしたいと思ったのか」「どうやったらそれが実現できるのか」「結局何ができるようになるのか」といった疑問が湧きます。そこでこの3点をそれぞれ WHY, HOW, WHAT として順に説明していきます。
WHY 高速で効率的なクラスタ copy が作りたい Preview Deployment はよくある体験 Vercel, Heroku, Netlify などのモダンな PaaS はどれも Pull Request を deploy した環境を作成する機能を持っています。それぞれこの機能の呼び方は異なりますが、ここではまとめて preview deployment と呼ぶことにします。
https://vercel.com/docs/git/vercel-for-github より引用
上の画像は Vercel が preview deployment として提供している機能の利用イメージです。この画像から想像に難くない通り、下のような体験が得られます。
ワンクリックで変更をプレビューできる 本番に近い環境で動いているものを触ることができる チーム全員で同じものを同時に見ることができる ローカルの開発環境にコードを落としてきて起動する方法だとどうしてもここまで気軽ではないですし、特にそのサービスの起動方法を知らない他のチームのエンジニアや、デザイナ、カスタマーサポートといった人だけで変更が意図どおりであるかを確認するのは簡単ではありません。ローカルで確認する方法だとまた本番と開発用ラップトップの環境差異、開発用ラップトップ同士の環境差異などの問題を踏むこともあるので予期せぬところで詰まってしまうこともあります。
Microservice では簡単ではない 上で述べた通り preview deployment は珍しい機能でも真新しいものでもありません。しかしながら microservice アーキテクチャに適応することはそこまで簡単ではありません。
4つの microservice が存在し直列に接続されている状況を考えます。この時最も front に立つ microservice に対して preview を与えようと思った場合は単純にその service を copy してそこにつなげる URL を発行すればよいでしょう。
しかしながら最も後ろ側に立つ microservice の変更を preview したい場合はどうでしょうか。その microservice に直接つなげる入り口を用意しても intergation した状態での preview ができません。そうすると必要な microservice を copy したくなってきます。今回のケースだとたかだか4つの component を copy してあげれば integration した状態が手に入ります。ただ現実問題として microservice が増えてくると全ての microservice の依存関係を把握するのは簡単ではなくなってきます。したがって preview deployment を microservice でも実現しようとするともう少し良いアプローチが必要であることがわかります。
Kubernetes Cluster 自体を copy すれば ok! もう少し良いアプローチと書きましたが我々のアイデアそれ自体はそんなに頭の良いものではありません。全部一気に copy してしまえば勝手に依存関係が満たされたものが用意できるし、そこで一部の microservice だけ自分のブランチのものに置き換えれば preview 環境ができあがります。この copy するという考えやそれに至る道程については CloudNative Days Tokyo 2020 でのトークでも詳しく話しているので興味がある方はぜひご覧ください
How Copy on Write で Virtual Copy ここまでで「Kubernetes Cluster の copy ができれば嬉しい状況がある」という確認を行いました。ここからは「どうやって copy を作るのか」について考えます。当然ながら真面目に Kubernetes Cluster を copy すると計算資源も費用も大量に費やすことになり現実的ではありません。そこで必要な部分だけ copy する Copy on Write 方式を考えます。
Wantedly では 140 の microservice と 50人の開発者がいます。ここで簡単のために各 microservice にリソース消費が同じであるとすると、全てを copy すると 140 * 50 = 7000 namespace 分のリソースが必要になります。対して各開発者が同時に変更したい microservice は基本一つであるため、Copy on Write で必要な部分だけ多重化すれば 140 + 50 = 190 で済むので圧倒的に効率的です。
用語定義 具体的な解決策を考える前に必要な Terminology を用意します。
Kubefork 今回作る基盤をこのように呼ぶことにしました。Copy on Write というアイデアがプロセスの fork に似ているという理由です。
Virtual Cluster Virtual Cluster はコピーされた仮想的な Kubernetes Cluster を指します。実際には一つの Cluster の中に複数あるように見せるわけですが、一つだと扱うことができる単位をこのように呼びます。つまり今回のプロジェクトは以下にして実 Cluster から Virtual Cluster を払い出すという体験を用意できるか。という問題になります。
Fork Identifer 一つの実 Cluster の中に複数の Virtual Cluster が混在する状態を考えるため、それぞれを特定できる必要があります。そこでこのような識別子を Fork Identifier と呼びます。 string
であると考えて差し支えありません。
Fork する 一つの実 Cluster から Virutal Cluster を払い出す操作をこのように呼びます。
Routing の問題に落とす さて、Copy on Write とは言ったもののこれを実現する方法は自明ではありません。Kubernetes を通して得られる全ての体験を本当に全て複数あるように見せようとすると本当に大変なので、 Service
と Deployment
のみが変更できれば良い、つまりそれ以外の resource は共有するしかないという前提をつけることにしました。これにより、request の経路を十分上手く制御すればよいという Routing の問題に落とし込めます。自分が送った request が 自分が変えたい場所だけ 通常と異なる場所に routing され、ほかは共通部分を通れば体感としては全てが copy された環境と変わらないということです。
例えば上の図では Forked A
は実際には2つめの microservice しか実体を持っていませんが、その実態以外の挙動は全て共用クラスタを使っています。複数の microservice を変更したい場合、一番 front に立つ microservice を変更したくなった場合も少し工夫してあげれば良さそうだと Forked B
の方を見るとわかります。
ここでいよいよどうやって作るのかという話になります。Wantedly では Context Propagation、Service Mesh、Gateway の3点が必要であると結論づけました。(それぞれ今回のプロジェクトに必要な component に付けた名前なので一般的な用語と完全に一致しない点もあります。)
基本的には下の方針です。
Context Propagation: HTTP Header として利用する Virtual Cluster を指定 Service Mesh: Header を元に routing を制御 Gateway: URL を header に変換 Context Propagation: Virtual Cluster の指定を奥でもわかるように Wantedly では servicex という共通ライブラリがあるため全ての microservice に挙動を流し込むことができます。ここで特定の HTTP Header 及び gRPC Metadata を伝搬するようにしました。今回のプロジェクトとは無縁の場所でもマイクロサービス共通ライブラリを持っておくこと、及び Context を microservice 間で伝搬することには価値があります。具体的には下のリンク記事に書いてあります。
今回のケースだと、 どの Virtual Service に接続しているつもりの request か を context として伝搬することでどれだけ複雑に入り組んだネットワークでも、任意の場所で特別扱いをしてあげることが簡単になります。
具体的に言うと X-Fork-Identifer: fork-a
のような header 付きで request の処理中に別の microservice に通信を行う場合は必ず X-Fork-Identifer: fork-a
を付けるという仕組みです。
Service Mesh: Virtual Cluster の指定があったら Request を捻じ曲げる Context Propagation で任意の microservice 通信において「どの Virtual Cluster に接続することを意図された request であるか」がわかる状態になっています。これに応じてつなぎに行く pod の向き先を変えてあげればどれだけ深い場所にある microservice でも、そこに到達する経路を用意できます。
開発者の立場としては自分が変更したい microservice が「どの pod から接続されているのか」および「どの pod に接続しに行くのか」という情報には興味がないのでそこだけ共用部分を使ってしまっても「全てが copy されている」という感覚を提供できます。
具体的に言うと X-Fork-Identifer: fork-a
という header がついた microservice の通信を見つけて、その request 先の service が fork-a
という Virtual Cluster に見つかったらそっちへの request として扱う仕組みです。
ただしこのレイヤは きれいに隠蔽すること が非常に大事だと考えています。 Wantedly ではこの記事で述べるような整理がまだできていない状態で先にこの routing 機能から実装を行ったため「Kubefork とは複雑な Routing を制御する難しいツール」であるという認識が多少持たれてしまいました。「自分専用のクラスタを持てる」という言い方は響いても「自分の pod に到達できるような routing 制御ができるツール」という言い方はあまり関心を引けませんでした。そこで次に routing を意識しない方法について説明します。
Gateway: Host を Header に変換 ここまでをまとめると下のようになります。
Header で Virtual Cluster を指定すると その Header は伝搬されるので 任意のマイクロサービス間通信に介入して copy された pod に接続できる Header を誰がつけるのか ここで解決されていない問題として、誰がその良い感じの header を付けてくれるのかという問題があります。もともと Wantedly では簡単に header を付与できる Chrome Extension を作っていましたが、これの評判があまり良くありませんでした。問題としては下のようなものがあります。
結局接続先は共有の cluster のものと同じなので copy されている感覚がない エンジニアでない人に案内しづらい Chrome 以外から、特に mobile デバイスから使うことができない Routing 制御ツールという実装特有の問題がUIの露出してしまったために、構築したいメンタルモデルを持ってもらうことが難しいものだったのです。
Host を Header に変換すれば良い
Virtual Cluster ごとに異なる URL を持つ Header を付けなくても指定した Virtual Cluster につながる これは意外とシンプルで Wildcard DNS を持つ proxy を一つ作って header を付与する機能を作るだけでした。これによって copy された場所に接続するという体験を作る事ができました。
例えば本番では、 some.wantedly.com
で提供されている service が開発環境では some-dev.wantedly.com
というで提供されているとします。そこで *.some-dev.wantedly.com
が向けられている proxy を用意します。 fork-a.some-dev.wantedly.com を host として request を付けとったら
DNS のラベルを一つ削った some-dev.wantedly.com
に request をたらい回しにしつつ。 X-Fork-Identifer: fork-a のように header を付与するようにすれば良いだけです。
より具体的には下のような Ambassador の mapping を Virtual Cluster の状況に合わせて生成する Custom Controller で実現しています。
apiVersion : getambassador.io/v2 kind : Mapping metadata : labels : fork.k8s.wantedly.com/manager : default name : some - dev - wantedly - com - some - identifier namespace : fork - proxy - ambassador spec : add_request_headers : x-fork-identifier : some - identifier ambassador_id : - fork - proxy - ambassador host : some - identifier.some - dev.wantedly.com prefix : / service : https : //some - dev.wantedly.com
このシンプルな Gateway という仕組みを作った経験はエンジニアとしても少し面白いものでした。技術的には Context Propagation と Service Mesh のほうが考えることが多く、Gateway は技術的には何も難しいことはありませんでした。しかしながらこれを作ってから Kubefork が圧倒的にわかりやすく使いやすいツールになりました。良い Developer eXperience を得るために技術的な難しさは必ずしも必須項目ではないわけです。
実装 どんなふうに作っているかのイメージをより深く理解したい方のためにもう少しだけ具体的な実装に触れます。新しい Virtual Cluster を作りたい場合は下のような Custom Resource を作成するようになっています。
apiVersion : vsconfig.k8s.wantedly.com/v1beta1 kind : Fork metadata : name : wantedly - some - identifier namespace : wantedly spec : identifier : some - identifier manager : fork - proxy - ambassador/default services : selector : matchLabels : role : web deployments : selector : matchLabels : newrelic : "true" template : metadata : labels : app : some - identifier role : fork annotations : wantedly.com/deploy-target : "false" spec : containers : - image : example : tag name : wantedly
この一つの resource を作るだけで前述の Ambassador の Mapping を含む必要な resource を全て作ってくれるようになっています。
全体像としては上記の Fork
resource を作ることで下の resource が作られます。
WHAT 応用と得られた体験 ここまでで「実質的に Kubernetes Cluster を copy する」ことができました。次にこれがあると何がうれしいのかを考えます。
Telepresence し放題 Telepresence はクラスタに流れる通信を自分のローカルに流すことのできる便利なツールで、これを上手く使うと docker image の build を待たずに自分のコードを動かしてみることができます。しかしながら共用クラスタで全ての request をトラップしてしまうと他の人の request まで吸い込んでしまうので共用環境の信頼性が下がってしまいます。
Kubefork によってCopy された Cluster では、自分専用の環境と感じることができるため、どんな Bug のある version を deploy しても誰にも迷惑をかけることがありませんし、どんなアグレッシブな Telepresence の使い方をしても問題ありません。
下記のCloudNative Days Tokyo 2020 でのトークは主にこのユースケースについて扱ったものです。
Feature Flag ごとに簡単に URL を発行 Wantedly には Feature Flag 基盤があります。これは「Rails の 任意のHelper の任意の method」「全ての AB test」「明示的に annotate した任意の method」のロジックを特定の request に対してのみ override する機構です。前述の Context Propagation の機構の上に成立しています。
実際の format とは異なりますが、下のような形式の Header を付けて request を送るとこれが伝搬され、各 microservice は、 new_feature?
という method が内部で呼ばれるとその戻り値を強制的に true
として評価するようになっています。
X-Feature-Flags: {"name": "new_feature?": "enabled": true}
この機構については下の資料で少しだけ触れています。
これも Virtual Cluster の指定の方法と同様の「どうやって Header を付与するのか」という問題があります。しかし Gateway は「Host を任意の Header に変換する機構」だったので、少し工夫するだけでこの Feature Flag の header をつけるようにするだけで Header 付与問題を解決できます。
具体的には下のような resource を作ります。この場合 service や deployment を指定していないので実コピーは一切作られませんが、 new-feature-enabled
を id に持つ Virtual Cluster が払い出されます。そして new-feature-enabled.some-dev.wantedly.com
のような URL も払い出されるため、「ここにアクセスすればある機能が必ず有効になっている環境を簡単に作成する」という体験を提供できます。
apiVersion : vsconfig.k8s.wantedly.com/v1beta1 kind : Fork metadata : name : new - feature - enabled namespace : wantedly spec : identifier : new - feature - enabled manager : fork - proxy - ambassador/default gatewayOptions : addRequestHeaders : x-feature-flags : '{"name": "new_feature?": "enabled": true}'
Wantedly では新機能のリリース時は、この手法で誰でも検証できる環境を作っています。この手法の場合はデザイナやビジネスなどエンジニアリングのバックグラウンドを持たないメンバーにも簡単に検証をお願いすることができるので非常に便利です。
Pull Request Preview URL ここでやっとタイトルのユースケースが登場します。Copy on Write なので多少アグレッシブに Virtual Cluster を発行してしまっても大丈夫です。そこでコードが push されるたびに適切な fork resource を作成するようにすると、どんなに後ろ側の Microservice を変更したときでもそこにつなげる URL を発行できます。
内部向けの資料なので少し見にくいものになってますが、下が全体のアーキテクチャです。
これは今回の登壇までに完成しているはずだったんですが実はまだ完成していません。そこで作られた世界のイメージ図をここに示します。
まとめと終わりに 今回作った基盤はこれまで作ってきたものの上に構築されたものです。簡単に示すと下のようなものがあります。
context propagation は feature flag 用の基盤として実装済みだった servicex があったから context propagation の実装が簡単だった istio が導入済みだったので service mesh の導入は簡単だった そもそも全 microservice が Kubernetes に移行が完了していた 今回作ったものをすべてゼロから積み上げていくとしたら莫大な時間がかかっていただろうなということが想像されます。良い基盤は一朝一夕にはできないので少しずつ良いものを良い粒度で作っていくことが大事だなと実感しています。
Context Propagation, Service Mesh, Gateway の3つの基盤を組み上げることで、Kubernetes を効率的に copy するという体験を提供することができます。これを実現することにより、Telepresence や Preview Deployment などマイクロサービスでは導入が難しくなってしまったツール/基盤を提供できるようになります。特に示した Context Propagation は Virtual Cluster という体験を完全に抜きにしてもかなり便利な基盤なのでぜひ作ってみてほしいなと考えています。
また、書いた通りまだ全然完成していない仕組みです。