envoy の gRPC proxy に関する便利機能
Adventarを支える技術 Advent Calendar 2019 の6日目です。
今日は envoy の gRPC に関する便利機能について紹介しようと思います。
gRPC-Web proxy
4日目の記事でも書きましたが、今回は gRPC-Web の proxy レイヤーとして envoy を利用しています。envoy で gRPC-Web の機能を有効するのは簡単で、HTTP filters に envoy.grpc_web
を書いて、ヘッダの設定をするだけです。
- https://github.com/adventar/adventar/blob/f580de20510f9debe6356a5ad193c4532d8f6a0d/api-server/envoy/envoy-prod.yaml#L44
- https://github.com/adventar/adventar/blob/f580de20510f9debe6356a5ad193c4532d8f6a0d/api-server/envoy/envoy-prod.yaml#L36-L38
これだけで gRPC-Web を受けて、upstream のクラスタに gRPC でリクエストするようになります。超簡単。他に書くことがありません。
gRPC-JSON transcoder
5日目の記事に書きましたが、Adventar では、gRPC と JSON API の両方を envoy によって実現しています。JSON API で受けて、upstream の gRPC に流すのは、envoy の gRPC-JSON transcoder という機能を使っています。
https://www.envoyproxy.io/docs/envoy/latest/api-v2/config/filter/http/transcoder/v2/transcoder.proto
これは grpc-gateway と同じようなものです。Protocol Buffers の定義から生成したスキーマを envoy が読んで、gRPC をバックエンドにした JSON API を提供します。例えば、次のような Protocol Buffers を定義します。
syntax = "proto3"; package adventar.v1; import "google/api/annotations.proto"; message GetCalendarRequest { int64 id = 1; } message Calendar { int64 id = 1; string title = 2; ... } service Adventar { rpc GetCalendar(GetCalendarRequest) returns (Calendar) { option (google.api.http) = { get : "/v1/calendars/" } } }
option
で指定しているのは、grpc-gateway でも使われているもので、 gRPC と JSON API のマッピングを定義するためのものです。
envoy 側の設定は簡単で、envoy.grpc_json_transcoder
を設定するだけです。
ここで指定ているproto.pb
は以下のようなコマンドで出力しています。
$ protoc \ --include_imports \ --include_source_info \ --descriptor_set_out=../api-server/envoy/proto.pb \ adventar/v1/*.proto
これで envoy と gRPC サーバーを起動すると、以下のようにアクセスすることが可能になります。
$ curl http://localhot:8080/v1/calendars?id=1 | jq { "id": 1, "title": "xxx", ... }
クライアント側が gRPC を使えなくても、普通の HTTP で通信できるので、様々な場面で便利な機能だと思います。
auto_mapping
gRPC-JSON transcoder は便利なのですが、Protocol Buffers の定義に
option (google.api.http) = { get : "/v1/calendars/" }
のようなアノテーションを書かないといけないのが面倒です。これを解消するのに、envoy v1.11.0 から auto_mapping という機能が追加されました。
https://github.com/envoyproxy/envoy/pull/6731
これはgoogle.api.http
の定義を書かなくても、POST /<package>.<service>/<method>
という URL でアクセス可能になるという機能です。
設定は簡単でauto_mapping: true
と書くだけです。
これで以下のようにアクセスできます。
$ curl -X POST -d '{"calendar_id":1}' https://localhost:8080/adventar.v1.Adventar/GetCalendar | jq { "id": 1, "title": "xxx", ... }
Adventar ではこの機能を使っているので、google.api.http
の定義は書いていません。
(余談)google.api.http を捨てられていない理由
google.api.http
の定義は書いていません、と言いつつ、実は一番下にgoogle.api.http
の記述があるのに気づいたと思いますが、これは gRPC サーバーの前に立てている AWS Application Load Balancer(ALB) のヘルスチェックが GET でしかできず、auto_mapping は POST にしか対応してないので、しかたなく書いています。
ちなみに ALB は今現在 HTTP/2 の pass through に対応していないので、普通は gRPC の前段に置くことはできなくて色々と面倒なのですが、今回は gRPC-Web と JSON API しか通しておらず、直接 ALB が gRPC をしゃべる必要がないので ALB が利用できて便利です。このあたりのインフラの概要については後日書く予定でいます。
明日は Firebase Authentication について書く予定です。
gRPC-Web と Server Side Rendering の苦労話
Adventarを支える技術 Advent Calendar 2019 の5日目です。
今日は gRPC-Web 導入にあたって最も苦労した Server Side Rendering(以降は SSR と書きます)の話を書きます。
前提条件
初日の記事 に書いたとおり、今回の Adventar のシステム変更の目的に、
- gRPC-Web を使う
- Serverless な仕組みで SSR をおこなう
というのを入れました。趣味プロダクトなので技術が目的になっています。なので、gRPC-Web をやめる、SSR をしない、という選択肢はそもそも取らない、という前提で読んでください。(どちらかをやめることができればどれほど楽だったか...)
また、ここでいう SSR とは Nuxt.js などで作った JavaScript のアプリケーションを同一コードでサーバー側でも動かして HTML をレンダリングする、という文脈のやつです。Universal JavaScript と言い換えてもよいです。
個人的には SPA の SSR は実装難易度や運用コストが増えるので、やらないで済むならやらないほうがいい派なんですが、今回は普段やらないようなこともやってみるというのも目的の一つにありました。
gRPC-Web が Node.js で利用できない問題
gRPC-Web の自動生成したクライアントは完全にブラウザ用になっていて、Node.js では利用できません。何が困るかというと、SSR する際にブラウザとサーバーで同一のコードが使えないので詰んでしまいます。
ブラウザ用の Polyfill などを入れてみたりしましたが、どうしても動かないので諦めました。正攻法は gRPC-Web のクライアント生成に手を入れて Node.js 用のトランスポートオプションをサポートすることだと思いますが、C++ (protoc の gRPC-Web プラグインの実装言語)が苦手すぎて今回は諦めました。
そこで今回は妥協案として、Node.js 向けに JSON API を提供することにしました。JSON API というのは普通に ContentType: application/json
で通信する API という意味です。つまりブラウザと API サーバー間は gRPC-Web で通信しますが、SSR する場合の Node.js と API サーバー間は JSON API で通信します。最悪な妥協策というのは理解しています。
envoy を使った JSON API Proxy
API を2種類用意するといっても、サーバーを別途を立てたわけではありません。gRPC-Web の proxy として利用している envoy に、JSON API を gRPC に変換する機能があるため、gRPC-Web と JSON API を同一のプロセスで処理できます。
envoy のこの機能については後日詳しく紹介しますが、少し設定を足すだけで済むので、サーバー側は特に手間はかかりません。2つの API があることで面倒なのはクライアント側だけということです。
クライアント側のコード
クライアント側の対応はサーバー側よりは面倒ですが、そこまで複雑ではありません。以下のように gRPC のクライアントと JSON API のクライアントを用意します。
そして以下のように ブラウザが実行する場合は gRPC、SSR の場合は JSON API を呼び出すだけです。
- gRPC の呼び出し
mounted
は SSR では呼び出されないのでクライアント側でしか実行されない
- JSON API の呼び出し
process.server
は SSR の場合だけtrue
になる
SSR は基本的に更新系のAPIを呼び出すことはないですし、今回は SSR するのをカレンダーの詳細ページ( https://adventar.org/calendars/3860 のようなページ)だけに留めたので、最終的にはこれぐらいのコード量でなんとかなりました。しかしここに行き着くまでにはだいぶ苦労しました...。
まとめ
仕事だったらおそらく頑張って Node.js のトランスポートオプションを実装するか、gRPC-Web を諦めて JSON API に寄せるか、SSR を諦めて別の方法を取る道をさぐったかもしれませんが、今回は前提条件にもあるように、gRPW-Web と SSR は妥協できなかったのでこのような方法を採用しました。来年時間が取れれば gRPC-Web に Node.js サポートパッチを書きたいです。
明日は envoy の便利機能について書く予定です。
gRPC-Web を利用したクライアント・サーバー間の通信
Adventarを支える技術 Advent Calendar 2019 の4日目です。
今日は gRPC-Web について書きます。
gRPC-Web とは
gRPC-Web は今年の10月に GA になったプロトコルで、今回の Adventar システムリニューアルでは絶対に gRPC-Web を production で使ってみる、という気持ちの元、 gRPC-Web を中心にその他の設計を決めました。
gRPC は Google が公開している RPC 方式で、Protocol Buffers と HTTP/2 をベースにしたバイナリプロトコルです。ブラウザは HTTP/2 に対応していないブラウザもまだまだ現役でたくさんいますし、バイナリを扱うのが苦手だったりします。
そこで、ブラウザでも利用できる gRPC-Web という新しいプロトコルを作り、gRPC-Web を gRPC に変換する proxy 層を介して通信することで、gRPC の旨味をブラウザでも利用できるようにする、というのが gRPC-Web です。
gRPC の旨味というのは、Protocol Buffers による API スキーマの定義、そのスキーマを利用したクライアントの自動生成などが挙げられると思っています。grpc-gatewayを利用すると gRPC のサーバーとブラウザで通信することはできますが、Protocol Buffers の定義を使ったクライアントの自動生成などはできませんでした。
proxy の実装は公式でいくつか提供されていますが、今回は envoy を利用しました。envoy 採用の理由は、gRPC-JSON transcoder などの機能も使いたかったためです。これについては後日の記事で解説します。
実際に gRPC-Web でブラウザがどのような通信を行っているかは Chrome のコンソールなどで確認できると思います。リクエスト・レスポンスの body は Base64 なので読めないですが。
TypeScript 対応
今回 gRPC-Web を採用して一番よかったと思うところは、TypeScript のクライアント自動生成です。まだ Experimental な機能ではあるのですが、公式で TypeScript のサポートをしています。
https://github.com/grpc/grpc-web#typescript-support
実際に生成しているのは以下で
以下のように利用しています。
自動生成されたクライアントを一段 wrap してアプリケーション内の型にキャストしているのがややダサいですが、これは実装し始めたときに PromiseClient というものが生えることに気づいてなくて、callback を Promise にキャストするついでに型変換も行っていた名残りだったりします。
このように、TypeScript によるクライアントを自動生成することで、API のリクエスト/レスポンスの型が保証されるのは非常に良い体験でした。
とはいえ、ハマったところや苦労したところも多々ありましたので、明日はそのあたりの話を書こうと思います。
Nuxt.js によるフロントエンドの構築
Adventarを支える技術 Advent Calendar 2019 の3日目です。
今日はフロントエンドのフレームワークとして採用した Nuxt.js について感想を書こうと思います。
個人的には Vue.js よりも React のほうが好きなので、最初は Next.js を検討しましたが、ルーティングまわりのできの良さ、エコシステム、全体の完成度などを吟味した結果 Nuxt.js を選択しました。Next.js が全然ダメというわけではないので、好みにもよると思います。
ここがよかった Nuxt.js
オールインワンな環境
ビルドや Lint の環境が自動できて、テストのライブラリや State 管理の仕組みも組み込まれていて、デプロイもコマンド一発で簡単にできる環境がすぐに整うのはよかったです。
小さいライブラリを自分で組み合わせて作るのもいいですけど、Webフロントエンドの技術スタックは選択すべきものが多すぎるので、何も考えずに Nuxt.js が勧めてくるものを脳死で使うというは楽です。
このあたりはおそらく Next.js も大差ないですね。
エコシステム
プラグインやドキュメントが豊富なのはよかったです。Nuxt PWAなどは少し設定するだけで PWA の設定ができたり、nuxt-fontawesomeでパっと fontawesome が導入できたりとか。
View のコンポーネントライブラリについても Vue.js のライブラリが利用できるので、欲しいものはだいたいあるので良いですね。個人的に UI コンポーネントはよほど面倒なものじゃない限り自作するのが好きなので今回はほとんど使いませんでしたが。
ルーティング
Next.js との比較になりますが、ルーティングの機能はよくできていると思います。特に動的なルーティングに大きな違いがありました。
Adventar はもともと Rails でできていたので、/calendars/:id
のような URL になっていました。これを Next.js で表現しようとすると、/calendars?id=100
のようにするか、express などのサーバーを置いて処理するしかありませんでした(つまり SSR 必須)。
Nuxt.js では、以下のようなディレクトリ構成にするだけでこのルーティングを express などのサーバーを建てずに実現できます。
. └── pages └── calendars └── _id # アンダースコアから始まる名前は動的な値を指定できる └── index.vue
ただ、この機能は Next.js にも少し前に入りました。
https://nextjs.org/blog/next-9#dynamic-route-segments
実際に試してはいませんが、これがあったら Next.js を採用していた可能性はあります。しかし、動的なルーティングのファイル名の規則が、Nuxt.js が _id
のようにアンスコで始まる、というものに対して Next.js は [id]
のようにブラケットで囲む、というものでありだいぶ気持ち悪い感じはありますね...。
ここは微妙だった Nuxt.js
TypeScript対応
TypeScript 対応に苦労しました。今は公式で対応しているので難しくないと思いますが、作り始めたころは絶賛 TypeScript 対応中という感じで、試行錯誤した覚えがあります。
また、これは Nuxt.js というよりは Vue.js の問題なのですが、TypeScript の型チェックがテンプレートに効かないので、結局テンプレートのところで型の齟齬が発生してエラーになることがあってせっかくの TypeScript が片手落ちという感じでした。Vuex と TypeScript の相性もアレですし。
Vue.js もそのうち改善されるとは思いますが、この点においては React のほうが圧倒的に好きです。
バージョンアップ
Nuxt.js を 2.8.1 -> 2.10.1 にあげたんだけど全然動かなくてセマンティックバージョニングとはいったい... という気持ちになってる
— hokaccha (@hokaccha) 2019年10月14日
つらい。けっきょくまだバージョンアップできてません。
まとめ
総合的には Nuxt.js はよくできているフレームワークで、採用してよかったと思いました。ただやはり個人的には TypeScript との相性の面で React のほうが好きなので、Next.js に期待したいところです。
明日は gRPC-Web について書きます。
Adventar をローカルで開発する
Adventarを支える技術 Advent Calendar 2019 の2日目です。
Adventar はオープンソースでコードを公開しているので、誰でも環境を再現することができます。ただ、システムがそこそこ複雑で、ドキュメントなども全然書いていないので、たぶんリポジトリを見ても起動する方法がわからないと思うので、自分用のメモも兼ねて Adventar をローカルで起動して開発する方法をここに記しておきます。
まずソースコードを手元に持ってきます。
$ git clone https://github.com/adventar/adventar $ cd adventar
$ tree -L 1 . ├── README.md ├── api-server # API server ├── batch # スケジュール実行されるバッチジョブ ├── db # database のスキーマ ├── frontend # Nuxt.js によるフロントエンド(SSRのコード含む) ├── image-server # 画像サーバー ├── protobuf # protocol buffers の定義 └── terraform # インフラ構成のコード
batch
, image-server
, terraform
, protobuf
あたりはローカルで起動するのには使わないので今回は気にしないでください。
まず認証で使っている Firebase Authentication に必要な Credentials を作る必要があります。これがなくても起動はできますが、ログインができないのでカレンダーの作成や登録ができなくて、実質なにもできません。
Firebase Consoleで新規プロジェクトを作り、Googleログインに設定を有効にします。
次にサービスアカウントの設定から秘密鍵の生成を実行します。
そうするとJSONファイルがダウンロードされるので、これをAPIサーバーの起動時に環境変数に設定します。
$ export FIREBASE_CREDENTIAL_JSON=$(cat ~/Downloads/adventar-test-firebase-adminsdk-xxx.json) $ cd api-server $ docker-compose up
これでAPIサーバーは起動するはずです。環境変数の設定は毎回やるのは面倒なら direnv や envchain などを使うといいと思います。僕は envchain を使っています。
これだけだとまだ DB に table ができていないのでスキーマを流します。
$ cd adventar/db $ mysql -u root -h 127.0.0.1 --port 13306 adventar_dev < schema.sql
最後にフロントエンドのサーバーを起動します。フロントエンドのサーバーにも Firebase の環境変数をいくつか設定する必要があります。
$ export FIREBASE_API_KEY=xxx $ export FIREBASE_PROJECT_ID=your-project-name $ export FIREBASE_AUTH_DOMAIN=your-project-name.firebaseapp.com $ cd adventar/frontend $ yarn install $ yarn dev
FIREBASE_PROJECT_ID
は先程ダウンロードした JSON に記述されているproject_id
です。
$ cat ~/Downloads/adventar-test-firebase-adminsdk-xxx.json | jq .project_id
などで確認できると思います。FIREBASE_AUTH_DOMAIN
は project id に.firebaseapp.com
を付け足したもの、FIREBASE_API_KEY
はFirebaseの設定画面から確認できるはずです。
これで http://localhost:3333/ をブラウザで開いてログイン、カレンダーの作成などができるはずです。
明日は Firebase Authentication についてもう少し詳しく書こうと思います。
Adventar 2019 の技術構成概要
Adventar を支える技術 Advent Calendar 2019 の1日目です。
Adventar はサービスを開始した2012年以来、Rails を利用してサービスを提供してきました。今年はそのシステムを一から設計し直し、以下のような技術要素を使って実装しました。
- Nuxt.js による SPA なフロントエンド
- Go で gRPC の API サーバー
- gRPC-web によるクライアント/サーバー間通信
- Firebase Authentication による認証
- envoy で gRPC の proxy
- AWS Lambda による Nuxt.js の Server Side Rendering
- Terraform による AWS リソースの管理
- Serverless Framework による AWS Lambda, API Gateway のデプロイ
- Amazon ECS, Fargate を利用した API サーバーのホスティング
- Lambda と CloudWatch Events を利用した定義ジョブ
- Lambda と API Gateway を使った画像のリサイズサーバー
他にも色々ありますが、概ねこんな感じです。また、今年からソースコードをオープンソースにしました。
技術選定について
細かいところは追々説明するとして、今回はなぜこういう技術を選定したのか、について書こうと思います。
実装する前からわかっていたことではあるのですが、これぐらいのサービス(tableは3つ程度、トラフィックも小規模)でこのような構成にするのは完全にオーバーテクノロジーで、私が一人で開発する前提であれば Rails で実装して Heroku でホストするのが実装コストも運用コストも絶対に安く済みます。
今回このようなオーバーテクノロジーな構成を選択したのは完全に個人的な学習目的であり、この規模でこのような構成を選択することはオススメしないということを一番最初に断っておきます。
今回の構成にするのに、最初から今の構成に決めて実装したわけではなく、最初に決めたのは、フロントエンドとサーバーサイドは実装を分けて、次の2つを使うということだけです。
理由としては、個人的に学びたいと思っているけど直近で利用する機会がなさそうなので、個人サービスで実験的に導入して試してみたいというのが大きいところです。
その他の構成要素はこれらを軸に実装していく上で、比較検討しながら決めていきました。
- Nuxt.js(フロントエンドのフレームワーク)
- Next.js か Nuxt.js の2択だった
- Vue.js よりは React のほうが好きだが、フレームワーク的に Nuxt.js のほうがよくできていそうだった
- Go(gRPC Server)
- ECS/Fargate(API サーバーのホスティング)
- gRPC をホストしないといけないので Heroku は使えない
- Kubernetes を試してみたい気持ちはあったが自前にしろマネージド(EKS, GKE)にしろ学習コスト、運用コストが高い、マネージドだと料金も高い
- 適当な VPS や EC2 で systemd とかで運用するというのも考えたが自前でサーバーを管理したくないので多少コストはかかるが ECS, Fargate を選択
- Firebase Authentication(認証サービス)
- 対抗は AWS Cognito、Auth0、自前でがんばる
- 自前はめんどくさいので最後の手段
- Cognito は要件を満たしてない、Auth0 はお金かかりそう、ということで Firebase Authentication
- CloudFront(CDN)
- 対抗は Fastly
- 金額などの面で CloudFront を採用(詳しくは後日)
- AWS Lambda(FaaS)
- Serverless Framework(Lambda のデプロイツール)
- Terraform(AWS のリソース管理)
- 特に考える余地なし(対抗は CloudFormation ぐらい?)
だいたいこんな感じです。選定の基準としては
- 技術的チャレンジの有無
- 金銭的コスト
- 時間的コスト(学習コスト、実装コスト、運用コストなどを含む)
- ユーザー体験
あたりのバランスを考えながら選んでいます。例えば時間とお金が無限にあれば、Kubernetes を使ってみたり、普段利用している AWS でなく GCP をフル活用してみたかったですし、何も技術的チャレンジをしないなら Rails, Heroku が一番コスパがいいのはわかっていました。
細かい話しはもっと色々ありますが、あと24日あるので後に譲ることにします。
明日は Adventar をローカルで起動する方法について書きます。
Nginx で grpc-web の proxy
Envoy より Nginx のほうが慣れているので Nginx を使いたい。一応公式でも Docker でビルドできるようになってるけど、無駄にサイズがでかい(2GB超え)しコンパイルオプションがいじりたいので alpine でビルドできるようにしてみた。
https://github.com/hokaccha/grpc-web-proxy-nginx
streamのサンプルも動いた。(client stream はまだ対応してないらしいので server stream だけ)