ENECHANGE Developer Blog

ENECHANGE開発者ブログ

ElasticBeanstalkでのデプロイ高速化!〜assets:precompileをCircleCI上で〜

プラットフォーム事業部の@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_COMPILATIONtrueを設定すると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
リスト1. deployコマンドで--labelオプションを付けるシェルスクリプト

リスト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
リスト2. .circleci/config.ymlにおける設定

デプロイ時に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
リスト3. デプロイ時にs3からassetsをダウンロードする処理

これで、EC2上でassets:precompileをせずにデプロイすることができました。デプロイ時間も半分以下に減りました。