概要
この記事ではRailsアプリケーションをAWS ECSへデプロイする方法を記述いたします
チュートリアル的な記事はよく見かけるのですが、実際に運用しているアプリケーションをどうデプロイするか、困った点などを中心にお伝えできればと思います
事前知識としてDockerについてはお調べください、乱暴にいうと軽量・高速な仮想環境(以降、コンテナと呼びます)を提供してくれるものです
システム要件
- Ruby: 2.4.1
- Rails: 5.0.2
- DB: PostgreSQL 9.6.3
- Store: Redis 3.2.9
Dockerの設定
システム要件それぞれを一つのコンテナに封じ込めます
その際にDocker Composeというものを利用します
OSはAlpineというものが特に軽量なのでそれを使っています
version: "3"
services:
psql:
image: postgres:9.6.3-alpine
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:3.2.9-alpine
volumes:
- redis-data:/data
worker:
environment:
DATABASE_URL: postgres://postgres@psql:5432
REDIS_URL: redis://redis:6379
build:
context: .
dockerfile: ./docker/development/Dockerfile
volumes:
- app-data:/app
depends_on:
- psql
- redis
command: bundle exec sidekiq -C config/sidekiq.yml
web:
environment:
DATABASE_URL: postgres://postgres@psql:5432
REDIS_URL: redis://redis:6379
build:
context: .
dockerfile: ./docker/development/Dockerfile
volumes:
- app-data:/app
depends_on:
- psql
- redis
command: bin/rails s --bind 0.0.0.0
ports:
- "3000:3000"
volumes:
postgres-data:
redis-data:
app-data:
external: true
MySQLやMemcachedなど他のミドルウェアを使う際などは適宜変更が必要です
volumeには永続化したいものを記述しているのですが、Macを使っている方はファイルの同期が遅すぎる問題(2017.9.7執筆時点)があるのでdocker-syncというものを使います
gem install docker-sync
brew install unison
brew install unox
でインストールして(Homebrew依存)、設定ファイルを書き込みます
version: "2"
syncs:
app-data:
src: '.'
sync_strategy: 'unison'
sync_excludes: ['.gitignore', '.git/', 'docker*', 'tmp/pids', 'log']
そしてコンテナを作るためのファイルを置けば準備完了です
FROM ruby:2.4.1-alpine
RUN apk add -U --no-cache \
bash \
fontconfig \
postgresql-client postgresql-dev \
git \
alpine-sdk \
nodejs \
tzdata
RUN mkdir /app
WORKDIR /app
ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
# Using hosts cache
ADD vendor/bundle /app/vendor/bundle
RUN bundle install --jobs=4 --retry=5 --path=vendor/bundle --without staging production
ADD . /app
Gemをインストールする際に足りないライブラリなどが多いのでそれを解決するためにalpine-sdkを入れることをおすすめします(少しサイズが大きいのでDockerのイメージサイズを少しでも下げたい場合は個別にインストールすることを推奨)
もし他に欲しいパッケージ(ImageMagickなど)があったら適宜インストールしてください
オプション
.dockerignoreファイルに記載されたものはビルド時にファイルコピーされません
うまく設定してみてください
Rails側での設定
Dockerを利用する際に構築の思想としてTwelve-Factor Appを読むことをオススメします
ここの第3節にあるようにアプリケーションの設定を環境変数に格納するようにしました
Gemとしてはconfigを使います
default: &default
adapter: <%= ENV.fetch("DB_ADAPTER", "postgresql") %>
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
url: <%= ENV.fetch("DATABASE_URL", 'postgres://postgres@psql:5432') %>/development
test:
<<: *default
url: <%= ENV.fetch("DATABASE_URL", 'postgres://postgres@localhost:5432') %>/test
staging:
<<: *default
url: <%= ENV['DATABASE_URL'] %>
production:
<<: *default
url: <%= ENV['DATABASE_URL'] %>
大体の環境変数はENV.fetch('環境変数名', 'デフォルト値')
で読み込むようにしておくと良いです
redis_url: <%= ENV.fetch('REDIS_URL', 'redis://redis:6379') %>
Sidekiq::Logging.logger = Rails.logger
Sidekiq.configure_server do |config|
config.redis = { url: Settings.redis_url, namespace: "sidekiq_#{Rails.env}" }
end
Sidekiq.configure_client do |config|
config.redis = { url: Settings.redis_url, namespace: "sidekiq_#{Rails.env}" }
end
先述したconfigというGemは設定したものをSettings.yaml_key
のような形式で読み取ることができます
ローカルで動かしてみる
これで準備は整いました(アプリケーションの構成はそれぞれのサービスによって大きく異なるので、最低限の情報しか記載していません、起動してみてRailsのエラーが出た場合は適宜修正してください)
docker-sync start # OS Xの場合のみ
docker-compose run worker bin/rails db:create db:migrate
docker-compose up # Ctrl-cで終了
docker-sync stop # OS Xの場合のみ、アプリ開発 終了時
docker-compsoe up
するとdocker-compose.ymlに記載された情報にしたがって、コンテナが立ち上がります
今回はWebサーバとしてPumaをWorker(非同期処理のサーバ)としてSidekiqを立ち上げる設定になっています
うまく起動されているようでしたら、Pumaへ接続してみてください
デプロイ
大きく悩んだのはここでした
AWS ECS
ドキュメントを参照して、正しくリポジトリ(ECR)とタスク定義、クラスタを作成、クラスタへサービスを登録してください
タスク定義でコンテナを設定する際のポートはホストが0でコンテナが3000としてください、またコンテナにはエンドポイントは不要で、コマンドにPumaやSidekiqを起動するコマンドを記載してください(カンマ区切り)
今回実際にデプロイしているアプリケーションは複雑な構成ではないのでPumaの前段にNginxなどを設置せずにシンプルな構成をとりました
Javascript/CSS/Imageなどのアセットファイルもサーバが直接提供します(ここはうまいことCDNでキャッシュしてやるとPumaの負担が減ります)
そして継続デプロイとしてCircleCIを使用しています
FROM ruby:2.4.1-alpine
RUN apk add -U --no-cache \
bash \
fontconfig \
postgresql-client postgresql-dev \
git \
alpine-sdk \
nodejs \
tzdata
RUN mkdir /app
WORKDIR /app
ADD Gemfile /app/Gemfile
ADD Gemfile.lock /app/Gemfile.lock
RUN bundle install --jobs=4 --retry=5 --path=vendor/bundle --without test development
ADD . /app
ARG RAILS_ENV
ARG DB_ADAPTER
ARG SECRET_KEY_BASE
RUN RAILS_ENV=${RAILS_ENV} \
DB_ADAPTER=${DB_ADAPTER} \
SECRET_KEY_BASE=${SECRET_KEY_BASE} \
bin/rails assets:precompile
version: 2
jobs:
build:
working_directory: ~/deploy
docker:
- image: docker:17.07.0-ce-git
steps:
- checkout
- setup_remote_docker
- apk add --no-cache py-pip=9.0.1-r1
- pip install awscli==1.11.129 ecs-deploy==1.2.0
- restore_cache:
keys:
- v1-{{ .Branch }}
- run:
name: Load Docker image layer cache
command: |
set +o pipefail
docker load -i /caches/app.tar | true
- run:
name: Build application Docker image
command: |
export SECRET_KEY_BASE=$(aws ssm get-parameter --name staging.secret_key_base --output text --query "Parameter.Value" --with-decrypt)
docker build --cache-from=app -t app \
--build-arg RAILS_ENV=staging \
--build-arg DB_ADAPTER=nulldb \
--build-arg SECRET_KEY_BASE=${SECRET_KEY_BASE} \
-f docker/deploy/Dockerfile .
- run:
name: Save Docker image layer cache
command: |
mkdir -p /caches
docker save -o /caches/app.tar app
- save_cache:
key: v1-{{ .Branch }}-{{ epoch }}
paths:
- /caches/app.tar
- run:
name: Deploy Docker image
command: |
eval $(aws ecr get-login --no-include-email)
docker tag app YOUR_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/staging:${CIRCLE_BUILD_NUM}
docker push YOUR_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/staging:${CIRCLE_BUILD_NUM}
- run:
name: Update services
command: |
ecs deploy staging worker --tag ${CIRCLE_BUILD_NUM}
ecs run staging worker-staging -c worker "bin/rails db:migrate"
ecs deploy staging web --tag ${CIRCLE_BUILD_NUM}
困った点
- アセットファイルを作成する際になぜかデータベースに接続しにいっている
- 秘匿性の高い環境変数どうしよう問題
- ECSへのデプロイどうしよう問題
解決策
色々と問題があったのですが、詳しく掘り下げていきましょう
1. アセットファイルについて
普通にsprocketsを使ってjavascript(coffeescript)をコンパイルしていたのでここで引っかかりました
他のサービスではwebpackerを使っておりwebpackでビルドするだけなので、また違う方法が必要です
ここはアセットファイルの扱いによってかなり変わってくる部分なのでそれぞれ対応が必要かと思います
2. 秘匿性の高い環境変数について
今回はAWSを使うのでAWS上で暗号化して保持するようにしました
方法はなんでも良いと思うので、とにかく外部に漏れないような設定を心がけてください
逆にRAILS_ENVなどのどうでも良い環境変数についてはベタがきで大丈夫です
3. ECSへのデプロイについて
AWS ECSへのデプロイに関しては他にもRubyやShell Scriptで書かれた開発が活発なツールがあります(CookpadのHakoなど)
私はPythonのbotoというAWSへのインターフェースを使ってAWSを操作するラッパーを作りかけたことがありPythonを選択しました
CircleCIでテスト後にデプロイする
大体のプロダクトでは頑張ってテストを書いているかと思います
CircleCIでテストが通ってからデプロイする場合はworkflowが利用できます
version: 2
jobs:
build:
working_directory: ~/test
docker:
- image: ruby:2.4.1-alpine
environment:
RAILS_ENV: test
RACK_ENV: test
- image: postgres:9.6.3-alpine
- image: redis:3.2.9-alpine
steps:
- checkout
- run: apk add -U --no-cache
fontconfig
postgresql-client postgresql-dev
git
alpine-sdk
nodejs
tzdata
- restore_cache:
keys:
- gem-v0.1-{{ checksum "Gemfile.lock" }}
- gem-v0.1
- gem-v0
- run: bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4
- type: cache-save
key: gem-v0.1-{{ checksum "Gemfile.lock" }}
paths:
- vendor/bundle
- run: bundle exec rake db:create db:schema:load --trace
- run: bundle exec rspec --profile 10 --format documentation --out /tmp/test-results/rspec.txt --format progress spec
- type: store_test_results
path: /tmp/test-results
deploy:
working_directory: ~/deploy
docker:
- image: docker:17.07.0-ce-git
steps:
- checkout
- setup_remote_docker
- apk add --no-cache py-pip=9.0.1-r1
- pip install awscli==1.11.129 ecs-deploy==1.2.0
- restore_cache:
keys:
- v1-{{ .Branch }}
- run:
name: Load Docker image layer cache
command: |
set +o pipefail
docker load -i /caches/app.tar | true
- run:
name: Build application Docker image
command: |
export SECRET_KEY_BASE=$(aws ssm get-parameter --name staging.secret_key_base --output text --query "Parameter.Value" --with-decrypt)
docker build --cache-from=app -t app \
--build-arg RAILS_ENV=staging \
--build-arg DB_ADAPTER=nulldb \
--build-arg SECRET_KEY_BASE=${SECRET_KEY_BASE} \
-f docker/deploy/Dockerfile .
- run:
name: Save Docker image layer cache
command: |
mkdir -p /caches
docker save -o /caches/app.tar app
- save_cache:
key: v1-{{ .Branch }}-{{ epoch }}
paths:
- /caches/app.tar
- run:
name: Deploy Docker image
command: |
eval $(aws ecr get-login --no-include-email)
docker tag app YOUR_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/staging:${CIRCLE_BUILD_NUM}
docker push YOUR_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/staging:${CIRCLE_BUILD_NUM}
- run:
name: Update services
command: |
ecs deploy staging worker --tag ${CIRCLE_BUILD_NUM}
ecs run staging worker-staging -c worker "bin/rails db:migrate"
ecs deploy staging web --tag ${CIRCLE_BUILD_NUM}
workflows:
version: 2
build_and_deploy:
jobs:
- build
- deploy:
requires:
- build
filters:
branches:
only:
- deploy
運用に関して
プロダクション環境での運用はまだしていないのですが、開発環境で使っている段階では
ログはタスク定義のawslogs(事前にAWS Cloudwatchで設定が必要)に出力して大丈夫かと思います。ECSは標準出力をログとして扱うので環境変数RAILS_LOG_TO_STDOUT
を設定するかGemとしてrails_12factorを入れるなどしてください
ログをみる際はいちいちCloudwatchからみていられないのでawslogsを利用しています
コンテナの中で作業したいときはECSにより立ち上がったEC2コンテナにsshで接続して
docker ps # コンテナIDを確認
docker exec -it CONTAINER_ID bash # コンテナのbashにログイン
bin/rails c
などしています
普通に安定運用している限りコンテナに入ることはないと思います(そもそもコンテナに入るのは煩わしいです)
経緯
最近副業でHerokuからAWSへのインフラ移行を手伝わせてもらっています
Herokuを使うのは初めてだったのですが、デプロイが超簡単なんですね
最初はAnsibleでサーバを構築してCapistranoでデプロイする気でした
デプロイが活発(かつ自動的)なサービスだったのでこの構成にすることで、自由度の高いインフラを手にしつつ手間もそこまで変わらないようにできたと思います
ただCircleCIからAWS ECSへのデプロイはHerokuへのデプロイに比べると時間がかかってしまうので、そこをどうにかできたらなぁと思います
最後に
Dockerの環境さえ作っておけばGoogle Cloud Platformなど他のクラウドサービスやオンプレミスへの移行もしやすいかと思います
この記事をみて興味を持ってくれた方が移行を検討していただければ幸いです
また本記事には最低限の設定しか記載しておらず、設定の記述漏れなどがある(ような気がします)
私に分かることでしたら、お答えしますのでTwitterのアカウントへ連絡願います