デプロイ頻度はどれくらい改善したか?

正徳巧氏(以下、正徳):この2つの改善、Dockerファイルを移したものと、自動プルリクを作るもので、どれくらい改善したかを計測してみました。メドピアのワークフローでいうとmasterへのマージはほぼデプロイの回数になるので、masterブランチのマージコミットの数を月単位で数えたコマンドを作りました。

同じように、developも数えてグラフを作りました。上のグラフがマージされた機能の数だと思ってください。デプロイ回数はそんなに増えていませんが、2020年7月が25回ぐらい。少しわかりづらいですが、2月までは少なかったものの、3月ぐらいからガッツリ増えている。これが定期デプロイを入れたあたりです。

最初の頃はGitHub Actionsではなく手で作っていました。運用がいい感じに回ったので自動化しようという話になり、GitHub Actionsになったという感じで改善されました。

この結果、Dockerのビルドは入れていませんが、単純なデプロイの時間だけでいうと7分ぐらいで終わるようになりました。回数は1日2回なので、45〜50回のほぼ倍ぐらいになりました。デプロイの回数がある程度できたので、今後はカナリアデプロイをしていきたいなと思っているので、カナリアデプロイの話をしていこうと思います。

先ほどはありませんでしたが、死活監視の話もあって。デプロイ回数が増えるということは、障害が起きた時にすぐに気付いて戻さないといけない。死活監視の強化も今期やっていこうかと思っています。

カナリアデプロイについて

正徳:カナリアデプロイの話です。Railsアプリの構成としてはこんな感じになっていて、ユーザからのアクセスをCloudFrontで受けます。CloudFrontから次にALBに行って、その後ろにECSサービスが2個あって。片方がWebで、タスクがいっぱい動いているのと、だいたいworkerでSidekiqを使うことが多いのでSidekiq用のworkerを用意しておきます。

後ろにRDSのAuroraとElasticacheを用意して、弊社ではMySQLとRedisが使われることが多いです。assetsファイルをS3から配信、CloudFrontをS3で配信するケースもありますが、弊社ではあまりやっておらず、現状だとECSから配信をして、assetsファイルもCloudFrontで/assets/と/packs/の下をキャッシュする構成になっています。

ECSの場合のカナリアデプロイについて調べたところ、AWSのCodeDeployでカナリアデプロイとECSの線形デプロイがあるので紹介します。まずカナリアデプロイは、最初に10パーセントのユーザだけを新しいバージョンに流し、残りの90パーセントを一定時間後に流す。デフォルトでは5分後か15分後の設定があるので、どちらかでシフトする設定があります。

線形デプロイは10パーセントずつユーザを増やしていく設定です。新バージョンを最初に10パーセント、次に20パーセント、次に30パーセントのユーザと、使っていく人が増えていくかたちになっています。

図で説明すると、ALBの中にListenerという設定が作れるので、Listenerのmainとtestを作っておきます。mainのほうは普通にドメインでアクセスできた時、example.comできた時はmainを使って、ポート番号8080を指定したら必ず新バージョンに行く設定にできます。

今後、この90パーセントと10パーセントの振り分けなどもALBの機能のスティッキーセッションを使って自動的にやってくれるので、まずは旧バージョンに90パーセントのユーザ、10パーセントのユーザが新バージョンにアクセスするようになり、問題が起きたらすぐに気付けるような、影響範囲を狭くするような方法がカナリアデプロイになります。

このListenerの切り替えや、サービスのデプロイの設定をいい感じにやってくれるのがCodeDeployです。開発者はこのポート番号を指定することにより、新バージョンで動作確認できるような構成になります。もう一つ説明をすると、CodeDeployができるカナリアデプロイはWeb側のデプロイだけになります。db:migrateというRailsの機能に関しては、CodeDeployは面倒を見てくれないので、自分たちでやる必要があります。

最近、私が検証でやっていたのは、RunTask APIというECSのAPIを使って一時的にコンテナを立ち上げ、マイグレーションをして終わったら終了する構成です。

CodePipelineとGitHub Actionsのどちらを使うべきか?

デプロイで、CodePipelineとGitHub Actionsのどちらを使ったほうがいいのかを検討してみました。比較したところ、AWSが提供しているAWS for GitHub Actionsはけっこう機能がよく。

なぜかCodePipelineよりもGitHub Actionsのほうが便利だったので、こちらを使おうと考えました。あとはTerraformにするとRailsエンジニアがつらいというか、あまり触りたがらない。同じリポジトリのYAMLであれば触りやすいので、YAMLのほうがいいだろうと考えました。それとAWSコンソールに移動するよりもGitHub 上で全部完結するほうが楽なので、GitHub Actionsを採用する方向で今は検証しています。

新しい開発フローと動作検証

新しい開発フローもメドピアの開発に合わせてやらないといけないので、考えました。まずはリリースプルリクを作ったあとにデプロイ承認して、OKであればカナリア環境にデプロイして動作確認する。これは検証用なので5分にしていますが、実際はたぶん15分とか、もう少し長くするかもしれません。

カナリア環境で問題がなかったらプロダクション環境にも自動的にデプロイして、問題がなかったらmainブランチにマージする。つまり、mainブランチは必ず安定板になるような構成になっています。もし問題が起きたらGitHub Actionsのワークフローをキャンセルすることで、CodeDeployのロールバックをAPI越しに叩くようにして、AWSコンソールを開かなくても済むような構成を考えました。

これを実際に社内サービスで動作検証をしました。作ったワークフローがこんな感じです。まずはカナリアデプロイをやり、URLが出るので叩くと、新しいバージョンにアクセスできます。動作確認をして問題がなかったら、プロダクションデプロイが終わり、リリースプルリクを自動的にマージしてくれるようになります。

右上にワークフローのキャンセルボタンがあり、何か問題があったら押すとマージを行われず、ロールバックの処理が動く感じになります。実際に社内で動かして、うまく動きそうなのがわかりました。

ここでGitHub ActionsのYAMLでいくつか苦労した点があるので、共有していこうと思います。まずはdb:migrateの実行で、db:migrateはdb/migrate/の下のdiffを判別してファイル数をチェックしてマイグレーションのステップ数を調べました。ecs:RunTask APIを使いますが、簡単にGitHub Actionsでやる方法がなかったのでactionを作りました。自作して公開しているので、もし興味があれば使ってみてください。

チェックアウトしてgit diffして、マイグレーションのサイズを拾ってきて、ゼロでなければその回数分だけマイグレーションするようなコードになります。

次はカナリアデプロイです。カナリアデプロイに関してはECSの公式のドキュメントを見るとそんなに難しくはありませんが、1点だけ注意があって。wait-for-service-stabilityという、CodeDeployのデプロイが終わるのを待つかどうかというbool値がありますが、これを待たないようにします。

なぜかというと、CodeDeployはカナリアデプロイが終わってプロダクションのデプロイが終わるまで完了扱いにならないので、さっさと終わらせてしまったあと、自前でAWSのCLIを使ってカナリア環境のデプロイ待ちを書いています。

プロダクション環境はあまり書くことがありませんでしたが、デプロイメントのサクセスを待っている状態です。CodeDeployでカナリアデプロイが終わるところをデプロイ(canary)スライドの方でやっているので、デプロイ(production)スライドのではCodeDeployの全体が終わるのを待っています。

何か問題があった場合はロールバック。ワークフローがキャンセルされた時、もしくはdeployment-id、CodeDeployの処理がある時はキャンセルしつつ、deployment-idがある場合、CodeDeployがすでに動いていた場合は、CodeDeployが動く前にキャンセルしているケースもあるので、これも考慮した条件分岐になっています。

その場合はAWSのコマンドを叩いてロールバックを指定しています。ロールバックの指定もDBのロールバックも必要なので、db:rollbackでステップ数を指定して、戻すようにしています。

最後に、問題が全部なかったらGitHubのCLI、ghコマンドを使ってプルリクをマージするような処理を書いています。

デプロイの改善ができて、カナリアデプロイも一通りの検証は終わりました。死活監視はあまりできていなくて、今後カナリアデプロイとの連携なども、もう少し調査していこうと思っています。

この検証結果を踏まえ、次はMedPeerに導入して、もっとデプロイ頻度を増やそうと考えています。カナリアデプロイなどを体験したい人や一緒に作ってみたい人がいたら、ぜひリクルートのページでカジュアル面談などで応募してください。以上、ご清聴ありがとうございました。

質疑応答

司会者:質問がきています。「先にビルドしていると使わないイメージがたくさんできると思いますが、このあたりはイメージが溜まっていかないような工夫とかあるんですか?」と。

正徳:ECRのほうのライフサイクルポリシーで、たぶん全社的に3ヶ月ぐらいで消すようにしています。溜まり過ぎることはないかなと思います。あとはプルリクを作るタイミングで、GitHub Actionsはmasterブランチへのマージのプルリクだけ作るようにしているので、基本的にはリリースプルリクを作る時だけDockerイメージでビルドしています。

司会者:基本的には時限的に処理されていく感じですね。

正徳:基本的には「これからデプロイするぞ!」という直前にビルドするので、あまり無駄になることは......。「リリースするぞ」となって、何か問題が起きてプルリクをクローズする時ぐらいで、社内的にはそんなことはあまりないので。あまり無駄にはならない感じです。

司会者:デプロイまわりは改善が進んでいるかなと、僕も実感があります。

正徳:でもまだ300回いっていないので。

司会者:ありがとうございます。あとは「デプロイの回数だけは回数を追うとか、質にもよると思うので、そのあたりの方針や指針はあるんですか?」みたいな質問がきているんですが。

正徳:質というのはどういうものなんだろう。

司会者:たぶんリリースする内容や、そういうものによって変わってくるんじゃないか、というところですね。

正徳:なるほど。その話が『LeanとDevOpsの科学』にあった気もするので、質問した方はぜひ読んでみてください。あとはもし意見があれば。回答というか。

司会者:僕の考えとしては数は多いほうがいいはいいんですが、数に囚われずに質はやはり重視していきたいところです。現状のメドピアは、だいたい1日に2回ぐらいのリリースです。これを20回にしたいかというと、質がついてこなければ20回にならなくていいかなと思っていますね。そういうバランスは取っていきたいんですが、速いに越したことはないので、速さについては追及していきたい。

正徳:そうですね。まだ7分なので3分を目指したいですね。

司会者:僕らの中で3分は意識があるかなという感じです。ということで正徳さんの発表、ありがとうございました。