こんにちは! SmartHRエンジニアの @tei-k です。
SmartHR ではインフラに AWS の Elastic Beanstalk (以降 EB ) を使っています。
Rails アプリですので、去年までは Ruby Platform 上で動いたのですが、今年から Docker Platform へ切り替えました。 ここでは移行するまでの工夫をご紹介できればと思います。
EB の概要
EB は、インフラストラクチャーを意識せず簡単にアプリケーションの構築、デプロイ、Auto Scaling などをマネジードしてくれる PaaS となります。 Ruby、Go など様々な言語や Docker にも対応しているため、すべての言語のアプリを EB 上で動かせるでしょう!
詳細は公式ドキュメントご参考下さい。
移行の背景
Ruby Platform には以下の制限や要望がありました。
- 最新の Ruby を利用したいが、EB の Ruby Platform が対応してくれない
- 最近 Ruby2.5 がサポートされました (Ruby 2.5 with Puma version 2.7.1)
- Pull Request の確認などで ECS を使ってるので、本番や開発環境も Docker を使用したい
- 環境ごと (production/staging/sandbox など) にアプリケーションバージョンが作られ、デプロイするバージョンもバラバラ
- CircleCI 上で
eb deploy
を行っていたので、リソースの転送に時間がかかっていた
Docker 化のポイント
Single Container VS Multi Container
App サーバと Worker サーバを EB の env で分けており特に複数コンテナを使うメリットもなかったため、シングルコンテナを使うことにしました。
ベースイメージ
当初はイメージサイズの小さい Alpine を検討しましたが、検証時に Gem のインストールで失敗したりしていました。
パッケージをいろいろカスタマイズする案もありましたが、メンテナンスの手間を考慮し Debian (ruby:2.4.3-stretch
) を使うことにしました。
Ruby Official Repository: https://hub.docker.com/_/ruby/
各種イメージ用の Dockerfile
イメージのリポジトリは ECR を使用しています。
- ベースイメージ (Ruby + node)
例) image tag: ruby-2.4.3-node-v8.9.4
FROM ruby:2.4.3-stretch ENV SHIPPABLE_NODE_VERSION 8.9.4 ENV NODENV_PATH /root/.nodenv WORKDIR /app RUN gem install bundler --no-document RUN git clone https://github.com/nodenv/nodenv.git ${NODENV_PATH} && \ git clone https://github.com/nodenv/node-build.git ${NODENV_PATH}/plugins/node-build && \ git clone https://github.com/nodenv/nodenv-package-rehash.git ${NODENV_PATH}/plugins/nodenv-package-rehash ENV PATH ${NODENV_PATH}/shims:${NODENV_PATH}/bin:${PATH}
- EB アプリケーションバージョン用 (ベースイメージ + Source + gem + npm)
例) image tag: [project_code]-[commit_hash]-[YYYYMMDDHHMMSS]
FROM xxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/codebuild:ruby-2.4.3-node-v8.9.4 ・・・ COPY Gemfile /app/ COPY Gemfile.lock /app/ RUN bundle install --jobs=8 COPY . /app/ RUN npm run clean-install && \ npm run deploy
- CodeBuild のホストイメージ用 (Docker in Docker!)
CodeBuild 上でイメージのビルドやデプロイを行うためのホストイメージとなります。
Docker Official Repository: https://hub.docker.com/_/docker/
FROM docker:17.10.0-ce-dind RUN apk add --no-cache \ linux-headers \ musl-dev \ gcc \ make \ libffi-dev \ openssl-dev \ git \ bash \ curl \ zip \ less \ openssh-client \ py-pip \ python \ python-dev \ && \ pip install awscli fabric boto docker-compose
EB バージョン構成
Dockerfile、Dockerrun.aws.json と .ebextensions を zip に固めたものとなります。
- Dockerfile (EBバージョンイメージ + EXPOSE + CMD)
コンテナ起動用の Dockerfile で、FROM 部分の URI は アプリケーションバージョンを作る際に ECR 上の Image tag に書き換わります。
FROM xxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/eb:smarthr-xxxxxxx-201801190137 EXPOSE 3000 ENTRYPOINT [""] CMD /app/run.bash
- Dockerrun.aws.json
Logging などホストとコンテナ間のボリュームマッピングなど定義します。
{ "AWSEBDockerrunVersion": "1", "Logging": "/app/log", "Volumes": [ { "HostDirectory": "/var/letter_opener", "ContainerDirectory": "/app/tmp/letter_opener" }, { "HostDirectory": "/etc/nginx/www_root/public", "ContainerDirectory": "/app/public" } ] }
- .ebextensions
NewRelic などパッケージのインストールや必要な Script など実行できます。
イメージ・EBバージョン再利用
複数 env (App、Worker サーバなど) では環境変数によってフォアグラウンドで起動するプロセスを分けることで同一バージョンの再利用を可能にしています。
if [ "${SERVER_TYPE}" == "worker" ]; then exec bundle exec sidekiq -e ${RACK_ENV} -C /app/config/${SIDEKIQ_CONF_NAME} -L /app/log/sidekiq.log -P /var/run/sidekiq.pid else exec bundle exec pumactl -F /app/config/pumaconf.rb start fi
リリースフローを見直す
従来
- Github の master ブランチへマージすると CircleCI 経由でデプロイスクリプト (
eb deploy
) が実行され EB 環境へデプロイされます。 - DB マイグレーション用のブランチが別途存在していて、リリース時にはこちらへもマージする必要があり、Commit hash の違うバージョンが作られたりしました。
- Github の master ブランチへマージすると CircleCI 経由でデプロイスクリプト (
新
- migration ブランチの運用を廃止しました。
- master へマージすると CodeBuild 経由で Dockerfile 通りに image を build し、ECR へ push 後に アプリケーションバージョンが作られます。
- 作られたアプリケーションバージョンを QA 環境などにデプロイし確認後、問題なければ本番環境へデプロイします。
CI と CD を CircleCI から切り離す
- 従来
CircleCI は CI と CD の両方を担っており、CI にリソースが取られリリース待ちになる、という状態がよく発生していました。
新
- CI は Buildkite へ
- CD は CodeBuild へ移行することでリソース待ちが解消できて、デプロイもより快適になりました。
メンテ時の Worker の止め方
App サーバは nginx config で制御できるが Worker サーバはコンテナ内のプロセスを kill しても eb-docker サービスによってコンテナは再起動されます。
Docker daemon を stop してしまうとデプロイ時に Image を pull できないので、監視役の eb-docker サービスを止めた後にコンテナを止める方式にしました。
def _stopSidekiq(): sudo("initctl stop eb-docker") sudo("docker container stop $(sudo docker ps -ql)") def _startSidekiq(): sudo("initctl start eb-docker")
.ebextensionsの活用
- nginx のカスタムコンフィグ
- td-agent のインストール
- NewRelic や Mackerel のインストール
- assets:precompile
・・・ echo "Running assets:precompile for aws_beanstalk/current-app" docker run --rm -v /etc/nginx/www_root/public:/app/public "${EB_CONFIG_DOCKER_ENV_ARGS[@]}" aws_beanstalk/current-app bundle exec rake assets:precompile || echo "The assets:precompile failed to run." ・・・
- db:migrate
・・・ echo "Running migrations for aws_beanstalk/current-app" docker run --rm "${EB_CONFIG_DOCKER_ENV_ARGS[@]}" aws_beanstalk/current-app bundle exec rake ${RAKE_TASK}[${MIGRATION_PROCESS_COUNT}] > /tmp/migration.log || echo "The Migrations failed to run."
その他
ELB → ALB にすることでパフォーマンス向上と http2.0 への対応を行いました。 EB 上でコンソールからの ALB 設定がまだ対応していないので、CLI で行う必要があります。
$ eb create smarthr-staging-app --elb-type application -p docker-17.10.1-ce --vpc.id vpc-xxxxxxxx --vpc.ec2subnets subnet-private-1a,subnet-private-1c --vpc.elbsubnets subnet-public-1a,subnet-public-1c --vpc.elbpublic --vpc.securitygroups sg-xxxxxxxx -ip [InstanceProfile]
- VPC を新規し、踏み台以外の全サーバは Private Subnet へ移動しました。
- コンテナイメージのビルドやデプロイを担う Fabric を導入しました。
- Rundeck + Fabric を組み合わせ、デプロイをより快適にしました。
新アーキテクチャ
- インフラ
- デプロイ
- staging
- production
最後に
Docker 化の一番大きいメリットは EB の Ruby Platform に依存しなくなったことです。
本番環境が先行で Docker 化完了後、開発環境の Docker 化も一気に進み、弊社エンジニアの多くは Vagrant を捨て Docker で開発環境を立てています。
今後も Docker 化のリファクタリングなど引き続き行う予定ですので、Docker 上で Rails アプリを一緒に開発してみませんか?
追記
4/12 (木) のデプロイにて本番環境も ruby 2.5.1
に対応しました。