【rails + docker】CircleCIによるテスト→ECRにpush→ECSにdeploy の環境構築をサポートしてくれるシェルスクリプトを作ってみた


はじめに

この記事は以前、私が書いた以下記事の改良版となります。

CircleCIによるrails自動テスト環境を構築してくれるシェルスクリプトを作ってみた

前回は開発環境構築まででしたが、今回はCircleCIによる自動テストまでをできるようにしました。
また、今回は手動で設定するべき項目が結構多いので「構築サポート」してくれるシェルスクリプトとしています。。。

私の環境は以下の通りです。

OS 環境
Windows 10 Home 64bit Ubuntu 18.04(WSL2)

構築する環境

構築する環境は以下の通りです。
GitHubにプッシュ → 自動テスト → ESRにイメージプッシュ → ECSにデプロイができるようになることをゴールとします。

項目    使用したもの バージョン
言語 ruby 2.6.6
Webフレームワーク rails 6.0.2
テストフレームワーク rspec 3.9
データベース  MySQL 8.0
Webサーバ Nginx 1.17.10
APサーバ puma 4.3.5

コンテナ構成は以下の通りです。

appコンテナ
ruby
rails
puma
dbコンテナ
MySQL
webコンテナ
Nginx
項目    使用したもの
コンテナリポジトリ ECR
コンテナオーケストレーション ECS

できること

GitHubへコードをpushしたら、CircleCIがそれを検知して、
自動的テスト → ECRにイメージを自動プッシュ → ECSに自動デプロイ
を実行してくれます。

最終的に目指す構成は以下の通りです。
(ALBRoute53が記載されていますが、必須ではないです。)

今回作成したシェルは以下の通りです。

今回作成したシェルスクリプト

まずこちらをローカルに落としてきて、[アプリ名]を引数として sudoで実行します。

この[アプリ名]は後のECSの設定に使うので覚えておいてください。

実行権限が付与されていない場合はchmodで実行権限を付与してあげてください。

10分ほど待って、以下状態になったら実行完了です。

app_1  | * Listening on tcp://0.0.0.0:3000
app_1  | * Listening on unix:///myapp/tmp/sockets/puma.sock
app_1  | Use Ctrl-C to stop

自動テストをするために CircleCI ⇔ GitHub のプロジェクト連携が必要です。
CircleCIのアカウントからポチポチすれば簡単に連携できます。

↑↑↑↑↑↑↑↑↑前回はここまで↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

次に、CircleCIの環境変数を追加します。

定義する環境変数としては以下の通りです。

# CircleCI上で指定が必要な環境変数
# AWS_ACCOUNT_ID          : AWSのデフォルトリージョン
# AWS_ACCESS_KEY_ID       : AWSのアクセスキー
# AWS_SECRET_ACCESS_KEY   : AWSのシークレットキー
# AWS_REGION              : AWSのデフォルトリージョン
# AWS_ECR_ACCOUNT_URL     : AWS ECRリポジトリのURL

次にAWSに各サービスを構築していくのですが、コンテナ関連サービス以外は割愛します。。。
主な作業は以下の通りです。

  • VPCの作成
  • PublicSubnet、PrivateSubnetの作成
  • RDSの構築
  • セキュリティグループの設定

ECRにはあらかじめレポジトリを作成します。
このとき、レポジトリの名前は以下の通り設定します。

  • [アプリ名]_app(appコンテナ用)
  • [アプリ名]_web(webコンテナ用)

イメージも適当なものをpushしておきます。

ECSのクラスタを作成します。

クラスタ名は[アプリ名]-clusterとしてください。
それ以外は適当でOKです。

次にECSのタスク定義を作成していきます。
クラスタ名は[アプリ名]-taskとしてください。

タスク定義内、コンテナ名は以下の通り設定してください

  • app(appコンテナ用)
  • web(webコンテナ用)

また、appコンテナには環境変数を定義する必要があります。

定義する環境変数は以下の通りです。

# AWS上で指定が必要な環境変数
# RAILS_MASTER_KEY          : ローカル環境にあるmaster.keyの値
# MYAPP_DATABASE_USER       : 本番環境データベースユーザ
# MYAPP_DATABASE_PASSWORD   : 本番環境データベースパスワード

次にECSのサービスを作成していきます。
クラスタ名は[アプリ名]-serviceとしてください。
今回はALBを使用したのでターゲットグループが設定されていますが、こちらは必須ではありません。

サービス内のタスク定義が13であることに注目してください。

自動デプロイを試すためにgithubにpushします。
すると、CirleCI上で処理が始まります。

CircleCIによるテスト → ECRにpush → ECSにdeploy

が自動で行われています。

CircleCI上の処理が終わった後、もう一度ECSのサービスの状態を見てみると。。

デプロイが成功してタスク定義が15に置き換わっていることが分かります。
appコンテナ、webコンテナそれぞれデプロイしたため+2されています。
ECRのレポジトリも確認すると新しいイメージがpushされていると思います。

シェルの内容

前回からの変更点

前回記事からの主な変更点は以下の通りです。

  • Dockerfileを開発環境と本番環境で分ける
  • CircleCI用のコンフィグファイルを修正

Dockerfileを開発環境と本番環境で分ける

スクリプト内、以下部分で開発用Dockerfile, 本番用Dockerfile を分けて作成しています。

# app用のDockerfile 作成(開発用)
echo "make app Dockerfile"
cat << EOF > docker/app/Dockerfile_development
FROM ruby:${RUBY_VERSION}
RUN apt-get update -y && \\
    apt-get install -y default-mysql-client nodejs npm sudo && \\
    npm install -g -y yarn
RUN mkdir /${APP_NAME}
WORKDIR /${APP_NAME}
COPY ./src/Gemfile Gemfile
COPY ./src/Gemfile.lock Gemfile.lock
RUN bundle install && \\
    rails webpacker:install
RUN useradd -Nm -u ${USER} ${USER_NAME} && \\
    groupadd -g ${GROUP} ${USER_NAME} && \\
    usermod -aG sudo ${USER_NAME} && \\
    usermod -u ${USER} -g ${GROUP} ${USER_NAME} && \\
    echo ${USER_NAME}:${USER_PASSWORD} | chpasswd
COPY ./src /${APP_NAME}
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
EOF

# app用のDockerfile 作成(本番用)
cat << EOF > docker/app/Dockerfile_production
FROM ruby:${RUBY_VERSION}
RUN apt-get update -y && \\
    apt-get install -y default-mysql-client nodejs npm sudo && \\
    npm install -g -y yarn
RUN mkdir /${APP_NAME}
WORKDIR /${APP_NAME}
COPY ./src/Gemfile Gemfile
COPY ./src/Gemfile.lock Gemfile.lock
RUN bundle install && \\
    rails webpacker:install
COPY ./src /${APP_NAME}
CMD ["bundle", "exec", "puma", "-e", "production", "-C", "config/puma.rb"]
EOF

分けている理由としては

  • 本番環境では開発用ユーザが不要であるため
  • アプリケーションを本番環境として起動するため

が挙げられます。

本番用Dockerfileの中身を見てみると、developmentユーザを作成しておらず,
pumaの起動を-e productionというオプションを付けて実行していることが分かります。

また、本番ではRDSを使うのでdbコンテナは使用しません。
加えて本番ではECSを使うのでdocker-composeも使用しません。

CircleCI用のコンフィグファイルを修正

以下処理でCircleCIのコンフィグファイルを作成しています。
基本的にCircleCIの公式ドキュメントを参考にしています。

# .circleci/config.yml 作成
echo "make .circleci/config.yml"
mkdir -p .circleci
cat << EOF > .circleci/config.yml
version: 2.1
jobs:
  test:
    machine:
      image: circleci/classic:edge
    working_directory: ~/repo
    steps:
      - checkout
      - run:
          name: Install Docker Compose
          command: |
            curl -L https://github.com/docker/compose/releases/download/1.19.0/docker-compose-`uname -s`-`uname -m` > ~/docker-compose
            chmod +x ~/docker-compose
            sudo mv ~/docker-compose /usr/local/bin/docker-compose
      - run:
          name: docker-compose up --build -d
          command: docker-compose up --build -d
      - run: sleep 30
      - run:
          name: docker-compose run app rails db:create
          command: docker-compose run app rails db:create
      - run:
          name: docker-compose run app rails db:migrate
          command: docker-compose run app rails db:migrate
      - run:
          name: docker-compose run app bundle exec rspec spec
          command: docker-compose run app bundle exec rspec spec
      - run:
          name: docker-compose down
          command: docker-compose down

#
orbs:
  aws-ecr: circleci/[email protected]
  aws-ecs: circleci/[email protected]

workflows:
  test:
    jobs:
      - test
 #webコンテナをデプロイ
  deploy-web:
    jobs:
   #webコンテナのイメージをECRにプッシュ
      - aws-ecr/build-and-push-image:
          filters:
            branches:
              only: master
          account-url: AWS_ECR_ACCOUNT_URL
          create-repo: true
          dockerfile: docker/web/Dockerfile
          repo: "${APP_NAME}_web"
          region: AWS_REGION
          tag: "\${CIRCLE_SHA1}"
   #webコンテナのイメージをECSにデプロイ
      - aws-ecs/deploy-service-update:
          requires:
            - aws-ecr/build-and-push-image
          family: "${APP_NAME}-task"
          cluster-name: "${APP_NAME}-cluster"
          service-name: "${APP_NAME}-service"
          container-image-name-updates: "container=web,tag=\${CIRCLE_SHA1}"
 #appコンテナをデプロイ
  deploy-app:
    jobs:
      #webコンテナのイメージをECRにプッシュ
      - aws-ecr/build-and-push-image:
          filters:
            branches:
              only: master
          account-url: AWS_ECR_ACCOUNT_URL
          create-repo: true
          dockerfile: docker/app/Dockerfile_production
          repo: "${APP_NAME}_app"
          region: AWS_REGION
          tag: "\${CIRCLE_SHA1}"
      #webコンテナのイメージをECSにデプロイ
      - aws-ecs/deploy-service-update:
          requires:
            - aws-ecr/build-and-push-image
          family: "${APP_NAME}-task"
          cluster-name: "${APP_NAME}-cluster"
          service-name: "${APP_NAME}-service"
          container-image-name-updates: "container=app,tag=\${CIRCLE_SHA1}"
EOF

ECR、ECSのorbsをインポートして、deploy-web以下の処理でwebコンテナをデプロイ、deploy-app以下の処理でappコンテナをデプロイしています。

configファイル内にはいくつか変数が使用されていますが、CircleCI側でそれらを定義する必要があります。
「できること」内でも記載したように、CircleCI上で以下環境変数を定義します。

# CircleCI上で指定が必要な環境変数
# AWS_ACCOUNT_ID          : AWSのデフォルトリージョン
# AWS_ACCESS_KEY_ID       : AWSのアクセスキー
# AWS_SECRET_ACCESS_KEY   : AWSのシークレットキー
# AWS_REGION              : AWSのデフォルトリージョン
# AWS_ECR_ACCOUNT_URL     : AWS ECRリポジトリのURL

以上になります、これでCircleCIによるCI/CDができるようになりました。
何かの参考に慣れば幸いです。

参考記事

以下記事を参考にさせて頂きました。ありがとうございましたm(_ _)m

AWS ECR/ECS へのデプロイ

丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)

CircleCI Orbsで ECR/ECS にデプロイ