プラットフォーム事業部の@yuyasatです。
背景
ENECHANGE社では、ElasticBeanstalkのRubyプラットフォームを利用してインフラを構成することが多くあります。ElasticBeanstalkを利用していて困るのがデプロイに時間がかかること。デプロイするRailsアプリケーションやデプロイ時のバッチ数(複数のEC2インスタンスに対し、何回に分けてデプロイを反映するか)にも依存しますが、長い時では20分近くかかってしまうこともあります。
デプロイに時間がかかると速やかに修正を反映できません。不具合時の対応に時間がかかり、デプロイすることへの心理的ハードルも上がってしまいます。デプロイ時間を速くすることは安定的なリリースには欠かせません。
デプロイのボトルネック調査
まず、どこに時間がかかっているかを調べるため、ログを見ながらデプロイを行いました。デプロイ時のログはEC2上の/var/log/eb-activity.log
に出力されます。このログをtail
しながらデプロイを行いました。
一番のボトルネックとなっているのがRailsのassets:precompile
であることが分かりました。この処理でEC2のCPUの負荷が高まり、時間がかかっていました。CPUはデプロイ時以外は十分にリクエストをさばくことができる処理能力であり、デプロイのためだけにEC2のインスタンスタイプをあげるのはナンセンスでした。
解決策
ElasticBeanstalkでは、環境変数でassets:precompile
をするかしないかをハンドルすることができ、RAILS_SKIP_ASSET_COMPILATION
にtrue
を設定するとassets:precompile
をスキップできます。EC2上でassets:precompile
を行わなければどこで行うのでしょうか?ENECHANGE社では創業当時からのサービスである電気とガスの簡単比較 エネチェンジに関しては、今のところElasticBeanstalkを利用しておらず、デプロイにはfabric
を使っています。また、assets:precompile
をCircleCI上で行なっています。エネチェンジを参考にしつつ、今回構築したい ElasticBeanstalk においても、CircleCI上でassets:precompile
を利用するように修正しました。
大まかな流れとして、CircleCI上でassets:precompile
を行いS3上に置きます。このとき、ブランチごとに配置するディレクトリを分けます。ElasticBeanstalkでは、デプロイ時にassetsファイルをS3からダウンロードします。どのassetsファイルをダウンロードすべきかを判断するため、ElasticBeanstalkではブランチ名を把握する必要があります。通常、ElasticBeanstalkではデプロイ時のgitのブランチ名を把握することができないため、バージョンラベルにブランチ名を含めることで対応しました。
eb deploy
コマンドでは--label
オプションを指定することでラベル名を任意に指定することができます。デプロイ時に--label
オプションを付け忘れるのを防ぐため、リスト1のようなシェルスクリプトを用意しました。バージョンラベルにはスラッシュ(/
)を用いることができないため、^
に置換しています。(^
はgitのブランチ名に用いることができないため。)
#!/bin/sh if [[ $1 = deploy ]]; then BRANCH=`git symbolic-ref --short HEAD` TIME=`date '+%Y%m%d_%H%M%S'` # labelは/を^に変換する echo "eb deploy $2 --label ${BRANCH//\//^}~${TIME} ${@:3}" eb deploy $2 --label ${BRANCH//\//^}~${TIME} ${@:3} else eb $@ fi
リスト2に.circleci/config.yml
での処理内容を示します。ここでは、assets:precompile
と、s3 sync
を行います。この時もブランチ名のスラッシュ(/
)を^
に変換します。
- run: name: Execute assets:precompile and s3 sync command: | if [[ $CIRCLE_NODE_INDEX -eq 0 ]]; then WEBPACKER_PRECOMPILE=false bundle exec rake assets:precompile && aws s3 sync public/assets/ s3://example/common/assets/${CIRCLE_BRANCH//\//^}/ fi
デプロイ時にassetsををS3からダウンロードするための処理を.ebextensions
に書きます(リスト3)。ElasticBeanstalkでは.ebextensions
に記述することでデプロイ時に任意の処理を実行することができます。
このとき、どのブランチでデプロイしているかはバージョンラベルで知ることができます。このバージョンラベルは、/opt/elasticbeanstalk/deploy/manifest
にJSON形式で格納されており、ここの値を取得します。ブランチ名の取得は書き慣れているrubyで取得するようにしました。
container_commands: 10-download_assets: command: | BRANCH=`ruby -rjson -e ' manifest = JSON.load(open("/opt/elasticbeanstalk/deploy/manifest")); label = manifest.dig("RuntimeSources", "example").keys.first; print label.split("~").first; '` if sudo aws s3 ls s3://example/base/common/assets/$BRANCH/; then sudo aws s3 cp s3://example/base/common/assets/$BRANCH/ public/assets/ --recursive else echo "[DEPLOY ERROR] No assets files in s3. Check whether CircleCI finished." # assetsが存在しない場合は終了コードを1とし、deployを失敗させる exit 1 fi 11-remove_old_manifest: command: ls -1t public/assets/.sprockets-manifest-*.json | awk 'NR > 1 {print}' | xargs -r sudo rm
これで、EC2上でassets:precompile
をせずにデプロイすることができました。デプロイ時間も半分以下に減りました。