View のモバイル対応
Adventarを支える技術 Advent Calendar 2019 の21日目です。
このご時世には信じがたい話ですが、去年まで Adventar はスマホで見ると PC View を縮小するだけで、スマホでは非常に使いづらいサービスでした。今年のシステムリニューアルでは機能追加したりや見た目を変える余裕はなかったのですが、さすがに恥ずかしい、アクセスも iOS が一番多いという言い訳のできない証拠があったので、モバイル対応だけはやることにしました。
レスポンシブと出し分け
モバイル対応(というかマルチデバイス対応)は、大きく分けて2つの手法があって、HTML は同一で、画面の大きさによって CSS を変更することで対応するレスポンシブ(Webデザイン)と言われる手法、ユーザーエージェントなどの情報を元に、スマホ用や PC 用などの HTML を出し分ける方法があります。
機能やレイアウトがガラっと変わるのであれば出し分けのほうがよいですが、スタイルの変更だけで要件がまかなえるのであればレスポンシブのほうが楽な場合が多いです。
個人的には、スマホで見たときと PC で見たときにコンテンツの位置が全然違ったり、PC にあった機能がスマホだとなくなっているという体験が嫌いなので、今回はレスポンシブで対応しました。
ブレイクポイントを決める
ブレイクポイントは感覚がよくわからないので Twitter Bootstrap を参考にしました。
https://getbootstrap.com/docs/4.3/layout/overview/#responsive-breakpoints
// Extra small devices (portrait phones, less than 576px) // No media query for `xs` since this is the default in Bootstrap // Small devices (landscape phones, 576px and up) @media (min-width: 576px) { ... } // Medium devices (tablets, 768px and up) @media (min-width: 768px) { ... } // Large devices (desktops, 992px and up) @media (min-width: 992px) { ... } // Extra large devices (large desktops, 1200px and up) @media (min-width: 1200px) { ... }
もともと Adventar の PC View は最大幅が 1000px で、992px, 1200px あたりは不要だったので、576px と 768px だけ採用し、以下の3パターンに絞りました。
サイズ | 幅 |
---|---|
Small | width < 576px |
Medium | 576px <= width < 768px |
Large | 768px <= width |
Small
Medium
Large
モバイルファーストで作る
PC 用のスタイルを画面の小さいスマホ向けに変更していくのはけっこう難しいのと、基本的に PC で開発するのでモバイルの確認が漏れがち、などの理由があり、モバイル向けの画面をプライマリで作り、そこから幅を広げた場合の画面を作っていくことにしました。
実装としては、メディアクエリに max-width
でなく min-width
を使うのがポイントです。例えば480pxをブレイクポイントにしてスタイルを変更するケースを考えてみます。max-width
を使うとこうです。
// PC 用 .btn { font-size: 20px; } // スマホ用 @media (max-width: 480px) { .btn { font-size: 14px; } }
min-width
の場合はこう。
// スマホ用 .btn { font-size: 14px; } // PC 用 @media (min-width: 481px) { .btn { font-size: 20px; } }
基本的には media query で元スタイルを上書きしていくほうがやりやすいので、min-width
を使うと自然と画面が小さいサイズ向けのスタイルがプライマリになります。
まとめ
UI をモバイル対応した話を書きました。明日は細かすぎて伝わらない UI の工夫について書こうと思います(ネタ切れです)。
Bugsnagを利用したエラートラッキング
Adventarを支える技術 Advent Calendar 2019 の20日目です。
今日はエラートラッキングについて書きます。
Bugsnag
エラートラッキングのサービスは色々あって、有名なのは Sentry や Airbrake あたりでしょうか。今回は Bugsnag というサービスを利用しました。これは Rails 時代から使っていて、無料で使える範囲が一番大きそう、という基準で選びました。
Bugsnag だと 250 events/day は無料枠で使えるので、Adventar ぐらいの規模であればスパイクしなければ余裕です。Sentry も今見たら無料で 5000 events/month なのでこっちでもいけそうな気がします(昔からこうだっけな)。
今回は Go の gRPC サーバー、Nuxt.js で SSR しているところに Bugsnag を使っています。フロントエンドの JS でも動くのですが、経験上フロントエンドでのエラートラッキングはノイズが多く、無料枠を食いつぶしてしまう可能性がありそうだったので今回は入れていません。もしかしたらそんなことはなくて、意外とさくっといける可能性はあります。
Rails では何もはまらずに使えていたのですが、Go と Node.js に有効にするのにけっこう苦労したので、それについて書いておきます。
Go/gRPCでの利用
Bugsnag の Go SDK はいくつかのフレームークに対応してますが、gRPC はありませんでした。
https://docs.bugsnag.com/platforms/go/
なのでOther Go appsを見て自力でどうにかする必要がありそうです。
Go のサーバーでエラーをトラッキングしたいのは主に
- 予期せぬエラーになった場合
- panic で死んだ場合
の2つで、この場合に
bugsnag.Notify(err, ctx)
を呼べばよさそうです。最終的には以下のようなコードを Interceptor に刺しこみました。
func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) { defer func() { if r := recover(); r != nil { err = grpc.Errorf(codes.Internal, "Internal Server Error") fmt.Printf("%s\n", r) if bugsnagAPIKey != "" { bugsnag.Notify(fmt.Errorf("%s", r), ctx) } } }() resp, err := handler(ctx, req) s, _ := status.FromError(err) if s.Code() == codes.Unknown { stacktrace := fmt.Sprintf("%+v\n", err) fmt.Print(stacktrace) if bugsnagAPIKey != "" { bugsnag.Notify(err, ctx, bugsnag.MetaData{"info": {"stacktrace": stacktrace}}) } err = grpc.Errorf(codes.Internal, "Internal Server Error") } return resp, err },
今回エラーハンドリングには xerrors を使っているのですが、xerrors で wrap したエラーを投げると、bugsnag 上でのエラーが全部 *xerrers.Wrap
になるの困っています。どうにかしたいのですが、エラーの量もそこまで多くなくて困らないので放置しています。
また、スタックトレースの情報がわかりづらく、例えば以下のエラーは
以下の箇所で発生していますが、entry.go:118
がスタックトレースに表示されません。
この問題はメタデータとして、fmt.Sprintf("%+v\n", err)
を送信することで一時しのぎしています。これは以下の表示されます。
わかりやすい...。もう少しちゃんとしたやり方があると思うので直したいところです。
Node.js/AWS Lambdaでの利用
SSR している Lambda では express を利用しているので、ドキュメントに沿って導入してみたが動きませんでした。どうやら bugsnag へのエラー送信が終わる前に Lambda が終了してしまうことが原因みたいだったようです。Issueもありました。
https://github.com/bugsnag/bugsnag-js/issues/495
エラーを送信する処理(bugsnagClient.notify
)を自分で実行して、その終了を待てばよさそうなのですが、必要な情報を自分で詰めないといけないのでややめんどうでした。例えば express plugin を使えばこのあたりでやってくれます。
とりあえず最低限の情報だけ詰めて対応しました。
app.use((err, req, _, next) => { const opt = { request: { headers: req.headers, httpMethod: req.method, url: req.url } }; // このコールバックでエラー送信の終了を待つ bugsnagClient.notify(err, opt, () => { next(err); }); });
また、もう一つの問題は source maps です。
https://docs.bugsnag.com/platforms/javascript/source-maps/
ブラウザの JS であれば、sourceMappingURL
に書いてある URL に取りに行ってくれるので便利ですが、Node.js の場合は source maps のファイルを別途 Bugsnag にアップロードする必要があります。がんばればできそうですが、今回はめんどうなので諦めました。いつかやるかもしれません。
まとめ
今回は Bugsnag を導入するのにはまったことなどを書きました。実際 Bugsnag はかなり役に立っていて、Bugsnag のおかげでいくつかのエラーを特定して潰すことができました。いくつか中途半端になっているところがあるので今後改善していきたいと思います。
明日は UI のスマホ対応について書きます。
SSR の CDN によるキャッシュ戦略
Adventarを支える技術 Advent Calendar 2019 の19日目です。
今日は Server Side Redering (以下 SSR) した結果を CDN でキャッシュする戦略について書きます。SSR の概要については以下にも概要を書いたので一読しておくとよりわかりやすいと思います。
上記に書いてありますが、今回は SSR を Lambda で行っていて、Lambda の実行はそんなに速くないし、実行回数での課金になるので、前段の CDN でキャッシュすることで、パフォーマンスとコストを最適化することができます。
その設計について書こうと思いますが、、先に書いておくと今回は SSR の結果をキャッシュせずに毎回リクエストのたびに Lambda を実行しています。そのあたりの理由も含めて書いていきます。
キャッシュのパージ
キャッシュはやれば速くなるのは確実ですが、コンテンツが更新されたタイミングで適切にパージしないと、新しいコンテンツがユーザーに届きません。パージの戦略は提供するコンテンツやキャッシュする範囲によっても様々です。
例えばブログサービスのようなサイトでページキャッシュするケースを考えてみます。単純に考えると、ブログ記事が更新されたときに対象のブログページだけパージすればよさそうですが、キャッシュする範囲によってはその限りではありません。
例えばブログにコメントや Like のような機能があって、それらも含めてキャッシュしているならコメントや Like が更新されたタイミングでも必要かもしれません。また、トップページや最新記事一覧ページのようなところに記事のコンテンツが表示されていて、それらのページもキャッシュしているなら、すべての記事作成、更新でそれらのページのパージが必要です。
さらに、はてなブログのようなログイン機能があったらどうでしょう。ログインしている状態でキャッシュをつくると最初にキャッシュしたときにログインしているユーザー情報が全員に表示されてひどいことになります。そんなことないだろ、と思うかもしれませんが気をつけていてもミスによってそういった状態になってしまう例をいくつも見てきました。最近ではメルカリのニュースが記憶に新しいです。
なのでキャッシュは狭い範囲で必要最小限に留めるべきです。例えば上記のようなブログサービスの例であれば、コメントや Like、ユーザー情報の表示はクライアント側(JS)でレンダリングする、キャッシュするのは記事ページのみに留める、などです。
今回の Adventar でも、キャッシュはカレンダーページ( https://adventar.org/calendars/3860 のようなページ)だけに留め、ログイン情報も SSR では扱わず、クライアント側で認証情報を付け足す、という設計にしました。認証については以下に書きました。
これで、キャッシュする範囲は最低限ですが、それでもパージするタイミングは少なくありません。
- カレンダーのタイトル、概要の更新
- エントリの登録・編集・削除
- ユーザー情報(名前、アイコン)の変更
- デプロイ
などです。このような操作が行われたときに、どうやってパージするかを見ていきましょう。
Fastly によるパージ
まず最初にこの設計を考えたときに一番手にあがったのは Fastly です。Fastly には Instant Purge という機能があり、パージリクエストをして 150ms 以内にキャッシュをパージしてくれます。
https://www.fastly.com/products/web-and-mobile-performance/caching-and-purging
パージの速さはユーザー体験につながるので、ぜひこれを使いたいところです。Fastly で具体的にどのようにパージするかを説明します。
- カレンダーのタイトル、概要の更新
- エントリの登録・編集・削除
これらは単純で、該当カレンダーページのキャッシュだけをパージすればいいだけです。Fastly のパージは非常にシンプルで、該当の URL に PURGE
メソッドでリクエストするだけです。
https://docs.fastly.com/api/purge
$ curl -X PURGE https://adventar.org/calendars/1
だけで済みます。特別なクライアントもいらず、非常に簡単です。
- ユーザー情報(名前、アイコン)の変更
はもう少し複雑で、そのユーザーが登録しているカレンダーページをすべてパージする必要があります。一つ一つ丁寧に上記のPURGE
メソッドでパージすることもできますが、数が多くなると負荷も大きくなりますし、fastly の API Limit もあります。こういうときに使えるのが Surrogate-Key
という機能です。
https://docs.fastly.com/en/guides/getting-started-with-surrogate-keys
この機能を使うと、キャッシュをタグのようなものでグルーピングして一括パージすることができます。リクエスト時にこのヘッダに複数の値を指定します。例えばSurrogate-Key
の値にu1 u2 u3
のように、そのカレンダーに登録しているユーザー ID のをもたせておきます。そして user_id: 1 のユーザーがプロフィールを更新したら、u1
を指定してキャッシュをパージすることができます。
- デプロイ
デプロイ時にはすべてのキャッシュを消す必要がありますが、それは purge_all という API があるのでこれを使います。
https://docs.fastly.com/api/purge#purge_bee5ed1a0cfd541e8b9f970a44718546
このように、やりたいことは完璧に実現できそうだったので Fastly を使いところではあったのですが、Fastly の料金は最低料金が $50/month ということで、コスト面が折り合わず今回は断念しました。
一方 CloudFront は、最低利用料金がなく、転送量だけであれば先日の記事にも書いたように、最もアクセスが多い12月でも$30〜$40ぐらいで落ち着きそうで、1月〜10月はほぼアクセスがないのでこれの 1/10 ぐらいに収まると思っています。個人サービスの上に1円も稼いでいないサービスなので $600/year はけっこうきついので、今回は CloudFront を採用しました。
なお、Fastly にはオープンソース向けの無償アカウントがあるので、これを申請してみようかと思っています。
https://docs.fastly.com/en/guides/accounts-and-pricing-plans#free-open-source-developer-accounts
CloudFront によるパージ
まず、CloudFront のパージは Fastly ほど高機能ではありません。パージの速度も Fastly ほど速くありません(公式のドキュメントが見つけられませんでしたが、少なくとも ms のオーダーではない)し、Surrogate-Key
のような機能もありません。
それだけであれば許容できると思ったのですが、調べてみると思ったよりパージに料金がかかることがわかりました。
https://aws.amazon.com/cloudfront/pricing/
月間で無効をリクエストしたパスの最初の 1,000 パスまでは追加料金なし。それ以降は、無効をリクエストしたパスごとに 0.005 USD かかります。
どのぐらいパージ処理が走りそうかを概算してみます。
- カレンダーのタイトル、概要の更新
- ユーザー情報(名前、アイコン)の変更
- デプロイ
については、回数も多くないし一旦無視します。問題は
- エントリの登録・編集・削除
です。去年ベースで考えるとエントリの数は13767、そのうちコメントが更新されているもの12063、URL が更新されているものが11893でした。Adventar ではこれらの更新処理はタイミング的に別々に行わられるので、単純に足し算になります。さらに削除や、コメントやURLを複数回変更する場合もあります。ここではざっくり全体の10%ぐらいで計算してみます。
- 13767 + (12063 * 1.1) + (11893 * 1.1) = 40118.6
これらはほぼすべて11月、12月で呼ばれるので、その2ヶ月でどのぐらいかかるかを出してみます。また、無料枠が 1000req/month あるので、それも加味します。
- (40118.6 - 2000) * 0.005 = $190.593
だいたい $100/month くらいといったところです。ざっくり計算ですが、カレンダーや登録数は年々線形に伸びているし、もっと多くなる可能性もありそうです。
11月と12月以外は無料枠に収まると思うので、トータルで見ると Fastly よりは安く済みそうですが、ピーク時のコストを10,000円前後ぐらいで考えていたので少し厳しい金額です。
なので、今回は CDN によるキャッシュを諦め、毎回 Lambda を実行することにしました。ちなみにこれによって Lambda の実行回数は多くなりますが、昨日の記事にも書いたように Lambda のコストは $0.4 ぐらいになりそうです。爆安。
まとめ
SSR の結果を CDN でキャッシュする戦略と、コスト面でその戦略を諦めた話を書きました。キャッシュによるパフォーマンスの最適化はユーザー体験にかなり寄与すると思っているので、Fastly のオープンソース向けの無償アカウントなどでコスト面の折り合いがつけばまたチャレンジしたいと思っています。
明日は Bugsnag によるエラートラッキングについて書きます。
インフラコストの最適化
Adventarを支える技術 Advent Calendar 2019 の18日目です。
昨日の記事で実際にかかっているコストを紹介しましたが、今日はコストを最適化するためにやったことを書きます。
Lambda を活用とスペックの調整
昨日の記事を見ても分かる通り、ECS などと比べると Lambda のコストは爆安です。今回、Nuxt.js のサーバーサイドレンダリングを Lambda でやっていて、キャッシュはしていないので、カレンダーページへのリクエストごとに Lambda が実行されます。実行回数はこの function が支配的ですが、その他にも画像サーバーやバッチジョブなどにも Lambda を利用しています。
現時点で実行回数は15万回ぐらいで、おそらく30万回ぐらいで着地すると思います。無料利用枠が10万回分、その後は10万回ごとに $0.2 なので、$0.4 ぐらいで収まる計算です爆安ですね。
実行回数だけでなく、実行時間でも課金されますが、こちらは現在無料枠の半分以下なので無料枠で収まりそうです。なお、実行時間の課金は利用しているメモリの容量が関係します。
https://aws.amazon.com/lambda/pricing/
Serverless Framework ではデフォルトのメモリが 1024 MB になるので、大量に呼ばれる関数には適切に設定してあげないと、思いがけずコストがかかってしまうことがありそうです。今回は大量に Nuxt.js の SSR は 512 MB に設定しました。
これがデフォルトの 1024 MB だと実行時間の計算は倍になり、無料枠の倍ぐらいの実行時間になりそうなので、400,000 * 0.0000166667
で $6.6 ぐらいになりそうです。大した額ではないですが。
メモリの使用量は、Lambda のログに出力されます。
REPORT RequestId: ee702bc9-c647-4d45-95c9-db8a2e6ba8bc Duration: 55.51 ms Billed Duration: 100 ms Memory Size: 512 MB Max Memory Used: 224 MB
現状 200 MB 〜250MB くらいなのでもう少し下げてもメモリは足りそうですが、メモリ使用量に比例して CPU の性能も決まるので、あまり下げすぎると性能が劣化します。実際、以下のように影響が出ています。
カレンダーページのパフォーマンスに直結するので $6 ぐらいであれば 1024 MB に上げてもよさそうだなとこれを書きながら思いました。下げたときはどのぐらい呼び出しがあるか読めなかったのでビビってたのです。
Fargate か EC2 か
これはコストを削減したというよりは、コスト削減と天秤にかけてメンテナンスコストが安いほうを選んだという話です。
ECS はタスクを実行するのに EC2 インスタンスか Fargate を選べます。Fargate だと EC2 のインスタンスを管理しなくてよくなるし、スケールなども楽にできますが、EC2 より少し高くなります。また、EC2 の場合はデプロイ時に複数タスクを配置する必要があるので、余分にリソースを空けておきたいところです。
Fargate の一番小さいサイズでもさばけそうだったので、Fargate は 0.25 CPU, 0.5 GB Memory、EC2 の場合は t3.micro(2vCPU, 1GB Memory)で比較しました。
- Fargate 約 $11/month
- EC2(t3.micro) 約 $10/month
これだとほぼ変わらないので、管理の手間などを考えて Fargate を選ぶことにしました。EC2 はもう一つ下の t3.nano でも足りるかも知れませんが、リソースが足りなくなってスケールアップ/アウトが必要になったときの手間を考えると選びづらい選択でした。
また、スポットインスタンスなどを使えばもっと安く済みますが、さらに管理コストが増えるので採用は見送りました。ちなみにこのときはまだ Fargate Spot が発表されてなかったので試していませんが、時間があったら試してみたいと思っています。
ちなみに以下は一番小さいサイズで余裕で捌けている図です。
RDS のストレージサイズを小さくする
これはコスト最適化というよりは単純にミスったのですが、ストレージサイズを無駄に100GB確保しているのにリリースしたあとに気づいてしまって、途中で最小の20GBに落としました。
https://github.com/adventar/adventar/commit/47d336c02a1619591ac33062e34022ab1a6ec356
ストレージサイズを小さくするのは動かしながらはできないので、2台立ててアクセスが少ない時間帯に手動でデータをマイグレーションして切り替えました 😅
以下が切り替えたときのコストの変動です。
これで $11/month ぐらい節約?できました。
private subnet をおかない
普通、VPC を設計する際、public subnet と private subnet を作り、インターネットから見える必要があるのもは public へ、そうでないものは private に置くことでセキュリティを強固にします。
しかし、private subnet に置かれたサーバーからインターネットにアクセスしようとすると、Nat Gateway が必要になります。
https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html
この Nat Gateway が地味に高くて、起動しっぱなしだと、$40/month ぐらいかかります。必要なときだけ起動するみたいな方法もあるかもしれませんが、手間を考えて public subnet だけの運用にして Security Group でがんばる方針にしました。
基本的な方針として、インターネットからのアクセスは ALB だけに許可して、その他のリソースはすべて VPC 内部からしかアクセスできないようにます。
しかし、DB はデータや設定を見たりする用途で、手元からつなぎたいケースがけっこうあります。ただ、DB はインターネットからのアクセスを許可したくないリソース一番手でもあります。
つなげるときだけ必要なものを詰め込んだ Fargate のタスクを起動して、一時的な踏み台として使う、という手も考えましたが、そこまでしなくてもいいかと思い止まり、手元の IP を許可した Security Group を作って、必要なときだけ RDS に刺すようにしました。
この Security Group は terraform で管理せずに手積みで管理しています。
まとめ
Adventar のインフラコスト
Adventarを支える技術 Advent Calendar 2019 の17日目です。
Adventar の運用コストが、何にどのぐらいかかっているかを書きます。AWS 以外は無料のサービスしか利用していないので、かかっているのは 100% AWS のコストです。AWS の構成については先日書いたので、こちらも参照してください。
今年の11月に新システムに移行したので、11月からのコストを見てみます。12月16日の時点でコストはこのような感じです。
ピーク時(12月)で月10,000円以内ぐらいを目安に設計していて、この中に Adventar 以外のサービスのコストが $10 ぐらい含まれているので、概ね予定通りといったところです。内訳はこのような感じです。
(月半ばの金額なので月額ではこれの倍かかる予想)
今回 EC2 のインスタンスは使っていないので、EC2 となっているのは ALB です。その他については以下のようになっています。
Lightsail は Adventar と関係ない個人で使っているやつなので、その他では
- Fargate: $5
- Route53: $3
- データ転送量: $3
- CloudWatch: $1
といったところです。
RDS や ALB、Fargate については、このぐらいかかると見込んでいたので予想通りでした。思ったよりもかかっていたのが CloudFront と API Gateway です。これらはリクエスト数がもろに料金に反映されます。アクセス数と料金の変異を見るとばっちり一致します。
アクセス
料金
なので、ピーク時期がすぎればもう少し落ち着いて、コスト的には半分ぐらいで落ち着くのではと思っています。逆に、そうなってくると Fargate や RDS などの、常時起動しておく必要があるリソースのほうが支配的になってくるはずです。
また、S3 や Lambda はヘビーに使っているにも関わらず、ほぼ無料で使えているのもわかります。
まとめ
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 でも十分そうということですね...!)