仕事で開発中のシステムで、 master ブランチに Pull Request が Merge されると自動的に AWS ECS に構築した社内向けの確認環境にデプロイが行われるような仕組みを導入した。自動テスト、コンテナイメージのビルド、デプロイには CircleCI を利用している。 .circleci/config.yml
は以下のような感じ。
version: 2
shared: &shared
working_directory: ~/app
docker:
- image: xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/organization/app
environment:
PGHOST: 127.0.0.1
PGUSER: user
RAILS_ENV: test
REDIS_HOST: localhost
- image: circleci/postgres:9.6-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
- image: redis:3.2-alpine
jobs:
build:
<<: *shared
steps:
- checkout
# Restore bundle cache
- &restore_cache
type: cache-restore
key: app-{{ arch }}-{{ checksum "Gemfile.lock" }}
# Bundle install dependencies
- &bundle_install
run: bundle install -j4 --path vendor/bundle
# Store bundle cache
- &save_cache
type: cache-save
key: app-{{ arch }}-{{ checksum "Gemfile.lock" }}
paths:
- vendor/bundle
# Database setup
- &db_setup
run:
name: Database Setup
command: |
bundle exec rake db:create
bundle exec rake db:structure:load
- type: shell
command: bundle exec rubocop
# Run rspec in parallel
- type: shell
command: |
mkdir coverage
COVERAGE=1 bundle exec rspec --profile 10 \
--format RspecJunitFormatter \
--out /tmp/test-results/rspec.xml \
--format progress \
$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
# Save artifacts
- type: store_test_results
path: /tmp/test-results
- type: store_artifacts
path: coverage
generate-doc:
<<: *shared
steps:
- run:
name: Install dependencies
command: |
apk add --no-cache git openssh ca-certificates
- checkout
- *restore_cache
- *bundle_install
- *save_cache
- *db_setup
# Generate document
- run:
name: Generate API doc
command: |
AUTODOC=1 bundle exec rake spec:requests
- run:
name: Generate Schema doc
command: |
diff=$(git diff HEAD^ db)
if [ -n diff ]; then
bundle exec rake schema_doc:out > doc/schema.md
fi
- run:
name: Setup GitHub
command: |
export USERNAME=$(git log --pretty=tformat:%an | head -1)
export EMAIL=$(git log --pretty=tformat:%ae | head -1)
git config --global user.email "${EMAIL}"
git config --global user.name "${USERNAME}"
- run:
name: Push updated doc to GitHub
command: |
git add doc
git commit --quiet -m "[ci skip] API document Update
${CIRCLE_BUILD_URL}"
git push origin ${CIRCLE_BRANCH}
deploy:
docker:
- image: docker:17.05.0-ce-git
steps:
- checkout
- setup_remote_docker
- run:
name: Install dependencies
command: |
apk add --no-cache \
py-pip=9.0.0-r1 jq curl curl-dev bash
pip install \
docker-compose==1.12.0 \
awscli==1.11.76
curl https://raw.githubusercontent.com/silinternational/ecs-deploy/ac2b53cb358814ff2cdf753365cc0ea383d7b77c/ecs-deploy | tee -a /usr/bin/ecs-deploy \
&& chmod +x /usr/bin/ecs-deploy
- restore_cache:
keys:
- v1-{{ .Branch }}
paths:
- /caches/app.tar
- 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: |
docker build --file=docker/app/Dockerfile --cache-from=app -t organization/app .
- run:
name: Save Docker image layer cache
command: |
mkdir -p /caches
docker save -o /caches/app.tar organization/app
- save_cache:
key: v1-{{ .Branch }}-{{ epoch }}
paths:
- /caches/app.tar
- run:
name: Push application Docker image to ECR
command: |
login="$(aws ecr get-login --region ap-northeast-1)"
${login}
docker tag organiation/app:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/organization/app:latest
docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/organization/app:latest
- run:
name: Deploy container
command: |
ecs-deploy \
--region ap-northeast-1 \
--cluster app-dev \
--service-name puma \
--image xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/organization/app:latest \
--timeout 300
workflows:
version: 2
build-and-generate-doc:
jobs:
- build
- generate-doc:
requires:
- build
filters:
branches:
only:
- master
- deploy:
requires:
- build
filters:
branches:
only:
- master
- master ブランチに対して出された Pull Request が Merge される
- CircleCI でテストが実行される
- テストが成功すると CircleCI 上からデプロイが行われる
- コンテナイメージをビルド
- ビルドしたイメージを AWS ECR にプッシュ
- プッシュしたイメージを利用するタスクを AWS ECS に作成
ecs-deploy 任せ - 古いコンテナから新しいコンテナに LB 切り替え
こちらも ecs-deploy にやってもらってる
- CircleCI 上実行された Request Spec で自動生成された API ドキュメントを GitHub にプッシュ
コードが Merge されると勝手に確認環境にデプロイされるので、クライアントサイドの開発者からデプロイを頼まれて対応する必要がないし、クライアントサイドの人はいつでも最新の API ドキュメントを GitHub 上で確認できる。 API ドキュメントは手動更新ではなくテストから自動生成されるので、ドキュメントと実際の API の挙動が異なる、というありがちな問題も回避できる。
自分としては結構頑張ったつもりだったんだけど、「それ ECS でやる意味あるの? というか Docker じゃなくて普通の EC2 インスタンスに Capistrano でデプロイするのでよくね?」というツッコミが入った。デプロイフローで CircleCI への依存度が強すぎる、イメージのビルドとデプロイに時間がかかりすぎるし、ちょっとした typo の修正のためにイメージをビルドしたりとかあり得ない、 Docker を使うにしても ECS は使わず、 EC2 で Docker を動かし、コンテナがマウントしたディレクトリに Capistrano でデプロイするべき、という意見だった。このときぐぬぬとなってしまってあまりうまく答えられなかったので考えられるメリットを書き出してみる。
確かに Docker と ECS による環境を構築するのには時間がかかる。デプロイのためにそこそこでかいイメージをビルドしてプッシュするというのも大袈裟だ。加えて Production で運用するとなるとログの収集やデータベースのマイグレーションなど、考えなければならない問題がいくつかある1。
ただコンテナベースのデプロイには以下のようなメリットがあると思う。
環境のポータビリティー
まず Ruby や Rails などのバージョンアップが容易になる。手元で試して確認した構成とほぼほぼ同じイメージをデプロイできる。デプロイ前にサーバーに新しいバージョンの Ruby をインストールしたりしなくて済むし、手元ではエラーにならなかったのに本番でエラーになった、というようなケースを減らすことができる。
サーバー構築手順のコード化
人数が少ない会社で専業のインフラエンジニアもいない状況だと Chef や Puppet でサーバーの構成管理をし、複数台あるサーバー群の管理をすることは難しい。 Dockerfile に手順を落とし込み、 Docker さえ入ってたらあとは何も考えなくて良いというのはとても助かる。少なくとも秘伝のタレ化しやすいサーバーの構築手順がコード化され、コードレビューのプロセスに載せることができる。
迅速なスケール
AWS ECS のようなマネージドコンテナサービスと組み合わせて使えばスケールアウトが楽ちん極まりない。 AWS マネジメントコンソールか cli で操作するだけで簡単にスケールさせることができる。スケールに際して LB に組み込む前にプロビジョニングしたり最新のコードをデプロイしたりする必要もない。
デプロイ失敗が減る
Capistrano によるデプロイはデプロイ対象が増えてくると SSH が不安定になりデプロイに失敗することが増えてくる。 ECS のような AWS の仕組みに載せることで、イメージを ECR にプッシュさえできれば IaaS 側でよろしくやってくれるというのはとても良い。
以上のようなところだろうか。まだ Production に投入するところまでは持って行けてないので、今の自分の考察が正しいのかどうかをこれから検証していきたい。
関連してそうな記事
-
いまは先人がいっぱいいるのでログの集約もマイグレーションも情報はいっぱいあると思う ↩