みなさんさようなら,最近デプロイが趣味の@h3_potetoです.
この記事は,僕が趣味で改善していたCrowdWorksのデプロイ周りの大改造の歴史を振り返ります.基本的には,デプロイが趣味の方向けの記事です.とても長いです.
手動構築の時代
その昔,CrowdWorksのサーバは手動で構築されていました. 手動でRuby等の設定をいれたサーバを複数台用意し,そこにcapistranoデプロイをしていました.
インフラは手動構築だった割に,なぜかBlue/Greenデプロイを実現していました.
Blue/Greenの実現方法
まずはじめに,
- Blueが稼働系
- Greenが待機系
です. Blue/Greenデプロイをする際には,Green系にデプロイをして,Blue系と挿げ替え,旧Blue系をそのまま廃棄します.
この動作をどこで実現しているかというと,なんとCrowdWorksのアプリケーションが入っているリポジトリに含まれていました.
通常,capistranoのデプロイというのは, config/deploy.rb
にタスクを書いたりして cap
コマンドを使ってデプロイしたりするはずです.
ところがCrowdWorksのデプロイは,rake cap
という謎コマンドが実装されていました.
こいつが何をするかというと,
- ELBに紐付いていないasgを特定する(これがGreen系)
- Green系のasgでstopになっているインスタンスを起動する
- 起動したインスタンス群にcapコマンドを流す
- capが終了したらcurlが通ることを確認する
- Green系のインスタンス群をELBに紐付ける(これでGreen系が新Blue系になる)
- 旧Blue系インスタンスをELBから外す
- 旧Blue系のインスタンスをstopする
- 終了!
という,過度にインフラレイヤーを触りまくるrakeタスクが眠っていました.
Chefの時代
やがて,CrowdWorksのインフラも手動構築から脱却します.脱却先はChefでした. cookbookを作る部分は今回は触れません.
問題は出来上がったcookbookをどうやって適用するか,いわゆるインフラデプロイの問題が残ります.
インフラデプロイの方法
ホストにcookbookを適用することだけなら,chef-clientを実行すれば良いだけです.至って簡単なことです. しかし,さすがにBlue系のサーバ群が,サービスを提供している最中にchef-clientを流すのはどうかと思います.
さすがにインフラの変更なので,失敗するリスクもあるわけですから.それをいきなり動いているサーバに入れて,nginxを再起動したら全滅,なんてことはやっちゃいかんわけです.
インフラデプロイも方系ずつ
そこで,ここでもBlue/Greenの仕組みに大きく乗ることになりました.
- Green系インスタンスを起動
- Green系インスタンスにchef-clientを実行
- Green系インスタンスにserverspecを実行
- Green系インスタンスを一時的にELBに接続
- Blue系インスタンスを一時的にELBから外す
- ELBからはずれたBlue系インスタンスにchef-clientを実行
- Blue系インスタンスにserverspecを実行
- Blue系インスタンスをELBに接続
- Green系インスタンスをELBから外す
- Green系インスタンスをstop
というような流れで,一時的にGreen系インスタンスにリクエストを裁かせている間にchef-clientを実行します. これにより,chef-client後のserverspecでテストが失敗しても,その時点でデプロイが止まるため,ユーザ影響なしにデプロイが可能となります.
一つ問題があります. 通常のアプリケーションデプロイは,先に述べた通りのBlue/Greenデプロイでした. そのため,Green系インスタンスを起動した段階では,中に入っているアプリケーションのリビジョンは古いままです. これを,Blue系で動いているアプリケーションのリビジョンと合わせるために,一度capistranoデプロイをする必要があります.
capistranoデプロイを誰がやるのか?
アプリケーションデプロイで用いるcapistranoは,元々 rake cap
というタスクから呼ばれることを前提に設計していました.
そのため,インフラデプロイ直前に実行するcapとしては,そのまま使えない形をしていました.
さらに,これをデプロイ用サーバからやるのか?という問題も残ります.
そこで採用されたのが,CodeDeployです.
CodeDeploy用のcapタスクを,アプリケーションデプロイとは別々に作っておきます. そして,Green系インスタンスが起動したところで,CodeDeployのAPIを叩いてcapistranoを実行させます.
このようにして,CrowdWorksのデプロイスクリプトはなぜか同じ環境に対する2種類のCapタスクが出来上がりました.
インフラのBlue/Greenを誰が実現するのか?
アプリケーションのBlue/Greenデプロイは,なぜかアプリケーションコードと同じリポジトリに眠っていました. これをそのまま使うことはできません.
というわけで,インフラのBlue/Greenを実現するための,シェルスクリプトが新たに生まれました.
なんとBlue/Green実現方法も2種類誕生しました.それもシェルで.
まとめるとこんな感じでデプロイすることになります.
苦悩の時代,そして無人化
いよいよインフラがchef化され,手動構築だったサーバは駆逐されました. しかし,この頃のCrowdWorksはデプロイは非常にリスキーでした.
インフラデプロイにやたら時間がかかる
インフラデプロイは前述のような手順をたどるため,非常に時間がかかりました. そもそも,この頃はアプリケーションデプロイ自体も30分近くかかっており(異常である),これに加えでインフラ側の処理が入ってくるため,1時間弱かかっていたかと思います.
また,この構成からも分かる通り,インフラデプロイをしている間は当然アプリケーションデプロイはできません.
そのため,一度インフラデプロイを始めてしまうと,1時間はアプリケーションデプロイができない状況でした.
インフラデプロイが失敗しやすい
加えてこのインフラデプロイ,高頻度で失敗しました. やることが多いため,問題が発生するのは理解できます. が,リカバリがあまりにも大変です.
どこの段階で失敗しているかにもよりますが,ログを読んで,失敗箇所を特定し修正できるのは,そもそもこの仕組と中のシェルスクリプトを理解している人にしかできません.
また,本質的ではない,たとえばELBにインスタンスを追加する際にawsコマンドがタイムアウトした,とか……そういった失敗もありました.
この場合,インフラの状態が異常になっているため,正常にインフラデプロイが完了するまで,当分の間はアプリケーションデプロイができなくなります.
幸いなことはどこで失敗しても,一応ユーザ影響がなかったくらいでしょうか.
デプロイが成功したように見えて障害に突入
こちらは,インフラデプロイの話ではないですが,アプリケーションのBlue/Greenデプロイも非常に難解なコードで書かれていました. ここをデバッグできる人も限られており,なおかつデバッグ環境がそもそも存在しないという…….
あるとき,普通にアプリケーションデプロイを実行しました. そして,プログラムはデプロイが成功したと言ってきました.安心していると,なんとCrowdWorksが障害判定されています.
どうやらBlue/Greenの難解なスクリプトの奥深くで,Blue系とGreen系の判定が間違っていたらしく,全てのサーバをBlue系と判定したようでした. そのため0台のGreen系インスタンスにデプロイし,ELBにつながっていた全てのBlue系インスタンスをELBから外してstopしてくれました.
一瞬にして障害に突入です.
この修正のために,Blue系とGreen系の判定が大きく見直されましたが,対処療法的に直すしか無く,その結果デプロイ時にBlue系とGreen系のインスタンス数が合っていなければデプロイを中止するようになりました.
平時には困りませんが,これはトラブル時に非常に困ります. 何らかの原因によりBlue系のインスタンスが死んだ時等,とりあえず最新をデプロイすれば治る!とわかっていても,デプロイができません.
正常な状態まで手動でもっていかないと,そもそもデプロイができないという構造になってしまいました.
属人化の次に来るもの,無人化
このようなBlue/Greenの仕組みを作っていた人がクラウドワークスを去りました. その結果,ここまでの複雑な仕組みを理解し,直し続ける人がいなくなります.
よく,「属人化は良くない!」と言われます.確かに良くないです. しかし属人化を放置しておくとどうなるんでしょう.
僕は,「属人化を放置した結果」として「無人化」した例が,これだと思っています.
まだ,属人化してる方がマシだわ.
デプロイに必要なもの,それは祈り
このような状況にあり,CrowdWorksのデプロイに必要なもの,それは祈りでした.
どうか無事にデプロイが完了してくれますように. 毎日が祈りデプロイ.
デプロイ大改造作戦
祈ることは否定しませんが,僕はデプロイのたびに祈るなんて嫌です. どうせ祈るならもっと建設的なことを祈りたい.
そして,どうせ無人化しているなら,誰かに属人化している方がまだマシなので,とりあえず全て僕に属人化させようと思いました.
Blue/Greenの廃止
本来,Blue/Greenとは何のために必要だったんだろう?ここまでデプロイがリスクになった状況で,果たしてBlue/Greenが必要なのでしょうか?
Blue/Greenの理由を聞き出す
無人化していても,ここの理由を知っている人はいました.
「かつて,デプロイ後のpassengerの再起動が,普通にタイムアウトするレベルで遅くて,それを避けるためにBlue/Greenにした」らしい. 全然Blue/Greenである必要を感じないぞ!!
今やそこまで遅くないし,普通に再起動も成功しているでしょう.っていうか,そもそもApache + passengerやめてnginx + unicornにしない?
Blue/Greenやめよう
ここまで聞いて決めたこと,アプリケーションデプロイをBlue/Greenにするのやめよう.
Blue/Greenでインフラ構成した場合,デプロイ時の切替方法は複数考えられます.
- Green系新しいリビジョンをデプロイし一斉に切り替える(CrowdWorksはこれでした)
- 1台ずつ新しいリビジョンをデプロイする
- 数台(例えば半数)ずつ新しいリビジョンをデプロイする
CrowdWorksで行っていた,この一斉に切り替えるというのは,Red/Blackデプロイと呼ばれています.
ただ,これを日常的なアプリケーションデプロイで多用するのはリスキーすぎて,とてもやっていられないです.
それに,まるっと切り替えるだけなのにBlue/Greenにしている……これならcapistranoのシンボリックリンク張替えと大差ないのではないでしょうか?
ならいっそのこと,やめよう.複雑なものは,本当にそれが必要になるまで,無理して作るべきではない,という判断をしました.
インフラデプロイの作り直し
Blue/Greenを廃止するにしても,ではインフラデプロイはどうしよう? やはり稼働中のインスタンスに直接chef-clientを流すのは避けたいですよね.
そこで,ここだけはBlue/Greenをやることにしました. しかし,今までのRed/Blackではなく,Blue/Greenである利点を活かしたデプロイ切り替えを実現します. 即ち,
- chef-clientを流した新しいインスタンスを生成
- それを全体のx%に混ぜてリリースして様子を見る
- 良さそうなら順次切り替え
ということを可能にします.
ということで,今までのインフラデプロイは全て捨てて,新しく作り直すことになりました.
capistranoの分離
Blue/Greenをやめるのであれば,謎の rake cap
も消える!
であるなら,そもそもCapのタスクをアプリケーションに書きたくなくなります.Capのタスクがアプリケーションと同居している限り,Capを実行するにはCrowdWorksのアプリケーションの実行環境が必要になります.
デプロイサーバにいちいちCrowdWorksが動くだけの環境を用意するのは手間でしかないわけです. ましてや,デプロイサーバはchefにすらなっていないわけで…….
というわけで,デプロイだけをやってくれるCapタスクを,別のリポジトリに切り出します.
ついでに,このときにcapistrano2系からcapistrano3系に変更します(ここは僕の趣味).
計画実行と障害
このようにして計画はだいたい経ちました. あとは実行に移すだけです.
しかし,いざ実行しようとすると,幾つかの障害が見えてきました.
unicorn化
アプリケーションデプロイはBlue/Greenを廃止します.ということは,稼働中のサーバにアプリケーションデプロイをかけることになります.
このとき,Railsのプロセスはgraceful restartしてくれないと,リクエストが途切れてしまいます.
これを実現するために,nginx + unicornでRailsを動かさなければなりません.
そもそも,CrowdWorksのサーバは,前段にnginxがいるにもかかわらず,そこからapacheにリクエストを投げ直します. そしてapache + passengerでRailsを動かしていました. なぜ,無駄にひとつリバースプロキシする必要があるのか…….
理由は誰も知らなかったんですが,特に必要なさそうなので,中間層のapacheを殺して,nginxからunicornにリクエストを処理させます.
この作業自体はそこまでトラブルもなく,無事切り替えに成功しました.
assetsのCDN化
次に障害となったのは,assetsでした.
稼働中のサーバでアプリケーションデプロイをするということは,assets:precompile
も稼働中のサーバで行われることになります.
これは流石に負荷がかかるし,あまりやりたくないという話になりました.
そこで出てくるのが,assetsをCDN経由で配信する方法です. これを前提にすれば,そもそもprecompileはデプロイ時ではなく,たとえばCIの最後にやらせておけば,CIが通ったときにはassetsの準備ができていることになります.
これを実現するために,asset_sync
を入れて,assetsをCDNから配信しました.
仕組みの導入ができたので,一度切り替えてみたのですが,盛大に失敗しました.
public配下に残るassets
Railsは app/assets/
配下に配置されているassetsをprecompileしてくれて, asset_sync
を使っている場合,precompileが完了したものをS3にアップロードしてくれます.
ところが,public/assets
ディレクトリを作り,そこにassetsを放り込んでいた場合,これらはそもそもprecompile対象ではありません.
おまけにprecompile対象ではないために,view側に仕込まれているリンクがhelperを使ったものではなかったのです.
大多数が,
<img src="/assets/hoge.png">
であり,一部もっとひどいものは
<%= image_tag "/assets/hoge.png" %>
というような形式で書かれているのです.
この頃の口癖は 「頼むからお前らRailsを書いてくれ」.
仕方ないのでこれを僕らで駆逐しました.
この書き換え作業に,約1ヶ月かかり,その1ヶ月で15万行以上のコードを削除し,5万行くらい追加したようです(cssの移動等がほとんどなので自分で5万行も書いているわけではないです).
こうして,壮大な駆逐作業の後,無事assetsはCDN化されました.
アプリケーションデプロイの切り替え
さて,いよいよ下準備は整いました.あとは,いい感じにデプロイできるように作り直すだけです.
アプリケーションデプロイで必要になる手順はどんなものでしょうか.
- 稼働しているインスタンスを見つけ出す
- そのインスタンス群に対してcapistranoを走らせる
- 終わり
とても単純になりました.しかしシェルスクリプトは書きたくなかったので,これをやってくれるgoのプログラムを作りました(ここも僕の趣味です).
goはとても良い.
awsの操作が入る部分もすべてsdkで実装し,1つのバイナリファイルでデプロイができるように閉じ込めます. さすがにcapistranoの実行は,OSのネイティブコマンドを叩くしかなかったですが…….
また,これにより嬉しい副産物が現れました. 今まで,Blue/Greenデプロイでデプロイしたものは,Rollbackが鬼門でした. Rollbackはコマンドとしては一切実装されておらず,手動でGreen系をBlue系を切り替えることで行われていました.
これが,capistranoのRollbackをそのまま使うことができるため,一切インフラのことを考慮することなくRollbackできるようになりました(オマケに早い!).
そしてインフラデプロイの再構築
最後の鬼門がインフラデプロイです.
さて,真のBlue/Greenはどのようにして実現しましょうか.
- Green系のasgのDesiredを増やして新たにインスタンスを生成する
- cloud-initでchef-clientを実行させる
- chef-clientの実行が完了したらcapistranoを流す
- serverspecを流す
- 準備ができたので,ELBへの追加等,この先の判断は人間がやる
という形を目指しました.
以前は,Green系のasgにstopしたインスタンスを眠らせておいて,デプロイ後もインスタンスは単にstopするだけでした. しかし,今回はデプロイのたびに毎回新しいインスタンスを生成させています.
これでイミュータブルインフラっぽくなる!
最後を手動にしているのは,「x%だけ混ぜてみてどうか」という判定をするのは,やはり人間にやらせるべきだと思ったからです. ここだけ手動なので,少しリスクは残りますが,適切に判定できるようなプログラムを組める気がしなかったので,手動のまま残しました.
cloud-initの終了判定どうする?
一つ問題があります.cloud-initはインスタンス起動後,インスタンス内で勝手に実行されます. 実行が終了しても,外側のawsコマンド等では知る術がありません.
しかし,chef-client実行が終わる前に,capistranoを流すことはできません.
どうやったかというと,cloud-initのラストに,awsコマンドを叩いて自身のインスタンスにタグを付けさせます.
cloud-init: success
みたいなタグを新たに付けておきます.
これを外側からウォッチすることで,cloud-initの完了を知ります.
インフラデプロイもgoで書く
これらの処理もアプリケーションデプロイと合わせてgoでプログラムを書きました. 同じくdockerで全てを実行できる形でまとめています.
capistranoコマンドについては,アプリケーションデプロイと同様のcapタスクを叩いています.
CrowdWorksデプロイの今
平時.
アプリケーションデプロイ.
インフラデプロイ. Green系を起動.
同時にchef-clientの実行.
Green系にcapistrano.
Green系にserverspec.
手動でELBの付け替え等.
こうしてCrowdWorksのデプロイは,だいたい1年前に妄想したとおりの安全な状態になりました.
- デプロイ,インフラデプロイの遅さ
- デプロイ時の障害リスク
- ロールバックの気軽さ
- 障害時の緊急デプロイ
- インフラデプロイの祈り
あたりの問題は軒並み解決されました.
さらに嬉しいことに,アプリケーションとの依存度が下がったので,デプロイを発火させるhubotやgoのプログラムも全てdockerで動かすことができます.
そして最近はめっきり祈ることもなくなりました.
趣味のデプロイ,満足した.