Adventar のインフラ概要
Adventarを支える技術 Advent Calendar 2019 の16日目です。
今年の Adventar のインフラはほとんど AWS を使って構築しています。AWS 以外だと、 Firebase Authentication や Bugsnag などのサービスも使っていますが、今回は AWS の構成について説明します。
インフラの構成は基本的に terraform で管理していて、コードは以下にあります。API Gateway や Lambda の管理には Serverless Framework、ECS の管理には ecs-cli を使っていたりするので、それらは terraform 外での管理になっています。
https://github.com/adventar/adventar/tree/619f222b9348e1cbfcfe50cc731fb8184e84ab2d/terraform
構成図は以下のような感じです。
それぞれ説明していきます。
www.adventar.org
これは最も簡単なシステムで、www.adventar.org
というドメインの旧 URL を www
なしの adventar.org
にリダイレクトするためだけの存在です。S3 には、空の bucket を使ってホスト名のリダイレクトができるという機能があるのでこれを使っています。
https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html
S3 の設定は簡単で、これだけです。
http
だけであればこの bucket にエイリアスレコードを設定すれば終わりですが、https
も対応したいので、CloudFront に ACM の証明書を刺して https
も受けられるようにしています。
img.adventar.org
画像のリサイズサーバーです。詳細は以下に書きました。
上記の記事でも書いたとおり、DB や API サーバーに依存しないので、構成をシンプルに保つことができます。
adventar.org
ユーザーフェイシングな HTML や静的ファイルのリクエストを受けます。詳細は以下に書きました。
図にもあるように、SSR をしている Lambda が API サーバーへリクエストしています。このリクエストは本来は VPC 内部で Lambda を起動して内部通信するのがいいのですが、めんどくさいのでインターネット経由になっています。
api.adventar.org
API サーバーです。VPC の中に配置した ALB がリクエストを受けて、その後ろにいる ECS がいて、ECS では Fargate でタスクがいて、Envoy と Go による gRPC のサーバーが動いています。DB は同じ VPC に配置されている RDS です。gRPC や Envoy の話は以下に書きました。
バッチジョブ
VPC の中にいる CloudWatch Events と Lambda は、定期実行されるバッチジョブです。詳細は以下に書きました。
まとめ
Adventar の AWS の構成についての概要を書きました。明日はインフラのコストについて書こうと思います。
schemalex による DB のスキーマ管理
Adventarを支える技術 Advent Calendar 2019 の15日目です。
今日は DB のスキーマ管理について書きます。
Rails の DB マイグレーションと Ridgepole
Adventar は昨年まで Rails で作っていて、DB の マイグレーションも Rails デフォルトの機能を使っていました。Rails の DB マイグレーションは、それなりによくできてはいますが、差分を積み上げていくので大量のマイグレーションファイルができて煩雑になる、多人数での開発の場合にコンフリクトしやすいなど、いくつか問題があります。
個人的にはこういった問題もあるので、Ridgepole のように DB のスキーマ定義だけを管理し、現在のスキーマとの差分を計算して ALTER 文を発行してくれるような仕組みのほうが好きです。
最初は Ridgepole を使おうと思ったのですが、API サーバーは Go で書こうと思っていたので、スキーマ管理のためだけに Ruby 依存を入れるのは微妙かな、と思い他のツールを検討しました。
schemalex と sqldef
Go 製の Ridgepole 的なツールは
の2つがあるのを知っていたので、どちらかを使うことにしました。これらのツールは、Ridgepole と違って、Ruby の DSL でなく、SQL でスキーマを管理できる点、Ruby のランタイムなどが不要でバイナリ単体で実行できて便利です。
ほとんど機能の違いはなのですが、大きい違いは schemalex が MySQL だけをサポートしているのにたいして、sqldef は PostgreSQL もサポートしています。今回は MySQL なのでどちらでもよかったのですが、schemalex のほうが歴史が深く、どちらかといえば安定していそうだったので schemalex を選びました。
ちなみに、偶然にも Ridgepole, schemalex, sqldef は全部同僚 or 元同僚が作っています。
schemalex による DB のマイグレーション
スキーマの管理には、以下のような普通の SQL の DDL を使います。
CREATE TABLE `users` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `auth_uid` varchar(255) NOT NULL, `auth_provider` varchar(20) NOT NULL, `icon_url` text NOT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `index_users_on_uid_and_provider` (`auth_uid`,`auth_provider`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
実際の Adventar のスキーマは以下のような感じです。
https://github.com/adventar/adventar/blob/619f222b9348e1cbfcfe50cc731fb8184e84ab2d/db/schema.sql
これはただの SQL なので、schemalex を使わずとも、初回のテーブル作成はできます。
$ mysql -u root adventar_dev < schema.sql
ここで新しくカラムを追加したい場合、例えば以下のような変更を加えます。
--- a/db/schema.sql +++ b/db/schema.sql @@ -1,6 +1,7 @@ CREATE TABLE `users` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, + `description` text NOT NULL, `auth_uid` varchar(255) NOT NULL, `auth_provider` varchar(20) NOT NULL, `icon_url` text NOT NULL,
schemalex を使うと、既存の DB のスキーマと上記 SQL の diff を計算して ALTER 文を作ってくれます。
$ schemalex 'mysql://root@tcp(127.0.0.1:13306)/adventar_dev' schema.sql BEGIN; SET FOREIGN_KEY_CHECKS = 0; ALTER TABLE `users` ADD COLUMN `description` TEXT NOT NULL AFTER `name`; SET FOREIGN_KEY_CHECKS = 1; COMMIT;
schemalex がやってくれるのは ALTER 文の生成だけなので、MySQL に食わせるのは自分でやります。
$ schemalex 'mysql://root@tcp(127.0.0.1:13306)/adventar_dev' | mysql -u root adventar_dev
実際の運用
複数人で運用するときのフローや、デプロイ時に適用場合のフローなども紹介できればよかったのですが、Adventar はほぼ一人で開発しているし、プロとして恥ずべき行為ではありますが、デプロイも手元から手動でやっているので、特に運用について言及できることはありませんでした(つまり手動 ALTER でも十分そうということですね...!)
まとめ
Lambda を使った画像のリサイズサーバー
Adventarを支える技術 Advent Calendar 2019 の14日目です。
今日は Lambda , API Gateway, CloudFront などを使って、画像のリサイズサーバーを作る話を書きます。
Adventar における画像のリサイズ
Adventar は記事を投稿した際に、記事の関連画像を取ってきて表示する機能があります。
これは OGP などのメタタグから画像の URL を取得していますが、取得した URL をそのまま使用して画像を取得すると、いくつか問題があります。
- 毎回リクエストが飛ぶので行儀が悪い
- 画像の URL が
http
(非 SSL) の場合に mixed contents になる - 画像サイズが大きいとパフォーマンスに影響する(OGP の画像は 1000px 超えで設定してるケースも多い)
- Adventar でほしいのはせいぜい、100px〜200pxぐらい
これを解決するために、取得した画像をhttps
で配信し、適切なサイズにリサイズし、キャッシュするようなサーバーがあるとよさそうです。
画像のリサイズは Fastly や ImageFlux などのサービス、ngx_small_light などの OSS を使うなど、様々な方法がありますが、今回は自前で画像を持っているわけではなく、外部サイトの画像を取得して利用したいという少し違うユースケースというのもあり、Lambda と CloudFront で自作することにしました。
といっても全然難しい仕組みではなく、画像 URL を受け取ってリサイズして画像を返す Go のプログラムを Lambda で動かして、CloudFront でキャッシュしているだけです。Go のコードも100行ちょっとの簡単なものです。
いくつか工夫していているとこがあるので説明します。
DB に依存させない
まず、一つ目が DB に依存せずに動作させるということです。画像の URL は、DB に保存されていますので、画像 URL を知るためには、カレンダーの投稿 ID を受け取って DB にアクセスし画像 URL を取ってくる、ということが必要になりますが、そのためだけに DB にアクセスするのはめんどくさいので、今回はリクエスト URL に直接画像の URL を埋め込んでいます。
https://img.adventar.org/?url=<画像URL>
のような感じです。Lambda でこの画像 URL を fetch して、適当なサイズにリサイズして返します。本当はサイズの指定もクエリでできるようにしたいのですが、とりあえず今は1サイズしかないので固定にしています。
ハードコードでだいぶひどい感じですが、かなりギリギリになって実装したので雑コードです(投稿が始まる12月までにあればいいので実装を一番最後に回した)。
ダイジェスト値による検証
ただ、これだと任意の URL のプロキシになってしまってよろしくないので、ダイジェスト値を設定して URL が改ざんされていないかを検証しています。OSS なのでコードを読めばわかりますが、単に画像 URL に SALT を混ぜて sha1 を取っているだけの簡単なものです。なので最終的な URL はこのような感じになっています。
https://img.adventar.org/img/<digest>?url=<画像URL>
実際の URL はこんな感じです。
デプロイ
フロントエンドのデプロイ や バッチジョブのデプロイ にも利用した Serverless を利用しました。今回 Serverless 大活躍です。設定はこんな感じで何も難しくない。
デプロイスクリプトも
$ serverless deploy
で終了。
まとめ
Lambda を使った画像のリサイズサーバーについて書きました。明日は DB のスキーマ管理について書きます。
Lambda と CloudWatch Events を利用した定期ジョブを Serverless で管理する
Adventarを支える技術 Advent Calendar 2019 の13日目です。
今日は定期ジョブの実行について書きます。定期ジョブというのは cron とかで定期的にプログラムを実行するやつのことです。
実行するプログラム
Adventar では、定期ジョブは一つだけしかなくて、定期的に投稿された URL のメタデータをフェッチしてくる、というものです。
Adventar は自分の担当の日に URL を投稿すると、その URL のタイトルと画像を取ってきて表示する機能があります。普段は URL を投稿するリクエスト中でその処理をしていますが、これだと予約投稿のような機能を使いたいときに困ります。
例えば 12/10 に自分の番がくるけど、記事はその前に書いて hatena blog の予約投稿で 12/10 の 0 時に公開されるように設定しておく、Adventar にも前日までにその URL をセットしておく、というのはユースケースとして考えられます。なお、このとき Adventar はその日にならないと投稿した URL は投稿者以外からは見えないようになっています(ネタバレになると面白くないので)。しかし、URL が Adventar に登録された時点では hatena blog のほうもまだ公開されていない状態なので、タイトルや画像が取れません。
そこで、1時間に一回定期ジョブを実行して、その日に URL があるけどタイトルや画像が空のレコードに対して、その URL にアクセスしてメタデータを取ってくる、というジョブを動かしたいのです。
ジョブのプログラムは簡単で、こんな感じです。
CloudWatch Events と Lambda
今回は常駐するサーバーがないので cron を実行する環境がありません。いくつか方法はありますが、CloudWatch Events を使うことにしました。CloudWatch Events は cron のような書式で定期的に何かの処理をトリガーすることができるので、今回はこれを利用します。
CloudWatch Events のトリガーには様々なものが設定できますが、今回のケースは、Lambda か ECS tasks でプログラムを実行するのがよさそうです。
どちらでもよかったのですが、Lambda であれば Serverless フレームワークを使って管理することができそうだったので、今回は Lambda で実行することにしました。
追記: これを書いた後で諸事情により ECS に変更したので、現在は ECS で動いています。
Serverless を使った設定
特に難しいことはなくて、以下のような設定を書くだけです。
これで Serverless が Go のビルドからデプロイ、CloudWatch Events の設定まで全部やってくれます。便利。
注意しないといけないのは、時間が UTC でしか設定できないので、JST で 12/1 の 0 時から12/25 まで、という設定は書きのように少しトリッキーな記述が必要です。なお、これだと12/26にも少し実行されますが、実行されても害はないのでさぼってます。
events: - schedule: cron(0 15-23 30 11 ? *) - schedule: cron(0 * 1-25 12 ? *)
まとめ
今日は Serverless で定期ジョブを管理する方法について書きました。明日は Lambda を利用した画像のリサイズサーバーについて書きます。
ecs-cli を利用したAPI サーバーのデプロイ
Adventarを支える技術 Advent Calendar 2019 の12日目です。
今日は API サーバーのデプロイについて書きます。
API サーバーは、4日目の記事 でも書いたように、gRPC のサーバーをたてています。この API サーバーは、Amazon ECS, Fargate を利用してホストしています。
ECS のデプロイツールは色々とあって、仕事で使っているのは hako というツールです。今回は他のツールも使ってみたいというのもあって、AWS 公式が提供している ecs-cli を使ってみました。
https://github.com/aws/amazon-ecs-cli
ecs-cli の他の良い点としては、docker-compose.yml
を設定ファイルとして使えるというところじゃないかなと思っています。docker-compose.yml
であれば、ツール独自のシンタックスを覚えなくても良いのがいいですね。
ただ、当然のようにdocker-compose.yml
だけでは足りなくて、ECS 独自の設定も別途必要になります。それはecs-params.yml
という設定ファルに書くのですが、設定ファイルが分散してしまうのが微妙という見方もありそうです。
Adventar ではそれぞれ以下に設定があります。
ecs-cli のコマンド
compose create service
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cmd-ecs-cli-compose-service-create.html
サービスを作ります。実行するのは初回だけです。ALB と target group は別途 terraform で作っています。
$ ecs-cli compose --project-name adventar --cluster adventar --region ap-northeast-1 service create --create-log-groups --launch-type FARGATE --container-name envoy --container-port 80 --target-group-arn arn:aws:elasticloadbalancing:ap-northeast-1:287379415997:targetgroup/adventar-api/xxx
compose service up
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cmd-ecs-cli-compose-service-up.html
これが実質デプロイコマンドです。
docker-compose.yml と ecs-params.yml の設定を見てタスク定義を作り、新しいタスクをデプロイしてくれます。
$ ecs-cli compose --project-name adventar --cluster adventar service up
なお、ドキュメントには
a combination of the create and start commands
とあるので、start
でもいいのかと思いきや、start
だと新しいタスクのデプロイは行わません。また、
This command updates the desired count of the service to 1.
とあるので、desired count が 2 以上だった場合は 1 になるのかと思いきや、既存の desired count をそのまま引き継いでくれます。ドキュメントを読んだだけだとこのあたりの挙動はよくわかりませんでした。
compose service scale
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/cmd-ecs-cli-compose-service-scale.html
タスク数(desired count)を変更します。
$ ecs-cli compose --project-name adventar --cluster adventar service scale 2
docker push
デプロイする際は、ECS のデプロイの前に docker push
する必要があるので、git の HEAD の revision で tag を打って dockdr push
してからデプロイしています。こんな感じです。
TAG=$(git rev-parse HEAD) cd $ROOT_DIR/grpc-server docker build -t hokaccha/adventar-grpc-server:${TAG} . docker push hokaccha/adventar-grpc-server:${TAG} cd $ROOT_DIR/envoy docker build -t hokaccha/adventar-envoy:${TAG} . docker push hokaccha/adventar-envoy:${TAG}
今回はコードもオープンですし、ECR は利用せずに Docker Hub の public なところに push しています。
実際のデプロイスクリプトはこんな感じです。
まとめ
API サーバーを ECS でデプロイする話しを書きました。明日は Lambda と CloudWatch Events を使った定期ジョブについて書きます。
フロントエンドのデプロイ
Adventarを支える技術 Advent Calendar 2019 の11日目です。
今日はフロントエンドのデプロイについて書きます。フロントエンドの構成は昨日の記事で書いたとおり、一部を Lambda で SSR していて、静的コンテンツは S3 で配信しています。
なので、S3 と Lambda の両方にデプロイする必要があってやや面倒です。
S3 へのデプロイ
まず、静的コンテンツの S3 へのデプロイ自体は簡単で、nuxt build
で生成された成果物(デフォルトではdist
ディレクトリ)を S3 にアップロードするだけです。
$ nuxt build $ aws s3 sync ./dist/ s3://your-bucket/prefix
SSR しなければ、これだけで Nuxt.js のアプリケーションは配信できます。注意する点としては、SPA の場合クライアント側でルーティングするので、すべてのパスに対して index.html
を返す必要があるという点です。
CloudFront と S3 でやる場合は、ファイルが存在しない場合 CloudFront には S3 から 403 が返るので、これをハンドリングして index.html
を返します。terraform で書くとこんな感じです。
custom_error_response { error_code = "403" response_code = "200" response_page_path = "/index.html" }
SSR のためのビルド
今回、一部だけ SSR するという構成にしたのでめんどくさかった点がこれで、Nuxt.js は SPA の場合は spa mode でビルド、SSR の場合は universal mode でビルドする必要があります。
https://nuxtjs.org/api/configuration-mode/
なので、ビルドプロセスも両方でやる必要があるのですが、spa mode と universal mode でビルドした JS ファイルのダイジェスト値が微妙に食い違ったりしたということがありました。そうするとどうなるかという、spa mode でビルドして S3 にアップロードした中に、universal mode からで生成した HTML に含まれる JS のファイル名がなくて JS が 404 になって動かない、ということが発生しました。たぶんこんな使い方想定してないのでエッジケースだと思います。
Nuxt.js を直すほどの元気はなかったので、universal mode でビルドした結果も S3 にアップすることでしのぎました。なんとかこれで動いてます。
以下が苦肉の策のコードです。
Serverless フレームワークを使った API Gateway と Lambda のデプロイ
次に、SSR するための Lambda のデプロイですが、これには今回 Serverless Framework を使いました。API Gateway や Lambda のデプロイは、Serverless や AWS SAM などを使うと便利です。SAM でもよかったのですが、今回は知見のあった Serverless を採用しました。
Serverless でのデプロイはそんなに難しいことはしていなくて、以下のような設定ファイルを書くだけです。
これで Nuxt.js のビルドを実行後に
$ serverless deploy
で API Gateway と Lambda にデプロイできます。
また、サーバーのコードも量が少ないとはいえ TypeScript で管理したいのと、一部クライアントの TypeScript のコードに依存があるので webpack で変換して bundle することにしました。
本当は Lambda にアップロードする容量を削減のため、必要なコードだけ bundle して一枚の JS にてアップロードしたかったのですが、色々とうまくいかずに bundle するのは諦め、webpakc で node_modules の解決は無視して、node_moduels をアップロードすることにして対応しました。Lambda の容量の上限に引っかかるほど大きくはないので一旦これで妥協しています。
キャッシュのパージ
今回 S3 にアップするアセットは CloudFront でキャッシュしています。JS や CSS はダイジェスト値がついていてファイル名がユニークになるのでキャッシュのパージは必要ありませんが、index.html
や sw.js
などはキャッシュのパージが必要です。また、本来は SSR した結果もキャッシュしたいと思っていたので、上記に書いたデプロイの処理をおこなったあとにキャッシュをパージする必要があります。CloudFront のキャッシュのパージは以下のようにします。
aws cloudfront create-invalidation --distribution-id xxx --paths '/*'
最終的な、デプロイスクリプトは以下ような感じです。
まとめ
フロントエンドのデプロイについて書きました。Nuxt.js と Serverless のおかげで、そんなに複雑には見えませんが、ここまでたどり着くにはけっこうな時間を消費しました...。
明日はこの流れで API サーバーのデプロイについて書きます。
Server Side Rendering の技術概要
Adventarを支える技術 Advent Calendar 2019 の10日目です。
今日は Adventar の Server Side Rendering(以下 SSR)している技術構成について書きます。
インフラ構成
まず、先日の記事に書いたように、Adventar ではすべてのページを SSR しているのではなく、カレンダーの詳細ページだけを SSR しています。SSR するカレンダー詳細は API Gateway で受けて Lambda でレンダリングしており、そうでない静的なアセットは S3 で配信しています。その前段のルーティングは CloufFront でおこなっています。
S3 で捌いているのは、JS や CSS などの静的ファイルは当然ですが、例えばトップページなどの HTML も S3 が返します。ただしこの HTML は SSR されていない、JS と CSS のリンクだけしかない HTML で、クライアントサイドで API を呼び出してコンテンツをレンダリングします。
なぜ Lambda か
Lambda でも Cloud Functions でもよかったのですが、単に他のインフラ構成要素が AWS になったからで、Cloud Functions との技術的な比較はしていません。
常駐型のサーバーと比べて Lambda などのサーバーレス環境を利用するのは、管理コスト(費用、手間)が安く済むというのが大きいでしょう。うまく使えば安く済むし、サーバーを管理しなくていいというのは非常に大きいメリットです。
一方デメリットとしては、パフォーマンスなどが挙げられると思います。Lambda は遅くはないですが、コールドスタートなどのデメリットもあるし、常駐型のサーバーと比べるとパフォーマンスの最適化はしづらい側面があると思っています。これについては、先日発表された Provisioned Concurrency によって解決できるかもしれないですけどまだちゃんと試してません。
https://aws.amazon.com/jp/blogs/aws/new-provisioned-concurrency-for-lambda-functions/
今回は SSR した結果を CDN でキャッシュするという予定だったので、パフォーマンス、費用面ともに常駐サーバーよりは有利になる予定でした。前日にも書いたとおり、現状は一旦キャッシュの実装は見送って毎回 Lambda が呼ばれていますが、費用的にはサーバーを用意するよりは安く済みそうです。パフォーマンスは HTML のレスポンスタイムが200ms〜300ms ぐらいかかっているので、速くはないが、激遅でもないという感じです。
Lambda で Nuxt.js の SSR
Nuxt.js は
$ nuxt start
で SSR なサーバーを起動することができ、レールを外れずに使う場合はこれでいいのですが、今回は Lambda を使って SSR したいのでこれは使えません。
Nuxt.js はカスタムサーバーを利用する場合、nuxt.render
という express の middleware などと互換性のある API を提供しているのでこれを使います。
https://nuxtjs.org/api/nuxt-render/
また、Lambda で express などのサーバーを利用するための serverless-http というモジュールがあるので、これも利用します。
https://www.npmjs.com/package/serverless-http
これらを利用すると、以下のような感じで Nuxt.js を Lambda で利用できます。
import { Nuxt } from "nuxt"; import serverless from "serverless-http"; import express from "express"; import config from "~/nuxt.config"; const app = express(); const nuxt = new Nuxt(config); app.use(nuxt.render); module.exports.handler = serverless(app);
他にも RSS などの動的コンテンツを同時に配信しているので、少し複雑になっていますが、コードは以下のような感じです。
これを Lambda で実行すれば、クライアント側で同じコードで HTML が生成されて返ってきます。前述したように、API Gateway を通してカレンダーページだけでこれを実行しているわけです。
まとめ
Adventar における SSR の技術構成について書きました。明日は SSR を含めた、フロントエンドのデプロイについて書こうと思います。