どうもこんにちは。
前回の記事では、PRにpushするとLambda MicroVMs上でRuboCop、Brakeman、RSpecが動くCI環境を作りました。
今回はその続きです。CIが成功したPRについて、レビュアーがブランチをgit pullしなくても、ブラウザだけで動作を確認できる「レビュー環境」を作りました。CI環境の仕組みは前回の記事で説明したので、この記事は「CIが成功したら」というところから話を進めます。
作りたかったもの
PRをレビューするとき、コードを読むだけでは画面の動きまでは分かりません。動かして確かめるには、ブランチをpullして、手元の環境でサーバを起動し直す必要があります。この手間をなくすのが目的です。
レビュー環境を構築する上で、決めたことは次の3つです。
- 1つのPRにつき、用意する環境は1つ
- 環境を作るのは、CIが成功したコミットだけ
- 環境はリンクを開いたときに起動し、使わなくなったら止める(マージ/却下されたら止める)
Lambda MicroVMsのありがてぇ性質
前回はMicroVMを「テストを流したら終わる箱」として使いましたが、今回は「しばらく動かしておくサーバ」として使います。そこで効いてくるのが、次の3つの性質です。
MicroVMごとに専用のURLが付く
MicroVMにはhttps://<ID>.lambda-microvm.<リージョン>.on.awsのような専用のHTTPSのURLが付き、中のポート8080に転送されます。中でRailsのサーバを8080番で起動しておけば、そのURLで画面を開けます。
ただし、リクエストごとにX-aws-proxy-authヘッダで認証トークンを送る必要があり、トークンは最長60分で切れます。ブラウザは普通の画面遷移で独自のヘッダを付けられないので、レビュアーがこのURLを直接開くことはできません。
これについては、後で説明しますね。
一時停止と再開ができる
アクセスが一定時間無いと自動で一時停止します。また、次のアクセスで自動で再開する設定ができます。
一時停止中は、稼働時間に含まれないため、コストはかかりません。
ただし、スナップショットの保管料だけはどうしてもかかってしまいます。
寿命は最長8時間
どんなに頑張っても踏ん張っても、起動から8時間で終了します。レビューが翌日になれば、環境はもう残っていません。
そのため「CIが成功したら環境を立ち上げておく」のではなく、「リンクを開いたときに環境が無ければ起動する」形にしました。開かれたPRの分しか動かないので、費用も抑えられます。
構成
登場するのは次のものです。CIの部分は前回の記事と同じです。
| 役割 | 使っているもの |
|---|---|
| CIの司令塔 | Lambda Durable Functions |
| リンクを受けて、MicroVMへ転送する中継 | Lambda(Function URL) |
| PRのソースを取得して固める | Lambda MicroVMs(取得専用) |
| Railsアプリケーションを動かす | Lambda MicroVMs(レビュー用) |
| 環境の状態とソースの置き場 | S3 |
| リンクの署名の鍵とGitの資格情報 | Secrets Manager |
CIの司令塔と呼んでいる、オーケストレーターについても前回登場しましたね。
これはDurable Functionsで作成してる、Lambda MicroVMsを起動したり終了したりする役目を担ってます。
構成図にするとこんな感じになります。
処理の順番
ここでは、PR作成かpushを契機としてCIが走っている前提で話を進めますね。
前提を確認したい方はこちらへreturn 前回の記事; してください!
1. CIが成功したら、PRにリンクを貼る
CIの司令塔は、CIが成功すると「このPRでレビュー環境を作ってよいコミット」をS3に記録します。そして、CIの結果コメントに「開く」と「管理」の2つのリンクを出します。イメージとしては以下のような感じです。
それぞれのリンクには、PR番号と期限(14日)に署名を付けています。PRを見られるのはプロジェクトのメンバーだけなので、このリンクを持っていることを「見てよい人」の印として扱っています。
2. レビュアーがリンクを開く
リンクの先は中継のLambdaです。ここでは署名と期限を確かめ、問題なければCookieを発行して、アプリの画面へリダイレクトします。以降のリクエストは、このCookieで確かめます。
3. 中継が環境の状態を確かめ、無ければ起動を頼む
環境が無ければ、中継は司令塔に起動を頼み、すぐに「準備中」のページを返します。
起動を待たずに返すのには、API Gatewayの最大待機時間が30秒という制限に抗うためです。
起動には1分半ほどかかりますが、API Gatewayは30秒で打ち切りますし、ブラウザや社内のプロキシもどこかで待ちきれなくなります。そこで、待つのはページの側に任せることにしました。
起動時間はアプリケーションの規模にもよります。
中継をAPI GatewayではなくFunction URLにしたのも、普段の画面操作で30秒を超える処理があっても切られないようにするためです。
複数のレビュアーが同時にリンクを開いても、起動は1回だけにしています。環境の状態をS3に書くときに条件付き書き込みを使い、最初に書けたリクエストだけが起動を頼む仕組みです。
4. 取得専用のMicroVMが、PRのソースを用意する
司令塔は、まず取得専用のMicroVMを起動します。このMicroVMはGitの資格情報を読めて、PRのコミットをcloneし、git archiveでtar.gzに固めてS3に置きます。これが完了したらすぐに環境は停止します。
わざわざ分けたのは、レビュー用のMicroVMにGitの資格情報を持たせないためです。レビュー用の環境ではPRのコードが最長8時間動き続けるので、その間に資格情報を読める状態にはしたくありませんでした。何かの間違いで、レビュー環境のURLが流出してしまった場合に、なるべく資格情報は持たせたくないなぁと思い...
5. レビュー用のMicroVMが、Railsアプリケーションを起動する
次に、レビュー用のMicroVMを起動します。こちらはシークレットを読めない実行ロールで動き、S3からソースを取って展開するところから始めます。展開のときは、絶対パスや..を含むファイル、外を指すシンボリックリンクを拒否しています。
そのあとは、いつもの手順です。
bundle install- データベースの作成と
db:seed rails server -b 0.0.0.0 -p 8080
Dockerfileを使っても大丈夫です。
が、MicroVM自体がDockerfileみたいなサンドボックス環境なので、MicroVMの中にコンテナを入れる構成にしたら冗長かなぁと思って、そのまま起動する形をとっています。
MicroVMのイメージは、Ruby、MySQL、gemを入れた状態をスナップショットにしてあるので、bundle installはほとんど一瞬で終わります。サーバが起動したら、runnerが自分でログイン画面とそのCSS、JSを1回読み込んでから「準備完了」を報告します。
MicroVM内では、developモードでアプリケーションを起動していますが、このモードでは最初のアクセスでアセットのコンパイルが実行されます。それをユーザに踏ませることはしたくないので、一回読み込む形をとっています。
6. 準備中のページが、自動で画面に切り替わる
準備中のページは、3秒おきに中継へ状態を問い合わせ、「ソースを取得しています」「データベースを準備しています」のように今の段階を表示します。準備ができると自動で再読み込みして、アプリのログイン画面に切り替わります。
7. 中継が、リクエストをMicroVMに転送する
準備ができたあとは、中継がブラウザからのリクエストに認証トークンのヘッダを付けて、MicroVMに転送します。トークンは、期限が切れる少し前まで中継の中で使い回しています。
ほかに、次のヘッダを組み立て直しています。
-
X-Forwarded-HostとX-Forwarded-Protoを中継Lambdaからのものにします- RailsのCSRFの検査を通すため
- 中継LambdaのCookieは、MicroVMには渡しません
- ブラウザから届いた
X-Forwarded-*などは不要なので使いません
8. 環境を閉じる
環境は、次のどれかで止まります。
| きっかけ | 止め方 |
|---|---|
| 同じPRに新しいpushがあった | 新しいCIが始まった時点で止めます |
| PRがマージかクローズされた | BacklogのWebhook(プルリクエストの更新)を受けて止めます |
| 管理用のページで「環境を終了する」を押した | その場で止めます |
| 30分アクセスが無い | 一時停止し、その1時間後に終了します |
| 起動から8時間たった | MicroVMの寿命で終了します |
一時停止中にアクセスがあれば自動で再開します。寿命やアイドルで終わったあとにリンクを開けば、また起動します。
Railsアプリケーションをdevelopmentモードで動かすための設定
レビュー用の環境では、アプリをdevelopmentモードで動かしています。アプリのリポジトリには手を入れず、環境変数と、展開した作業ディレクトリの中だけで対応しました。
RAILS_ENV=development
DATABASE_URL=mysql2://root@127.0.0.1:3306/app_development
RAILS_DEVELOPMENT_HOSTS=.lambda-microvm.ap-northeast-1.on.aws,.lambda-url.ap-northeast-1.on.aws
RAILS_DEVELOPMENT_HOSTSが無いと、RailsのHost Authorizationが、知らないホスト名からのアクセスを403で弾きます。先頭に.を付けると、その1つ下のサブドメインをまとめて許可できます。
これ僕は初めて知った...
あとは外部のサービスにデータを投げつけないように制限とかはしてます。
ハマったところ
「環境を終了する」を押すと403になった
管理用のページの「環境を終了する」は、他のサイトからのフォーム送信を受け付けないよう、Originヘッダが中継自身のオリジンと一致するかを確かめていました。ところが実際に押すと、毎回403になりました。
原因は、中継のページに付けていたReferrer-Policy: no-referrerでした。このポリシーのページからフォームをPOSTすると、ブラウザはOriginをnullにして送ります。テストではモックに正しいOriginを入れていたので、気づけませんでした。
ポリシーをsame-originに変え、あわせてOriginがnullのときはSec-Fetch-Site: same-originで同じオリジンからの送信かを確かめるようにして直りました。
終了した直後に、環境が起き上がった
「環境を終了する」で止めた2秒後に、新しい環境の起動が始まっていました。中継のログを見ると、別のタブで開いていたアプリの画面からリクエストが来て、「環境が無いので起動する」という判断が走っていました。
人が閉じたのに勝手に起き上がるのは分かりにくいので、手動で終了したときは、どのタブからアクセスしても「環境は終了しました」のページを出すようにしました。起動し直すのは、そのページの「起動する」ボタンを押したときだけです。寿命やアイドルで終わったときは、今までどおりリンクを開けば自動で起動します。
実際の所要時間
リンクを開いてから、ログイン画面が出るまでの内訳です。実際のPRで計測しました。
| 段階 | 時間 |
|---|---|
| 取得専用のMicroVMの起動、clone、tar.gzの作成とアップロード | 13秒 |
| レビュー用のMicroVMでのソースの展開 | 1秒 |
| bundle | 3秒 |
| データベースの作成とseed | 56秒 |
| サーバの起動とアセットの読み込み | 14秒 |
| 合計(リンクを開いてから使えるまで) | 約1分半(91秒) |
MicroVM自体の起動は、スナップショットから3秒ほどです。時間の大半はseedで、ここはアプリ側の処理です。
合計値はアプリケーション側の規模にもよるんでね、そこは注意!
今後
今後は、レビュー環境内の設定を変えたりしながら動作検証をしていけるような状態にできたらと思っています。
最新のソースをgit pullせずにレビューできる環境があるのはレビュアーとしては嬉しいので、作って良かったなぁと思います。
また、CI実行はCodeBuildでできますが、一時的なレビュー環境の用意はCodeBuildではできなくて、Lambda MicroVMsならではの使い方なんじゃないかなぁと思っています。
まぁいまだに「Lambda」を名乗ってるのに納得いってませんけどね!
以上!



