実現したいこと
- 従来はWheneverでcron定義を自動生成し、EC2上で定期バッチを実行させていた
- EC2からFargateへのインフラ移行に際して、定期バッチもFargateで実行できる形にしたい
- 定義の反映はアプリケーションのCI/CDに載せて自動化したい
 (「手作業でECS Schesuled Tasksを編集する」のような形は避けたい)
- 定義は設定ファイルで構成管理したい
 (「スケジュール追加スクリプトを実行する」ような形は避けたい)
選択肢
- CloudFormation でEventBridgeルールの定義を行い、CircleCI 内でaws cloudformation deployを実行する
- ecschedule(ECS Scheduled Task 設定を管理する gem)を使い、circleci 内でコマンドを実行する
- 
Elastic Whenever(Whenever のように cron ジョブのタスク一覧をconfig/schedule.rbで管理できるようになる gem)を使い、circleci 内でコマンドを実行する
デファクトスタンダードな手段が存在しなかったため、できるだけライブラリには依存しない形にする、という意図で1のCloudFormationで管理する形としました。
設定
ECSタスク定義
まずバッチが起動するECSのタスク定義を作成します。今回はあらかじめ以下が存在する前提とします。
- クラスター
 sample-app
- コンテナイメージ(アプリケーションで使うコンテナイメージと同じもの)
 362548988451.dkr.ecr.ap-northeast-1.amazonaws.com/sample-app:rails-21f7b833babf545f86fea29239ffb34c6969177d
  EcsTaskDefinition0001:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: sample-app # 起動させるクラスター名
      Cpu: '512'
      Memory: '1024'
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: arn:aws:iam::362548988451:role/ecsTaskExecutionRole
      ContainerDefinitions:
        - Name: batch # コンテナ名
          Image: 362548988451.dkr.ecr.ap-northeast-1.amazonaws.com/sample-app:rails-21f7b833babf545f86fea29239ffb34c6969177d
          Essential: true
          Environment:
            - Name: RAILS_ENV
              Value: production
            - Name: RAILS_LOG_TO_STDOUT
              Value: true
          Command: ['bash'] # EventBridgeルールでOverrideするのでなんでもOK
          Secrets:
            - Name: DB_HOST
              ValueFrom: arn:aws:ssm:ap-northeast-1:362548988451:parameter/sample-app/production/DB_HOST
            - Name: DB_USER
              ValueFrom: arn:aws:ssm:ap-northeast-1:362548988451:parameter/sample-app/production/DB_USER
            - Name: DB_PASSWORD
              ValueFrom: arn:aws:ssm:ap-northeast-1:362548988451:parameter/sample-app/production/DB_PASSWORD
            - Name: DB_DATABASE
              ValueFrom: arn:aws:ssm:ap-northeast-1:362548988451:parameter/sample-app/production/DB_DATABASE
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: /ecs/sample-app-batch
              awslogs-region: ap-northeast-1
              awslogs-stream-prefix: ecs
          PortMappings:
            - ContainerPort: 8080
定義ファイルの作成が完了したら、CloudFormationを適用します。
EventsRule定義
Railsプロジェクトのlib/tasks/schedule.ymlに以下を作成します。
今回はsitemap:refresh:no_pingを定期実行する定義とします。
Resources:
  EventsRule0001:
    Type: AWS::Events::Rule
    Properties:
      Name: sample-app-0001 # ルール名
      Description: run rake sitemap:refresh:no_ping
      # 日本時間の毎日4時(JST)に実行する
      ScheduleExpression: cron(0 19 * * ? *)
      Targets:
        - Id: !Sub 'sample-app-0001'
          Arn: !Sub 'arn:aws:ecs:ap-northeast-1:362548988451:cluster/sample-app'
          RoleArn: arn:aws:iam::362548988451:role/ecsEventsRole
          Input: '{"containerOverrides":[{"name":"batch","command":["bundle","exec","rake","sitemap:refresh:no_ping"]}]}'
          EcsParameters:
            TaskDefinitionArn: !Sub 'arn:aws:ecs:ap-northeast-1:362548988451:task-definition/sample-app-batch'
            TaskCount: 1
            NetworkConfiguration:
              AwsVpcConfiguration:
                SecurityGroups:
                  - sg-xxx
                Subnets:
                  - subnet-xxx # private-1a
                  - subnet-xxx # private-1c
            LaunchType: FARGATE
            PlatformVersion: LATEST
注意点
- RoleはマネージドなRoleArn: arn:aws:iam::362548988451:role/ecsEventsRoleを使う
 (自前のルールだと起動しなくてハマった)
- JSTでの時間指定はできないのでUTCで指定する必要がある
- TaskDefinitionArnのバージョンを記載しないことにより、最新のタスク定義が実行される
CircleCI定義
.circleci/config.ymlで
version: 2.1
orbs:
  aws-ecs: circleci/aws-ecs@4.0.0
  aws-ecr: circleci/aws-ecr@9.0.3
としてaws-ecsのorbsを使える状態にしつつ、jobs内で以下と定義します
jobs:
  build_and_deploy:
    <<: *defaults
    machine:
      image: ubuntu-2204:2024.01.1
    environment:
      DOCKER_BUILDKIT: 1
    steps:
      - checkout
      - run:
          name: Create master.key
          command: |
            touch ./config/master.key
            echo $RAILS_MASTER_KEY > ./config/master.key
      - aws-cli/setup
      - aws-ecr/build_and_push_image:
          auth:
            - aws-cli/setup
          account_id: '${AWS_ACCOUNT_ID}'
          region: '${AWS_DEFAULT_REGION}'
          dockerfile: Dockerfile
          repo: sample-app
          tag: 'rails-${CIRCLE_SHA1}'
      - aws-ecs/update_task_definition:
          container_image_name_updates: 'container=batch,tag=rails-${CIRCLE_SHA1}'
          family: 'sample-app-batch'
      - run:
          name: Update ECS Scheduled Tasks
          command: aws cloudformation deploy --template-file lib/tasks/schedule.yml --stack-name ecs-scheduled-tasks-sample-app
workflows:
  version: 2
  build-test-and-deploy:
    jobs:
      - build_and_deploy:
          filters:
            branches:
              only:
                - master
これにより、
- masterブランチへのプッシュ駆動でbuild_and_deployが実行される
- 
aws-ecr/build_and_push_imageでコンテナイメージのビルドとECRへのデプロイが行われる
- 
aws-ecs/update_task_definitionでタスク定義にビルドしたイメージを指定する
- 
aws cloudformation deploy --template-file lib/tasks/schedule.yml --stack-name ecs-scheduled-tasks-sample-appでCloudFormationの反映を実行する
という流れでEventBridgeルールを最新化が行われます。
CircleCIジョブの実行後、Amazon EventBridgeのルール管理画面でsample-app-0001が作成されていればOKです。

