Fargateでバッチサーバを作る

## Fargateについて

FargateはAWSのサービスで、Dockerコンテナを実行する基盤の一つです。
以下、Fargateの利点とECSとの違いを説明しています。

> https://aws.amazon.com/jp/fargate/
> クラスター管理が不要

まず、ECSとの一番の違いはクラスターを事前に用意しなくていいところです。
ECSではEC2インスタンスを用意して、その上でTaskやServiceを設定していました。
FargateではTaskやServiceを作るだけでコンテナを動かすことができます。

> https://aws.amazon.com/jp/about-aws/whats-new/2018/08/aws-fargate-now-supports-time-and-event-based-task-scheduling/
> AWS Fargate が時間とイベントベースのタスクスケジューリングをサポート

> https://aws.amazon.com/jp/fargate/pricing/
> AWS Fargate の料金は、コンテナイメージのダウンロード (docker pull) を開始した時点から Amazon ECS タスク* が終了するまでに使用された vCPU およびメモリリソースに基づいて計算され、最も近い秒数に切り上げられます。1 分の最低料金が適用されます。

さらに最近、時間やイベントをトリガーに動かすことができるようになりました。
これを使えば、バッチ実行時のみ課金されるので利用料を節約できます。

例えばECSだと、バッチ利用時にEC2インスタンスを起動し、利用後に停止するという手順が必要でした。
しかし、FargateではEC2インスタンスを管理する必要はなく、Taskが動いている時間だけ課金されます。

気をつける点として、Fargateでは使用するvCPUによって使えるメモリ量が制限されます。
例えば、メモリを大量に使用する処理の場合、最大で30GBのメモリを使うには4vCPUを選択する必要があります。
費用対効果を考えてEC2やECSを使い分ける必要はあると思います。

## 実装

それでは実装に入って行きます。
今回はCloudFormationのみでFargateを作っていきます。

### ポイント

- バッチ名やバッチ実行に必要なコマンド、cronの設定は`Parameters`に記述しています。
- 今回実行する`バッチ（Dockerコンテナ）はCFnで環境を構築後にECRにPush`してください。
    - Dockerhubから直接取ってくる場合は、`ECR`の部分を削除後`ECSTaskDefinition`の`image`を書き換えてください。
- Fargateには`vCPUは1024、memoryは2048`を設定しています。
    -  コンテナにはMemoryReservationで1024を設定しています。
- バッチを実行するだけなので`AssignPublicIpはDISABLED`にしています。
- `セキュリティグループは事前に作成`し、SecurityGroupsに設定します。
    - CFnで作っている場合は、importValueなどを使って設定してください。
- `Privateサブネットも事前に作成`し、Subnetsに設定します。
    - `AssignPublicIpはDISABLED`なので、サブネットもPrivateなものを使います。
- `DesiredCountは0`に設定しています。
    - 実はここが味噌で、サービスは作るけど普段は動かさないので、0に設定します。
- `TaskCountは1`に設定しています。
    - サービスは動かしませんが、定期実行用のTaskは動かしたいので、1に設定します。

```Yaml:fargate.yml

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'fargate scheduled task for batch execution'
Parameters:
  BatchName:
    Type: String
    Description: A name of batch
    Default: 'fargate-batch'
  EntryPoint:
    Type: String
    Description: A command execute at start
    Default: '/bin/sh'
  ScheduleExpression:
    Type: String
    Description: Cron settings
    Default: 'cron(0 1 * * ? *)'
Resources:
  ECR:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Sub "${BatchName}"

  CloudwatchLogsGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "${BatchName}/app"

  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub "${BatchName}"

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
                - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
      Path: /
      RoleName: { "Fn::Join" : ["-", [ !Sub "${BatchName}-ecs-task", { "Ref" : "AWS::Region" }]] }

  ECSTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties: 
      Cpu: 1024
      Memory: 2048
      Family: !Sub "${BatchName}"
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole"
      TaskRoleArn: !Ref TaskRole
      NetworkMode: awsvpc
      ContainerDefinitions:
        - Name: !Sub "${BatchName}"
          Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${BatchName}"
          MemoryReservation: 1024
          EntryPoint:
            - !Sub "${EntryPoint}"
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-group: !Sub "${BatchName}/app"
              awslogs-region: !Sub "${AWS::Region}"
              awslogs-stream-prefix: !Sub "${BatchName}"

  ECSService:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref BatchName
      LaunchType: "FARGATE"
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          SecurityGroups:
            - <<SecurityGroups>>
          Subnets:
            - <<Subnets>>
      DesiredCount: 0
      TaskDefinition: !Ref ECSTaskDefinition

  TaskScheduleEvents:
    Type: AWS::Events::Rule
    Properties:
      Name: cron-event
      ScheduleExpression: !Ref ScheduleExpression
      State: ENABLED
      Targets:
        - Id: 1
          RoleArn: !GetAtt TaskRole.Arn
          EcsParameters:
            TaskDefinitionArn: !Ref ECSTaskDefinition
            TaskCount: 1
          Arn: !GetAtt ECSCluster.Arn

```

## 所感

`Scheduled task`を使ってFargateを動かすと`docker compose run`と同じように、
１回きりの実行(one-off)になり、バッチには最適な実行環境だなと感じました。
EC2インスタンスを使用しないので、手間が省けていいです。

> http://docs.docker.jp/compose/reference/run.html

ただし、Fargateに限らずECSもそうなんですが、エラーの確認が大変です。
EC2インスタンスがあれば、sshしてログやリソース使用量を確認できるのですが、Fargateでは難しそうです。

簡単にお安くバッチ環境がほしいという方には最適かなと思います。