ECS Fargate を定期的に停止＆起動する仕組みを CloudFormation で作成する

# 概要
ECS のタスク数を AWS EventBridge Scheduler にて定期的に変更する仕組みを CloudFormation で構築します。

### 仕組み
AWS EventBridge Scheduler より直接 ECS へ API を発行し、ECS の DesiredCount を変更します。
![eventbridge-scheduler.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3346823/4cdec029-3ff0-e161-e56e-4ad31156ff65.png)

### 動作検証
ECS Fargate 環境でのみ動作確認しています。

# 手順
以下の CloudFormation を AWS Management Console か AWS CLI などから展開します。
展開後、AWS EventBridge Scheduler にスケジュールが作成されていることを確認します。

[パラメータ]
すべて必須パラメーターです。

- NamePrefix: 任意のプレフィックス
- EcsClusterName: ECS cluster 名
- EcsServiceName: ECS service 名
- ScheduleExpressionTimezone: タイムゾーン(Asia/Tokyoなど)
- StopScheduleExpression: 停止するスケジュールを cron式で記載します
- StartScheduleExpression: 起動するスケジュールを cron式で記載します
- StopDesiredCount: 停止時の ECS の 起動数を指定します（停止=0）
- StartDesiredCount: 開始時の ECS の起動数を指定します

``` yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Create EventBridge Scheduler for ECS task"

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Global setting"
        Parameters:
          - NamePrefix
      - Label:
          default: "ECS setting"
        Parameters:
          - EcsClusterName
          - EcsServiceName
      - Label:
          default: "Schedule setting"
        Parameters:
          - StopScheduleExpression
          - StartScheduleExpression
          - ScheduleExpressionTimezone
      - Label:
          default: "ECS DesiredCount setting"
        Parameters:
          - StopDesiredCount
          - StartDesiredCount

Parameters:
  # General Setting
  NamePrefix:
    Description: "Prefix"
    Type: String
    Default: ""

  # ECS Setting
  EcsClusterName:
    Description: "ECS Cluster Name"
    Type: String
    Default: ""

  EcsServiceName:
    Description: "ECS Service Name"
    Type: String
    Default: ""
  
  # Schedule Setting
  StopScheduleExpression:
    Description: "Schedule to stop Fargate task"
    Type: String
    Default: "cron(0 18 * * ? *)"
  
  StartScheduleExpression:
    Description: "Schedule to start Fargate task"
    Type: String
    Default: "cron(0 9 * * ? *)"

  ScheduleExpressionTimezone:
    Description: "Set Timezone. e.g. Asia/Tokyo. See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones"
    Type: String
    Default: "Asia/Tokyo"

  # ECS DesiredCount Setting
  StopDesiredCount:
    Description: "DesiredCount to stop Fargate task"
    Type: Number
    Default: 0
   
  StartDesiredCount:
    Description: "DesiredCount to start Fargate task"
    Type: Number
    Default: 1


Resources:
  ScheduledRuleStop:
    Type: AWS::Scheduler::Schedule
    Properties: 
      Description: "Schedule to stop Fargate task"
      Name: !Sub "${NamePrefix}-stop-ecs-task-schedule"
      ScheduleExpression: !Ref StopScheduleExpression
      ScheduleExpressionTimezone: !Ref ScheduleExpressionTimezone
      FlexibleTimeWindow: 
        Mode: "OFF"
      State: "ENABLED"
      Target: 
        Arn: "arn:aws:scheduler:::aws-sdk:ecs:updateService"
        Input: !Sub "{ \"DesiredCount\": ${StopDesiredCount}, \"Service\": \"${EcsServiceName}\", \"Cluster\": \"${EcsClusterName}\" }"
        RoleArn: !GetAtt ScheduleRole.Arn
        RetryPolicy: 
          MaximumRetryAttempts: 0

  ScheduledRuleStart:
    Type: AWS::Scheduler::Schedule
    Properties: 
      Description: "Schedule to start Fargate task"
      Name: !Sub "${NamePrefix}-start-ecs-task-schedule"
      ScheduleExpression: !Ref StartScheduleExpression
      ScheduleExpressionTimezone: !Ref ScheduleExpressionTimezone
      FlexibleTimeWindow: 
        Mode: "OFF"
      State: "ENABLED"
      Target: 
        Arn: "arn:aws:scheduler:::aws-sdk:ecs:updateService"
        Input: !Sub "{ \"DesiredCount\": ${StartDesiredCount}, \"Service\": \"${EcsServiceName}\", \"Cluster\": \"${EcsClusterName}\" }"
        RoleArn: !GetAtt ScheduleRole.Arn
        RetryPolicy: 
          MaximumRetryAttempts: 0

  ScheduleRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${NamePrefix}-ecs-task-schedule-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "scheduler.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Policies:
        - PolicyName: "ecs-task-schedule-policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: "arn:aws:logs:*:*:*"
              - Effect: "Allow"
                Action:
                  - "ecs:UpdateService"
                Resource: !Sub "arn:aws:ecs:${AWS::Region}:${AWS::AccountId}:service/${EcsClusterName}/${EcsServiceName}"

```

# Tips


### AWS EventBridge Scheduler で指定出来るターゲットとは？
以下のページに "templated targets" や "universal targets" などで紹介されています。
幅広いサービスで利用できるようです。
https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-targets.html
対応していないアクションとして "Unsupported actions" にも記載があるので注意する必要があります。

上記サービス一覧に記載の "arn:aws:scheduler:::aws-sdk:ecs:[apiAction]"などの "[apiAction]" の指定部分ですが、以下の JavaScriptSDK 向けの値を参考にしました。
https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-ecs/index.html

### Target の Input は Pascal Case で入力
例えば、AWSJavaScriptSDK で ECS の起動タスク数は "desiredConunt" を指定しますが、EventBridge Scheduler では "DesiredCount" と指定する必要がありました。

[AWS EventBridge Scheduler のターゲット設定の画面]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3346823/97d5ef27-23d8-c236-ad90-d56800d2543b.png)

# 参考
タイムゾーンの指定: 
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

EventBridge Scheduler で指定可能なサービス一覧: 
https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-targets.html

AWSJavaScriptSDK
https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest

GitHub
https://github.com/coni524/aws-ecs-fargate-schedule