AWS FargateでRakeタスクを定期実行する

## 実現したいこと
- 従来はWheneverでcron定義を自動生成し、EC2上で定期バッチを実行させていた
- EC2からFargateへのインフラ移行に際して、定期バッチもFargateで実行できる形にしたい
- 定義の反映はアプリケーションのCI/CDに載せて自動化したい
（「手作業でECS Schesuled Tasksを編集する」のような形は避けたい）
- 定義は設定ファイルで構成管理したい
（「スケジュール追加スクリプトを実行する」ような形は避けたい）

## 選択肢

1. CloudFormation でEventBridgeルールの定義を行い、CircleCI 内で`aws cloudformation deploy` を実行する
2. [ecschedule](https://github.com/Songmu/ecschedule)（ECS Scheduled Task 設定を管理する gem）を使い、circleci 内でコマンドを実行する
3. [Elastic Whenever](https://github.com/wata727/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
```
これにより、
1. masterブランチへのプッシュ駆動で`build_and_deploy`が実行される
1. `aws-ecr/build_and_push_image`でコンテナイメージのビルドとECRへのデプロイが行われる
1. `aws-ecs/update_task_definition`でタスク定義にビルドしたイメージを指定する
1. `aws cloudformation deploy --template-file lib/tasks/schedule.yml --stack-name ecs-scheduled-tasks-sample-app`でCloudFormationの反映を実行する

という流れでEventBridgeルールを最新化が行われます。
CircleCIジョブの実行後、Amazon EventBridgeの[ルール管理画面](https://ap-northeast-1.console.aws.amazon.com/events/home?region=ap-northeast-1#/rules)で`sample-app-0001`が作成されていればOKです。