- バックエンド / リーダー候補
- PdM
- Webエンジニア(シニア)
- Other occupations (17)
- Development
- Business
皆さんこんにちは 👋Wantedly DXチームインターンの森本です。
今回は私がインターン課題として取り組んだ、Istioを使って「Fast, Dependency-Agnostic, Isolated」な開発体験を実現した話を紹介します。
DXチームとは
デラックスチームではありません。Developer eXperience チームです 😉社内の開発体験を向上させることを目標に頑張っています。詳しくはメンターの大坪さんの記事を御覧ください。
マイクロサービス
Wantedlyのアプリケーションは異なる役割を持った複数のサービスによって構成されています。これをマイクロサービスアーキテクチャと呼びます。サービスは、RubyでDBにアクセスしユーザー情報を管理するもの、Pythonで機械学習を行うもの、Goで通知を発行するものなど様々です。それぞれのサービスは、モバイルやブラウザおよび他のサービスからのリクエストを受け取り、結果を返します。そしてKubernetesがマイクロサービス全体の管理(プロセスのスケジューリング)を行っています。
「Fast, Dependency-Agnostic, Isolated」
あるサービスを変更したら、マイクロサービス全体で動作確認が必要となります。私たちは、マイクロサービスの開発には、変更を素早く試せる(Fast)、依存関係を意識しない(Dependency-Agnostic)、他人の変更の影響を受けない(Isolated)体験が必要であるという結論に至りました。しかし既存の開発手法ではこの3つの両立がなされていません。
ここからは「Fast, Dependency-Agnostic, Isolated」な開発体験のイメージを膨らませるために、既存の開発手法をFast, Dependency-Agnostic, Isolatedの観点から評価していきます。
既存の開発手法 🙅
1. ローカルでプロセスを立ち上げる
Fast ○ Dependency-Agnostic ☓ Isolated ○
1番単純なやり方は、変更したサービスおよび依存するサービスやDBをローカルで立ち上げることです。変更をしてからそれを試せるまでの時間が秒単位なのでFastであり、ローカルで試しているので他人の影響は受けずIsolatedです。しかし、変更したサービスの依存関係を把握しローカルで全てのプロセスを立ち上げる必要があるので、Dependency-Agnosticではないと言えるでしょう。
2. ステージング環境にデプロイする
Fast ☓ Dependency-Agnostic ○ Isolated ☓
Wantedlyでは、GitHubに変更をpushするとTravis CIによって変更が反映されたDockerイメージがビルドされます。このDockerイメージによってステージング環境のプロセスを書き換えることができます。このやり方はステージング環境で動かすので、自分の興味のあるコードさえ書き換えればよく、Dependency-Agnosticです。しかし変更がDockerイメージとしてビルドされるまで数分かかるためFastではありません。また開発者で共有しているステージング環境を書き換えているので、Isolatedではありません。
3. Telepresenceでローカルにリクエストを流す
Fast ○ Dependency-Agnostic ○ Isolated ☓
Telepresenceを用いると、リクエストをステージング環境からローカルに流すことができます。つまり変更のみをローカルで動かすので、2と違ってDockerイメージをビルドする手間を省くことができます。したがって変更を秒単位で試せるのでFastです。もちろん変更以外はステージング環境で動かすので、自分の興味のあるコードさえ書き換えればよくDependency-Agnosticです。しかしこの方法では、ステージング環境の全てのリクエストがローカルに流れてしまうので、Isolatedではありません。
以上を踏まえると、今までの開発手法では「Fast, Dependency-Agnostic, Isolated」の3つをすべて確保することが困難であるとわかります。
これからの開発手法 🙆
私たちは今回、既存手法3のTelepresenceでローカルにリクエストを流すがIsolatedを満たすようにすることで、「Fast, Dependency-Agnostic, Isolated」の3つを両立した新しい開発手法を提案します。
これからの開発手法は変更をローカルで動かすことでFastを確保します。そして変更以外はステージング環境で動かすことで、依存関係を考慮する必要がなくDependency-Agnosticになります。さらに自分専用のステージング環境を持っているような開発環境を構築することで、Isolatedを満たします。
ここからはどのようにして、自分専用のステージング環境を持っているような開発環境を構築したのか説明します。
方針 🧭
実際にそれぞれの開発者にステージング環境を用意することは困難です。そこで自分のリクエストだけがローカルに送られ、他のリクエストはステージング環境のプロセスに飛ばすことにしました。今回はリクエストヘッダに自分だけの特別な値を挿入することで、自分のリクエストを判別できるようにしました。そしてヘッダの値を見て、自分のリクエストのみをローカルのプロセスにルーティングするようにしました。
このためには以下の4つを実装する必要があります。
1. 任意のアプリケーション(モバイル/ブラウザ)から特別なヘッダが挿入できるデバッグオプション
2. サービス間でのヘッダ伝播
3. 自分のリクエスト専用の受け口(Service)
4. ヘッダの値による動的なルーティング制御
やったこと
私は今回、4. ヘッダの値による動的なルーティング制御を担当しました。もしリクエストヘッダに特別な値が挿入されていたら、自分のリクエスト専用の受け口(Service)にリクエストを飛ばします。
Virtual Service
KubernetesのServiceでは動的なルーティング制御ができないため、Istioをサイドカーとして追加する必要があります。Istioを追加するとVirtual Serviceを用いてServiceを拡張し、ヘッダによるルーティング制御を行うことができます。
上図のルーティング制御を実現するVirutal Serviceは次のように書けます。ここでは、foobarの特別なヘッダの値を request-sender : foobar としています。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
spec:
hosts:
# ルーティングを制御したいService名
- service-b
http:
- match:
# request-sender:foobarがヘッダに含まれていたら
- headers:
request-sender:
exact:
foobar
route:
- destination:
host:
# foobarのリクエスト専用のServiceにルーティングする
service-foobar
- route:
- destination:
host:
# それ以外はデフォルトのServiceにルーティングする
service-b
次に、hogehogeも自分のリクエストをローカルに飛ばしたくなった場合を考えます。この場合のVirtual Serviceに期待される挙動は、foobarのリクエストはfoobarに、hogehogeのリクエストはhogehogeに、それ以外はデフォルトに飛ばすということになります。hogehogeの特別なヘッダの値を request-sender : hogehogeとすると、これは以下のように書けます。
spec:
hosts:
- service-b
http:
- match:
- headers:
request-sender:
exact:
foobar
route:
- destination:
host:
service-foobar
http:
- match:
- headers:
request-sender:
exact:
hogehoge
route:
- destination:
host:
service-hogehoge
- route:
- destination:
host:
service-b
このように、全てのヘッダ条件をチェックしたのちに、どれにも該当しない場合はデフォルトに飛ばすという挙動を実現するためには、Virtual Serviceは1つのServiceに対して、2つ以上存在しないように制限する必要があります。なぜなら、全てのヘッダ条件を把握していないとVirtual ServiceをMECE(漏れなく・ダブリなく)に分解することができないからです。したがって、複数人が同時にあるServiceに対するルーティングを制御したい場合は、Virtual Serviceのヘッダ条件の追加をしていくことになります。
しかし手動で開発者がVirtual Serviceにヘッダ条件を追加することには問題点があります。既存のヘッダ条件を上書きせず自分の条件を追加していくには、他の人のヘッダ条件を把握する必要があることです。
Virtual Service Controller
そこで、自分の追加したいヘッダ条件を登録したら、既存のヘッダ条件と自分の追加したい条件を合わせて自動でVirtual Serviceを作成してくれるコントローラーを用意することにしました。Virtual Service Controllerは適切なタイミングでヘッダ条件を収集し、Virtual Serviceの作成および更新を行うことができます。
controllerの作成にはKubebuilderを用いました。Kubebuilderを用いると、カスタムリソースの定義や、リソースのCRUDのタイミングでの処理を簡単に記述することができます。
VSConfig
開発者はVSConfigというカスタムリソースを用いて、自分が追加したいヘッダ条件を登録します。VSConfigは以下のように、ルーティングを制御したいService名、自分だけの特別なヘッダの値、自分のリクエスト専用の受け口(Service)を登録することができます。
apiVersion: vsconfig.k8s.wantedly.com/v1beta1
kind: VSConfig
spec:
host:
# ルーティングを制御したいService名
service-b
service:
# 自分のリクエスト専用の受け口(Service)
service-foobar
# 自分だけの特別なヘッダの値
headerName: request-sender
headerValue: foobar
Virtual Service ControllerはVSConfigの情報からヘッダ条件を作成し、デフォルトのルーティングを追加したVirtual Serviceを作成します。
詳細
ここからはVirtual Service Controllerの設計について具体的にお話します。
まずはVirtual Service Controllerの責務についてです。Virtual Service Controllerは、入力として指定されたServiceをOwnerとするVirtual Serviceの作成と削除を担います。入力として指定されたServiceが存在するかどうかや有効であるかどうかは考慮しません。
さらにVirtual Service Controllerの責務を上記の様に定めたので、すべてのServiceに勝手にVirtual Serviceを生やすことにしました。これによって、どのServiceがVirtual Serviceを必要としているのか判定する手間を省きました。
よって、Virtual Service ControllerはServiceとVSConfigのCRUDを監視することが必要になりました。ServiceのCRUDを監視することで、すべてのServiceが必ず1つのVirtual ServiceのOwnerになります。
今回はServiceやVSConfigのCRUDが行われるたびに、該当するVirtual Serviceを0から作成し直すことにしました。これはイベントの種類に関係なく同一の操作を行うためです。また毎回Virtual Serviceを作成し直すことで、デフォルトのルーティングを安全に最後に追加することができます。以下は具体的な実装です。
1. Serviceが作成 / 更新された時
作成 / 更新されたServiceのヘッダ条件に関するVSConfigを全て取得して、最後にデフォルトのルーティングを追加したVirtual Serviceを作成します。そして既にVirtual Serviceが存在していれば、Virtual Serviceを新しいものに更新します。存在していなければ、Virtual ServiceのOwnerとしてServiceを紐付けた上で作成します。
2. Serviceが削除された時
Referenceを設定しているので、ServiceがOwnerとなっているVirtual Serviceは自動で削除されます。
3. VSConfigが作成 / 更新された時
VSConfigがヘッダ条件を追加しようとしているServiceに関するVSConfigを全て取得し、最後にデフォルトのルーティングを追加したVirtual Serviceを作成します。そして既にVirtual Serviceが存在していれば、Virtual Serviceを新しいものに更新します。存在していなければ、Virtual ServiceのOwnerとしてVSConfigのターゲットとなるServiceを紐付けた上で作成します。
4. VSConfigが削除された時
VSConfigが作成されたnamespaceにあるVirtual Serviceを全て再生成します。これは削除されたVSConfigのインスタンスを取得することができなかったためです。削除されたVSConfigを取得できれば、どのServiceに関するヘッダ条件が消されたかわかるので、特定のVirtual Serviceを上書きするだけで済みます。この部分は今後の改善点です。
まとめ
Istioによって自分のリクエストだけをローカルのプロセスに飛ばすことで、自分専用のステージング環境を持っているようなデラックスな開発環境を構築することができました。これによってIsolatedを満たしながらTelepresenceを使えるようになり、「Fast, Dependency-Agnostic, Isolated」な開発体験が実現しました 🎉
KubernetesやIstioの存在を全く知らなかった私ですが、約2ヶ月間でControllerの設計や作成ができるようになりました 👏メンターの大坪さん、デバッグに協力してくださった皆さんありがとうございました〜!