DEVELOPER’s BLOG

技術ブログ

アジャイル開発を加速させるCircleCI×AWS ECSを活用した、テスト・デプロイの自動化

2023.10.19 Harumi Motono
AWS CI/CD SRE
アジャイル開発を加速させるCircleCI×AWS ECSを活用した、テスト・デプロイの自動化

目次

  • 挨拶
  • CI/CD導入の背景
  • 前提
  • プロジェクト構成
  • ディレクトリ構成
  • .circleci/config.yml 全体の流れ
  • 試行錯誤したところ
  • 今後の展開


挨拶

CircleCIでdocker-composeを使ってテストからAWS ECSへのデプロイまでを自動化してみたので紹介します。


CI/CD導入の背景

参加しているプロジェクトではアジャイルを取り入れ、毎月リリースするようになりました。
毎月リリースがあるということは、それだけ機能改修、追加、削除があるということで、その分テストが必要になります。リリースの作業も毎月やってきます。時間がかかります。

そこで、CircleCIを導入しテスト工程とデプロイ工程、さらには2工程間の処理を自動化することで、工数を減らしつつ品質担保を実現しました。


前提

AWS ECSのサービス/クラスターの作成や、CircleCIの登録とSet up projectまで済んでいます。
AUCではVCSにGitHubを、CI/CDツールとしてCircleCIを利用しています。どちらもAUCが構築したAWS上で動いています。
技術ブログ『AWSを利用した弊社の開発環境』


プロジェクト構成

プロジェクトは、AWS ECS上に構築され、開発はDockerを用いてRuby on Railsで行っています。テストはRSpecで行っています。

flow.png


ディレクトリ構成

.
├── .circleci
│   └── config.yml
├── Dockerfile
├── Dockerfile.ecs
├── docker-compose.ecs.yml
├── docker-compose.rspec.yml
├── docker-compose.yml
├── ecs-service.json
├── spec

docker-composeファイルは、開発用、CirlceCIのRSpec実行ジョブ用、CirlceCIのAWS デプロイジョブ用の3つに分けています。それぞれで無駄なイメージをプルしたり、不要なキャッシュを設けたりするのを省くためです。Dockerfileも同じように不要な設定を省く目的で2つに分けています。

全体の流れを紹介するために.circleci/config.ymlとデプロイに使うdocker-compose.ecs.ymlを載せています。docker-compose.rspec.ymlやDockerfileは長くなるので割愛しました。

.circleci/config.yml

version: 2.1

jobs:
  rspec:
    docker:
      - image: cimg/python:3.11.5
    working_directory: ~/repo
    steps:
      - checkout
      - setup_remote_docker
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-
      - run:
          name: Create Docker Network
          command: docker network create --subnet=172.19.0.0/16 itid_group
      - run:
          name: Start Docker Compose services
          command: docker-compose -f docker-compose.rspec.yml up --build -d
      - run:
          name: Create DB
          command: docker exec -it app_web bundle exec rake db:create RAILS_ENV=test
      - run:
          name: Run RSpec tests
          command: |
            docker exec -it app_web bundle exec rspec
            docker-compose down
      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

  build_image:
    docker:
      - image: cimg/python:3.11.5
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Install AWS CLI
          command: |
            sudo pip install awscli
      - run:
          name: Build image
          command: |
            $(aws ecr get-login --no-include-email --region ap-northeast-1)
            docker-compose -f docker-compose.ecs.yml build --build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY} --build-arg RAILS_ENV=production
            docker tag project_web ${ECR_DOMAIN}/app-web:$CIRCLE_SHA1
            docker tag project_web ${ECR_DOMAIN}/app-web:latest
      - run:
          name: Push Docker Image
          command: |
            docker push ${ECR_DOMAIN}/app-web:$CIRCLE_SHA1
            docker push ${ECR_DOMAIN}/app-web:latest
  deploy:
    docker:
      - image: cimg/python:3.11.5
    steps:
      - run:
          name: Install AWS CLI
          command: |
            sudo pip install awscli
      - run:
          name: Migration
          command: |
            aws ecs run-task \
              --region ap-northeast-1 \
              --launch-type FARGATE \
              --network-configuration "awsvpcConfiguration={subnets=["subnet-*****************", "subnet-*****************", "subnet-*****************", "subnet-*****************"],securityGroups=["********************"],assignPublicIp=ENABLED}" \
              --cluster app-ecs-cluster --task-definition app-migrate
      - run:
          name: Deploy
          command: |
            aws ecs update-service --cluster app-ecs-cluster --service app-service --task-definition app --force-new-deployment

workflows:
  version: 2
  test:
    jobs:
      - rspec
      - build_image:
          filters:
            branches:
              only: ecs_deploy
      - deploy:
          requires:
            - build_image
          filters:
            branches:
              only: ecs_deploy


docker-compose.ecs.yml

version: "3.9"
services:
  web: &web
    container_name: 'app_web'
    build:
      context : .
      dockerfile: Dockerfile.ecs
    command: bash -c "yarn install &&
             rm -f /app/tmp/pids/server.pid &&
             freshclam &&
             service clamav-daemon start &&
             bundle exec rails s -p 3000 -b '0.0.0.0'"
    ports:
      - "3000:3000"
    tty: true
    stdin_open: true
    environment:
      REDIS_URL: 'redis://localhost:6379/1'
    networks:
      - app_group
      - default
  # 毎回ビルドする必要がないため、別個ECRにpushした
  # redis:
  #   container_name: 'app_redis'
  #   image: redis:7-alpine
  #   ports:
  #     - "6379:6379"
  #   command: redis-server --appendonly yes
  sidekiq:
    <<: *web
    container_name: 'app_sidekiq'
    ports:
      - "5001:3000"
    command: bundle exec sidekiq -C config/sidekiq.yml


.circleci/config.yml 全体の流れ

  1. プロジェクトメンバーがGitHubにpushすると、CircleCIがトリガーされ、rspec jobのRSpecテストが実行されます。

  2. RSpecのテストが通ったらレビューを行います(人力)。これにパスしたら開発環境へのデプロイ用ブランチ ecs_deployにmergeします(人力)。

  3. ここで再度CircleCIが実行され、今度はrspec job実行後、build_image jobでAWS ECRにイメージが保存されます。

  4. イメージの保存が完了すると、最後にdeploy jobが実行され、ECRに保存されたイメージを利用してAWS ECSにアプリケーションがデプロイされます。

build image jobのPush Docker Imageで$CIRCLESHA1とlatest2つのタグをつけたイメージをpushしています。
latestタグがついたイメージをdeploy jobで使用しています。$CIRCLE
SHA1タグをつけるのはバージョン管理のためです。
CircleCI 定義済み環境変数

deploy jobでは、まずapp-web:latestイメージを用いてDBのマイグレーションを行っています。
その後、Deployで同じapp-web:latestイメージを用いてapp-deployタスクを実行し、app-ecs-clusterクラスターにアプリケーションをデプロイします。


試行錯誤したところ

docker-compose を利用したビルド

CircleCI導入当時、.circleci/configは直にrubyのイメージをプルして直に環境構築することを想定していましたが、開発環境はもともとDockerで構築していたため、CircleCIもdocker-composeを利用して構築することにしました。
管理するコードを減らし、ローカルとCircleCI環境の環境差がrspecテストに影響しないようにできました。

docker:
      - image: cimg/ruby:3.1.2-browsers
    steps:
      - checkout

# ↓各jobのイメージはdocker-composeをインストール済みの仮想マシンを使用するよう変更

docker:
      - image: cimg/python:3.11.5
    steps:
      - checkout
      # -setup_remote_dockerでリモート Docker環境をアクティブ化。これでdockerコマンドが使えるようになる
      - setup_remote_docker


RedisのECRイメージだけ別でpushした

ECRのイメージは本当はapp-web1つにまとめられると良かったのですが、docker-compose.ymlを利用したビルドではRedisはRedis単独のイメージとしてpushされておりapp-webイメージ、redisイメージの2つができていました。それに気づかずデプロイを進め、Redisが参照できないとエラーが出ていました。

生成されたredisイメージを使うことも考えましたが、プロジェクトで使用しているRedisは現在バージョン7で固定しており、毎回のイメージビルドは不要です。
そこで、最初に別個ECRへredis-7イメージをpushしてそれを使用するようにしました。
docker-compose.ecs.ymlでredisの部分をコメントアウトしているのはそのためです。Redisのバージョンアップがある際にはまたpushする予定です。


rspec jobのDB接続がうまくいかない

DBセットアップ前にmigrateしようとしてエラーが発生したため、migrateとseedはrspecコマンド実行時にspec/rails_helper.rb内で行われるようにしました。

エラーメッセージ

rails aborted!
ActiveRecord::NoDatabaseError: We could not find your database: app_test. Which can be found in the database configuration file located at config/database.yml.

spec/rails_helper.rb

# 一部抜粋
RSpec.configure do |config|
  config.before(:suite) do
    Rails.application.load_tasks
    # migrateにはridgepole gemを使用
    Rake.application['ridgepole:apply'].invoke
    Rails.application.load_seed
  end
end


rspec jobのsubdomain付きURLへのアクセスでNet::ReadTimeoutエラー

ローカルでのテストはパスしていたのですが、CircleCI上ではsubdomain付きURLへのアクセス箇所で失敗していました。
プロジェクトではRSpecのテストでWEBブラウザを操作するためにSeleniumを使用しています。
調べてみると、Webdriverが内部的にいろいろなドライバーと通信するのにHTTPを使用していて、その通信にはRubyの標準ライブラリであるNet::HTTPが使われているようです。このNet::HTTPのデフォルトタイムアウトが60秒になっていてそこでひっかかっているようでした。
Selenium ドキュメント

Failures:

1) 管理画面にログインする
Failure/Error: visit login_path

Net::ReadTimeout:
Net::ReadTimeout with #

Seleniumのドキュメントの通りread_timeoutを設定するとうまく通りました。

spec/support/capybara.rb

Capybara.register_driver :remote_chrome do |app|
  # 一部抜粋
  client = Selenium::WebDriver::Remote::Http::Default.new
  client.read_timeout = 240
end


今後の展開

CI/CDを導入してテストからデプロイまで一気通貫で行えるようになりました。今後の展開として以下に上げる点を改善していきたいです。


latestタグ運用の廃止

deploy jobのDeploy、Migrationでlatestタグのついたイメージを使用しているのは、コミットハッシュのタグを使う運用に変更したいです。
具体的には、ECSのタスク定義でlatestタグのついたイメージを使用するようにしていますが、毎回コミットハッシュのタグ付きのイメージを使用するようタスク定義を書き換えるようにします。
そうすることで、障害発生時の切り戻しが迅速に行えるようになるのでベストプラクティスのようです。


デプロイの前に手動承認のひと手間を加える

CircleCIには、[Approval (承認)] ボタンがクリックされるまでジョブの実行を待つ手動承認の機能があります。
現在のconfigファイルではcommitしたらすぐにテストからデプロイまで走ってしまいますが、手動承認を実装してデプロイは実行者の承認のもと行うようにしたいです。


マイグレーション前のDBバックアップ

プロジェクトではDBにRDSを使用しています。マイグレーションの前にスナップショットを撮るようにして、バックアップするようにしたいです。


Net::ReadTimeoutエラーの解消方法

今回はread_timeout = 240として解消しましたが、240秒はちょっと長すぎるので、根本原因を探り改善したいです。



Image by Freepik

関連記事

「コスパならBigQuery」?実験で比較してみた【Redshift vs. BigQuery】

.blue-link, .blue-link:visited { color: blue; } 目次 「費用対効果」で選ぶには? 結果:BigQueryは「安定」している 実験条件 データセット データウェアハウス設定・計測条件 クエリ コード集 前処理 データロード等 計測記録の取得(BigQuery) リザルトキャッシュの無効化(Redshift Serverless) クエリ1-1:小規模構造化データ、単純集計 ク

記事詳細
「コスパならBigQuery」?実験で比較してみた【Redshift vs. BigQuery】
AWS コラム データ分析
【Redshift vs. BigQuery】「運用性でRedshift」という選び方

.blue-link, .blue-link:visited { color: blue; } 目次 結局、何が違うのか? Redshiftの運用性 1. AWS上の対向システムとの統合容易性 2. 機械学習ベースの管理タスク自動化 3. Glue/Lake Formation による一元的なメタデータ・アクセス権限管理 まとめと次回予告 出典(いずれも記事公開時点閲覧) 結局、何が違うのか? こんにちは、アクセルユニバ

記事詳細
【Redshift vs. BigQuery】「運用性でRedshift」という選び方
AWS コラム データ分析
オンプレミスからAWSへの移行ステップと活用サービスの紹介

はじめに AWS移行のためのステップ 活用できるAWSサービス AWSとAzureの比較 まとめ 1.はじめに オンプレミス環境からクラウドに移行することは、企業にとって大きな変革の一歩です。AWS (Amazon Web Services) への移行は、コスト削減、運用効率の向上、スケーラビリティを提供するだけでなく、最新のクラウドテクノロジーを活用してビジネスを加速させる大きな機会となります。しかし、移行プロセスは複雑であり、慎重

記事詳細
オンプレミスからAWSへの移行ステップと活用サービスの紹介
AWS
VMwareからAWSへの移行の手順と選択肢の紹介

VMwareからAWSへの移行について 導入 移行のステップ 移行戦略のパターン 移行後のアーキテクチャ 移行の成功事例 まとめ 本記事では、VMware環境からAWSへの移行手順・移行の進め方・構成決定方法・成功事例について紹介します。 1. 導入 近年、クラウド移行を検討する企業が増える中で、VMware環境からAWS(Amazon Web Services)への移行が特に注目されています。特に、2

記事詳細
VMwareからAWSへの移行の手順と選択肢の紹介
AWS

お問い合わせはこちらから