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

関連記事

AWSにてクラウド財務管理(CFM)を実践するための3つのミッション

はじめに AWSのコスト管理を実施するにあたり「Well-Architected Framework コスト最適化の柱」への考慮は欠かせません! その中でも「クラウド財務管理(CFM)」の実装は特に重要です! 今回は、そのクラウド財務管理(CFM)を実践するためにクリアすべきミッションを3つご紹介いたします。 目次 クラウド財務管理(CFM)とは? ミッション① : コストの可視化 ミッション② : コストの最適化 ミッション③ : コストの計画・予測の確

記事詳細
AWSにてクラウド財務管理(CFM)を実践するための3つのミッション
AWS
Amazon ECSから考える安全なアプリケーションデリバリー

はじめに AWSにてデプロイのリスクを緩和するにあたり「Well-Architected Framework 運用上の優秀性」への考慮は大変重要です。 今回は、AWS Summit Japan「Amazon ECSから考える安全なアプリケーションデリバリー」をベースに、デプロイのリスクを緩和する方法をご紹介いたします。 目次 安全なアプリケーションデリバリーとは いかに影響範囲を最小限にするか? 新機能を段階的に公開する手法 線形デプロイ/Canaryデプ

記事詳細
Amazon ECSから考える安全なアプリケーションデリバリー
AWS
【データ分析基盤】利活用を成功させる構築、2つの

データ分析基盤構築とは、大量のデータを蓄積・変換・分析するためのインフラを開発することです。主軸となるデータレイク・データウェアハウス・BIツールの他、NoSQLデータベース、データパイプラインツール、ETLツールなど様々な要素があり、それぞれ様々なベンダーから多種多様な製品が出ています。 比較項目は膨大で、複雑です。性能・機能・セキュリティ・コスト......一体何を基準に選べばよいのでしょうか。当社には一つの戦略があります。それは、 " 分析ツール

記事詳細
【データ分析基盤】利活用を成功させる構築、2つの"秘訣"
AWS コラム データ分析
生成AIで架空飲食チェーン店のVOC分析やってみた

ご挨拶 AWS全冠エンジニアの小澤です。 今年の目標はテニスで初中級の草トーナメントに優勝することです。よろしくお願いいたします。 本記事の目的 本記事では、生成AIでVOC分析を行うことで得られた知見を共有したいと思います。 昨今、生成AIの登場など機械学習の進歩は目覚ましいものがあります。一方、足元では自社データの利活用が進まず、世の中のトレンドと乖離していくことに課題感を持たれている方も多いかと思います。また、ガートナーの調査(2024年1月)による

記事詳細
生成AIで架空飲食チェーン店のVOC分析やってみた
AWS SRE 機械学習

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