Slide 1

Slide 1 text

GCP活用事例と実践的Tips Cloud Run + API Gateway から フロントエンドのソリューション選定まで

Slide 2

Slide 2 text

Riku Ogura 2 ● Software Engineer ○ 本業: FE ~ BE 横断のアプリケーション開発(一時は MLも) ○ BonBon: インフラチーム所属・フロントエンド開発も兼任 ● Bio ○ 2018/04 ~ @Yahoo! JAPAN (EC) ■ Java (SpringBoot), Node.js, React + TypeScript, Python(sklearn, keras) ■ プライベートクラウド , マイクロサービス, DDD, gRPC, GraphQL ○ 2021/01 ~ @freee (HR) ■ Ruby on Rails, Golang, React + TypeScript ■ AWS, モジュラモノリス, DDD, kubernetes (EKS) ● Achievements / Activities
 ○ ドメイン駆動設計で保守性をあげたリニューアル事例 〜 ショッピングクーポンの設計紹介 ○ Next.js + NestJS + GraphQLで変化に追従するフロントエンドへ 〜 ショッピングクーポンの事例紹介 ○ Next.js ドキュメント日本語訳プロジェクト | Next.js Documents Japanese Translation Project  tw: @ogugudayo

Slide 3

Slide 3 text

3 今日のアジェンダ API Gateway の活用事例と実践 Cloud Run の活用事例と実践 GCPにおける フロントエンドの ソリューション選定 1 2 3

Slide 4

Slide 4 text

4 今日のアジェンダ Cloud Run の活用事例と実践 GCPにおける フロントエンドの ソリューション選定 2 3 API Gateway の活用事例と実践 1

Slide 5

Slide 5 text

API Gateway: 概要 5 ● GCPのマネージドなゲートウェイサービス (2021/01 GA) ○ 比較対象: Cloud Endpoints, Apigee ● 特徴 ○ OpenAPIによるルーティング記述 ○ 認証レイヤーのサポート ■ ユーザー認証: Custom JWT, Firebase Auth, Auth0, Google ID Tokens ■ サービス間認証(サービスアカウントベース) ○ レートリミット ○ Monitoring & Tracingとの統合 ● BonBonにおける導入目的 ○ 認証レイヤーの吸収 ■ Firebase Auth による認証を API Gateway で一挙に担う ○ 今後増え続けるプロダクト・マイクロサービスを整理・標準化するための API 基盤 ■ Cloud Run サービスや App Engine インスタンスの URL を知らなくてもよい世界 ○ いざというときのモニタリングのポイントを絞るため ■ トレースIDを自動で発行・送出してくれるため、それを伝搬させればトレーシングは充実する

Slide 6

Slide 6 text

API Gateway: 類似ソリューションとの比較 6 API Gateway Cloud Endpoints Apigee ルーティング記述 OpenAPI OpenAPI GUI もしくは OpenAPI コンピューティングの制約 App Engine / Cloud Run / Cloud Run for gRPC / Cloud Functions (ほぼ制約なし ?) コンピューティングとプロトコル (gRPC/OpenAPI) 次第でばらつきあり (参考) 制約なし(オンプレ環境対応) ユーザー認証 Firebase, Auth0, Okta, Google ID Token, カ スタムJWT Firebase, Auth0, Okta, Google ID Token, カスタ ムJWT 任意の OAuth2 準拠の認証方式 サービス間認証 サービスアカウントベース サービスアカウントベース ? 認可 なし なし VerifyJWTポリシーやFirestore連携などを活用 すれば可能 (?) カスタムドメイン 設定可能 (Cloud LBを利用) 設定可能 (Cloud LB 非利用) Apigee側で設定可能 CORS 現時点でサポートなし (2021 H1予定?) allowCorsを記述する サポートあり APIドキュメント生成 なし Cloud Endpoint Portal デベロッパーポータル その他 料金体系 0~200万rps/月 であれば $0.00 料金体系 0~200万rps/月 であれば $0.00 条件付きロジック、手続き型コード組み込み、収 益化機能などなど 「開発側としてはApigeeを使って頂きたいが、オーバースペックに感じる場合はそれ以外を」 @GC_OH

Slide 7

Slide 7 text

API Gateway: BonBon における活用例 7

Slide 8

Slide 8 text

API Gateway: BonBon における活用例 8 ● 各プロダクトのAPIの前段には API Gateway がいる ● ユーザー認証は API Gateway の Firebase Auth を利用

Slide 9

Slide 9 text

API Gateway: BonBon における活用例 9 ● 各プロダクトのAPIの前段には API Gateway がいる ● ユーザー認証は API Gateway の Firebase Auth を利用 ● 認可は専用のマイクロサービスを 作成し、API Gatewayを挟む ● 認可サービスにはAPI Gateway の サービスアカウント認証を利用 ● 各マイクロサービスは自らの サービスアカウントトークンを送出

Slide 10

Slide 10 text

API Gateway: OpenAPI仕様の記述 10 OpenAPI仕様を利用してルーティングルールを記述する (ref. OpenAPI Extensions ) swagger.yaml swagger: '2.0' info: title: example-api x-google-backend: address: https://example-api-reufhirgr-an.a.run.app deadline: 60.0 securityDefinitions: firebase: authorizationUrl: "" flow: "implicit" type: "oauth2" x-google-issuer: "https://securetoken.google.com/example-project" x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com" x-google-audiences: "example-project" security: - firebase: []

Slide 11

Slide 11 text

swagger: '2.0' info: title: example-api x-google-backend: address: https://example-api-reufhirgr-an.a.run.app deadline: 60.0 securityDefinitions: firebase: authorizationUrl: "" flow: "implicit" type: "oauth2" x-google-issuer: "https://securetoken.google.com/example-project" x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com" x-google-audiences: "example-project" security: - firebase: [] API Gateway: OpenAPI仕様の記述 11 OpenAPI仕様を利用してルーティングルールを記述する (ref. OpenAPI Extensions ) swagger.yaml アップストリームのURL (トップレベルで記述した場合は全 path適用) アップストリームへのリクエストにおけるタイムアウト値 (トップレベルで記述した場合は全 path適用)

Slide 12

Slide 12 text

API Gateway: OpenAPI仕様の記述 12 OpenAPI仕様を利用してルーティングルールを記述する (ref. OpenAPI Extensions ) swagger: '2.0' info: title: example-api x-google-backend: address: https://example-api-reufhirgr-an.a.run.app deadline: 60.0 securityDefinitions: firebase: authorizationUrl: "" flow: "implicit" type: "oauth2" x-google-issuer: "https://securetoken.google.com/example-project" x-google-jwks_uri: "https://www.googleapis.com/service_accounts/v1/metadata/x509/securetoken@system.gserviceaccount.com" x-google-audiences: "example-project" security: - firebase: [] swagger.yaml アップストリームのURL (トップレベルで記述した場合は全 path適用) アップストリームへのリクエストにおけるタイムアウト値 (トップレベルで記述した場合は全 path適用) 認証情報の発行者 (Firebase Authの例) APIが受け入れるオーディエンスのリスト JWTの署名検証に利用するプロバイダの公開鍵 URL

Slide 13

Slide 13 text

API Gateway: OpenAPI仕様の記述 13 OpenAPI仕様を利用してルーティングルールを記述する (ref. OpenAPI Extensions ) paths: /aiseki/videos: get: security: - firebase: [] x-google-backend: address: https://aiseki-api-xds2faolwa-an.a.run.app path_translation: APPEND_PATH_TO_ADDRESS tags: - Videos operationId: GetVideos responses: '200': description: '' post: tags: - Videos swagger.yaml “/aiseki/videos” というパスを aiseki-api という Cloud Run サービスにルーティングする例

Slide 14

Slide 14 text

paths: /aiseki/videos: get: security: - firebase: [] x-google-backend: address: https://aiseki-api-xds2faolwa-an.a.run.app path_translation: APPEND_PATH_TO_ADDRESS tags: - Videos operationId: GetVideos responses: '200': description: '' post: tags: - Videos API Gateway: OpenAPI仕様の記述 14 OpenAPI仕様を利用してルーティングルールを記述する (ref. OpenAPI Extensions ) swagger.yaml [GET] /aiseki/videos ● APPEND_PATH_ADDRESS: パスをアップストリームに引き継ぐ ○ https://aiseki-api-xds2faolwa-an.a.run.app/aiseki/videos ● CONSTANT_ADDRESS: パスをアップストリームに引き継がない ○ https://aiseki-api-xds2faolwa-an.a.run.app アップストリームのURL (オペレーション単位での適用 ) “/aiseki/videos” というパスを aiseki-api という Cloud Run サービスにルーティングする例

Slide 15

Slide 15 text

API Gateway: 注意点① 15 ● 注意点: OpenAPIファイルは単一ファイルである必要がある ○ → だが swagger ファイルが肥大化するためプロダクトごとに分割したい ○ → 何らかのCLIを用いて、デプロイ前に分割した swaggerを単一ファイルに統合したい ● 解決策: swagger-combine を採用 ○ すべての $ref を解決して統合してくれる ■ 通常のOpenAPI では使えない場所でも $ref を使える ○ paths / parameters / schemas / tags に対して置換処理を行える ■ 正規表現置換:  { from: /\pet\/(.*)/, to: “/some-prefix/pet/$1” }  ■ functionによる置換:  (path) => `/some-prefix/${path}`  ● 運用方法 ○ 特定dirに配置した swagger.yaml が自動で読み込まれるように (規約) ○ paths などに対して必ずサービス名プレフィックスを付与するようにする ○ OpenAPIをterraform templateとして扱い、tfvarsに定義した変数をバインドする

Slide 16

Slide 16 text

const serviceDirList = (await fs.readdir('./resources/openapi/services', { withFileTypes: true })) .filter(dirent => dirent.isDirectory()) .map(({ name }) => _.kebabCase(name)); // -> [‘aiseki’, ‘porous’, ‘authz’, ...] const createServiceConfig = (serviceName) => { // サービスごとの swagger を読み出す設定を用意する関数 return { url: `./resources/openapi/services/${serviceName}/swagger.yaml`, // 読み込む OpenAPI ファイルの指定 paths: { base: `/${serviceName}`, // ベースパスにサービスプレフィックスを付与 }, securityDefinitions: { rename: { firebase: `${serviceName}-firebase` // securityDefinitions も重複しないようにプレフィックス付与 } }, // tags や operationId などについても同様のリネームを行う } } API Gateway: 運用方法 16 openapi-merge.mjs

Slide 17

Slide 17 text

const swaggerJson = { swagger: "2.0", info: { title: "BonBon API Gateway", description: "An integrated API Gateway across BonBon services.", version: "1.0.0" }, apis: serviceDirList.map(createServiceConfig), // 読み出したサービスDirの配列に対して swagger-combine の設定を作成 } const generate = async () => { try { // merge -> json to yaml -> file output const json = await swaggerCombine(swaggerJson, { useBasePath: true }); const yaml = jsYaml.dump(json, 'utf8'); await fs.writeFile('./resources/openapi/swagger.yaml', yaml); console.log('SUCCESS: OpenAPI spec was integrated.'); } catch(err) { console.error(err); } } API Gateway: 運用方法 17 openapi-merge.mjs

Slide 18

Slide 18 text

paths: /aiseki/videos: get: security: - firebase: [] x-google-backend: address: ${aiseki_api_url} # terraform の templatefile として記述する path_translation: APPEND_PATH_TO_ADDRESS # … API Gateway: 運用方法 18 swagger.yaml locals { api_config = templatefile("${path.module}/../openapi/swagger.yaml", { // templatefile() を利用して swagger nい変数をバインド gateway_project_id = var.project_id, aiseki_api_url = var.aiseki_api_url, // var は apply 時に外部の tfvars ファイルから取得 // ... }) } api_config.tf

Slide 19

Slide 19 text

API Gateway: 運用方法 19 ● API Gatewayに対する CI/CD を Cloud Build で整備 (ref. 公式ドキュメント) ○ CI ■ swagger-combineによるOpenAPIの統合 ■ swagger-cliによるバリデーション ■ terraform plan ○ CD ■ swagger-combineによるOpenAPIの統合 ■ swagger-cliによるバリデーション ■ terraform apply ● API Gatewayのサービスアカウントに 「Cloud Run 起動元」 権限が必要なので、そのロール付与も terraform 内で行う(ひっかかりがち)

Slide 20

Slide 20 text

API Gateway: 注意点② 20 ● 注意点: CORSをサポートしていない ○ API Gateway は Cloud Endpointsにおける allowCors を利用できない ○ ref. API Gatewayユーザーグループの議論 ● 解決策① そもそもCORSが起きないようにする ○ API Gatewayの前段に Cloud LB を挟み、フロントと同じカスタムドメインを当てる ○ ref. 公式ドキュメント

Slide 21

Slide 21 text

API Gateway: 注意点② 21 paths: /api/videos: options: operationId: OptionsVideoList security: [] x-google-backend: address: ${aiseki_api_url} path_translation: APPEND_PATH_TO_ADDRESS responses: '200': $ref: "./swagger.yaml#/responses/options" # ... swagger.yaml ● 解決策② 各 path に対して OPTIONS メソッドを定義する ○ OpenAPI 上の CORSを許容したい path に対して OPTIONS メソッドを定義する ■ ただし、preflight request時には Authorization ヘッダが付与されない ■ そのため、OPTIONS メソッドのみ security: [] として認証を無効化する ○ サーバー側では Access-Controll-Allow-Origin などを返す CORS middleware を実装する 現状は解決策②で対応しつつ、徐々に解決策①を適用していく予定

Slide 22

Slide 22 text

22 今日のアジェンダ API Gateway の活用事例と実践 Cloud Run の活用事例と実践 GCPにおける フロントエンドの ソリューション選定 1 2 3

Slide 23

Slide 23 text

23 今日のアジェンダ GCPにおける フロントエンドの ソリューション選定 3 API Gateway の活用事例と実践 1 Cloud Run の活用事例と実践 2

Slide 24

Slide 24 text

Cloud Run: BonBon における Cloud Run の運用 24 ● コンピューティング ○ バックエンドは Cloud Run を採用 ● コンテナイメージ ○ Google Cloud Buildpacks で作成 ○ Dockerfile のメンテコスト削減 ○ ※ 一部動画サービスは ffmpeg を利用するため独自 Dockerfileを利用 ● 開発環境 ○ 各種IDEの拡張機能 Cloud Code で手軽に起動 ○ minikubeベースで動く Cloud Run のローカルエミュレーター (デプロイも可) ● CI/CD ○ Cloud Build を利用 ○ 他GCPソリューションとの統合が容易(特に引っかかりがちな IAM権限周り) ● 監視 ○ Cloud Logging / Monitoring / Tracing を利用 ○ 他GCPソリューションとの統合が容易

Slide 25

Slide 25 text

Cloud Run: 注意点 25 ● 注意点: 最小インスタンス設定を行っても idle → active の昇格に 約4sec かかる ○ コールドスタート防止のため最小インスタンス設定を 1 にした ○ idle インスタンスが存続するようになったが、 CPU 割当は制限されている ○ 特にシーケンシャルな依存関係を持つ n 個のサービスが Cloud Run で構成される場合、約 4n sec かかる ■ ex) サービスA → サービスB → サービスC の呼び出しで約 12 sec ● 解決策①: Cloud Scheduler の定期ポーリングによるウォームアップ ○ 実装に手を入れる必要がなく手軽かつ課金料金も安く済む ○ Cloud Scheduler の最小実行間隔が 1 min のため、わずかだが暖まっていない時間ができてしまう ● 解決策②: SIGTERM 無限ループ ○ アプリケーション内でSIGTERM通知をハンドリングし、自分自身を呼び出すことで暖める ○ 実装に手を加える必要があるためやや面倒 ● 解決策③: Always on CPU を利用する (2021/09 Preview Release) ○ これによって active 昇格時のオーバーヘッドを根本解決できる ○ 現在 Preview 機能であり、本件当初はまだ発表されていなかった 基本は最小インスタンス設定 or 解決策①で対応、一部 (基盤マイクロサービスなど) で解決策③を適用している状況

Slide 26

Slide 26 text

26 今日のアジェンダ API Gateway の活用事例と実践 Cloud Run の活用事例と実践 GCPにおける フロントエンドの ソリューション選定 1 2 3

Slide 27

Slide 27 text

Cloud Run の活用事例と実践 2 27 今日のアジェンダ API Gateway の活用事例と実践 1 GCPにおける フロントエンドの ソリューション選定 3

Slide 28

Slide 28 text

フロントエンドのソリューション選定: ホスティング手段 28 ● BonBon のフロントエンド技術 ○ 純粋なReact (CSR only) ■ 現状は Firebase Hosting へのデプロイ ○ Next.js (CSR only, no SSR/ISR) ■ ゼロコンフィグの恩恵を受けるため、新規開発では積極的に採用 ■ これまで以下の 1. を選択していたが、最近は 4. を選択している ● GCP における Next.js のホスティング手段 1. Static HTML Export + Firebase Hosting ← API Gatewayとドメイン統合する際に不便 2. Static HTML Export + Cloud Storage + Cloud CDN ← 1. を面倒にしただけ 3. Cloud Functions + Firebase Hosting ← コールドスタート有・ us-central1のみ 4. App Engine + Cloud CDN ← 採用 5. Cloud Run + Cloud CDN ← 4. よりレイテンシが気になる (Always on CPUを使えばアリか)

Slide 29

Slide 29 text

フロントエンドのソリューション選定: ホスティング手段 29 1. Static HTML Export + Firebase Hosting ○ 実現方法 ■ next export すると pages/hoge.tsx は hoge.html として出力される ■ `/hoge` → hoge.html のリライトルールを firebase.json に記述すればよい ■ または cleanUrls: true とすると、”.html” を削除したルールが設定される ○ 問題点 ■ API Gateway の CORS 問題回避のためにフロントとドメインを統合したい ■ その場合、Cloud LB → Firebase Hosting という歪な構成になる

Slide 30

Slide 30 text

フロントエンドのソリューション選定: ホスティング手段 30 3. Cloud Functions + Firebase Hosting ○ 実現方法 ■ Cloud Functions for Firebase に Next.js の SSR サーバーをデプロイ ● 厳密には、サーバースクリプトを Cloud Functions 用の実行ファイル化 ■ 公式example: https://github.com/vercel/next.js/tree/canary/examples/with-firebase-hosting ○ 問題点 ■ 参考記事: Next.jsをFirebase Hostingにデプロイする (ucworkさん) ■ 初回アクセス時のコールドスタートが遅い ■ us-central1 以外のリージョンを利用すると動かない ● 公式「Firebase Hosting に接続されている関数は us-central1 に配置する必要があります。」

Slide 31

Slide 31 text

フロントエンドのソリューション選定: ホスティング手段 31 4. App Engine + Cloud CDN ○ 実現方法 ■ App Engine に Next.js をデプロイ (つまり SSR 前提) ■ App Engine の前段に Cloud LB を置き、LBに対してCloud CDNを有効化する ■ 公式ドキュメントの手順 ● 外部IPアドレスの予約 ● SSL証明書リソースの作成 ● 外部HTTP(S)ロードバランサの作成 ● ロードバランサのIPアドレスを利用するようにDNSレコードを登録 ○ 利点 ■ 同じロードバランサのバックエンドバケットに API Gatewayを追加すれば、バックエンドとフロントエンドのドメ インを統合できる ■ Next.jsアプリをVercelからGoogle Cloudに移行した話 (catnoseさん) ● Cloud CDN は stale-while-revalidate にも対応している ● Cloud CDN ではなく GAE のカスタムドメインだと東京リージョンでレイテンシ悪化

Slide 32

Slide 32 text

フロントエンドのソリューション選定 32 今後の展望や話しきれなかったこと ● ユーザー投稿型プロダクト (動画系サービスなど) の SEO 対策 ○ Next.jsであれば ISR による逐次レンダリングを行う ○ ただし、Vercel 以外だと難しい ○ そこで、SWR (stale-while-revalidate) を利用する ■ キャッシュを利用してコンテンツを返しつつ、裏でオリジンを再検証・キャッシュを更新する ■ 高速なコンテンツデリバリー & 鮮度の高いコンテンツ の両立 ○ Cloud CDN は SWR に対応しているため、積極的に利用していきたい ○ ref. stale-while-revalidate対応のCDNでISRのような挙動を実現する (catnoseさん) ● プライベート npm パッケージの展開 ○ Artifact Registry を利用して共通ID基盤実装の npm パッケージなどを社内配布し始めた ○ Cloud Build との統合も詰まりどころはなく、使いやすかったのでお勧めしたい

Slide 33

Slide 33 text

まとめ 33 ● API Gateway ○ 認証を API Gateway で吸収しつつ、認可はマイクロサービスで担うとよい ■ 認可サービスはサービスアカウント認証を設けている ○ 単一の OpenAPI ファイルである必要がある ■ swagger-combine で統合すると命名の競合が起きなくて楽 ○ CORS をサポートしていない ■ アップストリーム側でも preflight request を考慮する必要がある ■ 今後は Cloud LB を挟んでドメイン統合を行い CORS が起きないようにする ● Cloud Run ○ Cloud Code / Cloud Buildpacks など開発生産性が高い ○ Cloud Run 同士にシーケンシャルな依存関係があるとオーバーヘッドがかさむ ■ Cloud Scheduler による定期ポーリングで低コストに暖機できる ■ 基盤的なマイクロサービスは Always on CPU を適用すべし ● フロントエンドのソリューション選定 ○ Next.jsのホスティング手段は、ドメイン統合・キャッシュ戦略を踏まえて App Engine + Cloud CDN に ○ プライベートパッケージ配布には Artifact Registry が使いやすかった BonBonのプロダクト開発に関わりたい・医療に関わる人に感動と喜びを与えたい方は是非ご連絡を!

Slide 34

Slide 34 text

34 ご清聴ありがとうございました