AWS BatchをCloudformationで作ってみた

## AWS Batchとは
AWS re:Invent 2016 で発表されたサービスで、2017/06より東京リージョンがリリースされました。

どんなサービスなのか簡潔に言うと、
<font color="navy"><b>ECSを利用して、処理が実行している間だけ自動でEC2インスタンスを起動し、終わったら自動でterminateしてくれるサービス</b></font>です。

今回、試しに簡単な処理を実行するものを作ってみたので、その手順を書きたいと思います。

## AWS Lambdaとの違い
処理を実行する時だけ起動するという点では、<font color="navy"><b>よく似ているサービスにAWS Lambda</b></font>があります。

関連: [Serverless FrameworkでAWS Lamda関数を作成する](https://qiita.com/Esfahan/items/736d09f732fa619d2410)

しかし、似ているようで、全く別のサービスとなっています。
以下が、Lambdaとの相違点です。

- Lambdaは最長５分しか実行できないが、<font color="navy"><b>AWS Batchは実行時間に上限はない</b></font>
- LambdaはFaaSなので開発言語が限定されているが、<font color="navy"><b>Batchは環境構築を自分で行う</b></font>ので自由
- Batch単体で定期実行はできないので、別途定期的にキックするトリガーを用意する必要がある

まず、サーバーの環境構築は自分で行う必要があります。
具体的には、あらかじめDockerイメージを作成しておいて、
処理起動時に、AWS Batchが指定された<font color="navy"><b>Dockerレポジトリからイメージを取得してコンテナを立ち上げる</b></font>という流れになります。

自分で環境構築するため、<font color="navy"><b>Lambdaのように開発言語を限定されない</b></font>ので、自由度が高いです。

また、最長実行時間に上限がないため、時間のかかる処理に向いています。

## AWS Batchのオートスケーリング
<font color="navy"><b>オートスケーリングは、AWSがよしなにやってくれます。</b></font>
自分で制御することも可能です。
その他、以下のような特徴があります。

- **クラスタ数の上限を自分で設定することも可能**
    - インスタンス数ではなくvCPU数で指定する
- **使用されるEC2インスタンスタイプを指定することもAWSにおまかせすることもできる**
    - t2系などは選択できない
    - m4 familyのような柔軟な設定も可能（その中からAWSが最適なものを選ぶ）
    - optimal(おまかせ)を選択すると最新の C、M、および R インスタンスファミリーから最適なものが選択される
- **立ち上がったインスタンスは、実際にECSやEC2の画面で確認できる**


## 構成図
今回構築した環境の構成図です。

![qiita-aws-batch.png](https://qiita-image-store.s3.amazonaws.com/0/105936/5e9fba51-1516-83c5-e9c3-6271b7fce6e1.png)

今回は上図の構成で構築してみました。
処理のフローは以下になります。

1. **jobをsubmitし、Batchのqueueに登録(FIFO)**
    - スケジューラがqueueのjobの優先順位を把握する
1. **ECSがEC2インスタンスが起動させる**
    - 既に起動している場合は、起動しているインスタンスが使用される
    - 既にEC2が起動している場合は、ほとんどタイムラグなくjobが実行されるが、起動していない場合は、job実行開始が、インスタンスが起動する時間分タイムラグとなる
    - 立ち上がったインスタンスは、ECSやEC2の画面で確認できる
1. **AWS ECRに登録されているコンテナイメージを使って、EC2内にコンテナを立ち上げる**
1. **コンテナ内で処理を実行する**
1. **処理が終わったら、EC2インスタンスを削除する**
1. **後続のjobがある場合は、削除されずにそのまま使用される**


### トリガーについて
まず、<font color="navy"><b>AWS Batchは単体で定期実行することができない</b></font>ので、別途トリガーを用意する必要があります。
方法は色々ありますが、AWSのサービスを利用するのであれば、トリガーを投げるLambdaをCloudwatchで定期実行させるのが簡単かと思います。

### job
AWS Batchは、<font color="navy"><b>キックされるとjobにキューを溜めます。</b></font>
基本的にFIFO（First In, First Out）ですが、他のjobとの依存関係や優先度を指定することも可能です。

### ECSで処理実行
jobが実行されると、ECSが、<font color="navy"><b>EC2クラスタインスタンスを立ち上げます。</b></font>

次に、ECSがDockerレポジトリ（今回はECR）からDockerイメージを取得して、
<font color="navy"><b>EC2クラスタインスタンス上にDockerコンテナを立ち上げます。</b></font>

このコンテナ上で、処理が実行されます。
<font color="navy"><b>処理の重さに応じてオートスケーリングが行われます。</b></font>

図の例では、データソースから取得したデータ加工して、HDFSに書き出すという処理が行われています。

処理が終了したら、<font color="navy"><b>EC2クラスタインスタンスが削除されます。</b></font>


## 環境構築手順
### AWS ECRにDockerイメージの登録
まずは、以下の記事を参考に、Dockerイメージを作成して、AWS ECRに登録してください。
[AWS ECRにdockerイメージを登録する](https://qiita.com/Esfahan/items/2ddc8d481afd012da357)

### AWS Batchの環境構築
では、AWS Batch環境構築をします。

Cloud Formationを利用して環境構築をするので、
まず、awscliなどをインストールした、Cloud Formationを実行できる環境を用意してください。

Cloud Formationの環境構築手順については、以下を参考にpythonをインストールし、pipでawscliをインストールするか、Amazon Linuxを使ってください。

- [python(pyenv + pyenv-virtualenv) + CentOS7インストールメモ](https://qiita.com/Esfahan/items/0dfe70357549f92b23da)

## Cloud Formation実行
### yamlを作成
以下のyamlを作成します。
適宜、ご自分の環境にあった設定に読み替えてください。

```yaml:aws-batch.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Build AWS Batch environment

Parameters:
  SubnetIds:
    Description: Subnets For ComputeEnvironment
    Type: List<AWS::EC2::Subnet::Id>
    Default: subnet-xxxxxxxx,subnet-yyyyyyyy
  SecurityGroupIds:
    Description: SecurityGroups For ComputeEnvironment
    Type: List<AWS::EC2::SecurityGroup::Id>
    Default: sg-zzzzzzzz
  ContainerImage:
    Description: Uri of container image to use in ECS
    Type: String
    Default: xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/sample-aws-batch-repo
  # あらかじめ、key pairを作成しておいてください
  KeyPair:
    Description: key pair
    Type: String
    Default: sample_key

Resources:
  ####### IAM #######
  # ECSインスタンスを実行するrole
  ecsInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - ec2.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
        - arn:aws:iam::aws:policy/AmazonEC2FullAccess
        - arn:aws:iam::aws:policy/AmazonECS_FullAccess
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
      Path: "/"
  # Set InstanceProfile
  ecsInstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Roles:
        - !Ref ecsInstanceRole

  # AWS Batchを実行するrole
  AWSBatchServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - batch.amazonaws.com
            - ecs-tasks.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole
      Path: "/service-role/"

  ####### ComputeEnvironment #######
  SampleComputeEnv:
    Type: "AWS::Batch::ComputeEnvironment"
    Properties:
      Type: MANAGED
      # 作成したIAMから取得
      ServiceRole: !GetAtt AWSBatchServiceRole.Arn
      ComputeEnvironmentName: sample-batch-compute-env
      ComputeResources:
        Ec2KeyPair: !Ref KeyPair
        # 最大vcpu数、最大インスタンス数となります
        MaxvCpus: 256
        # 最小vcpu 0にすると自動でインスタンスをterminateしてくれる
        MinvCpus: 0
        # 起動するインスタンスの希望するvCPUの数
        DesiredvCpus: 1
        SecurityGroupIds: !Ref SecurityGroupIds
        Type: EC2
        Subnets: !Ref SubnetIds
        # 作成したVPCから取得
        InstanceRole: !GetAtt ecsInstanceProfile.Arn
        InstanceTypes:
          # 使用するインスタンスタイプ、optimalならおまかせ
          - optimal
        Tags: {"Name": "Batch Instance - sample"}
      State: ENABLED

  SampleJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - Order: 1
          ComputeEnvironment: !Ref SampleComputeEnv
      State: ENABLED
      # jobの優先順位
      Priority: 1
      JobQueueName: sample-batch-queue

  SampleJobDefinition:
    Type: AWS::Batch::JobDefinition
    Properties:
      Type: container
      JobDefinitionName: sample-batch-definition
      ContainerProperties:
        Command:
          - sh
          - /usr/local/init.sh
        # memoryの上限
        Memory: 4048
        Vcpus: 2
        Image: Image: !Ref ContainerImage
      RetryStrategy:
        Attempts: 1
```


### syntax check
yamlのsyntax checkをします。

```
$ aws cloudformation validate-template --template-body file://aws-batch.yml
```

### 実行
実行して、環境を作成します。

```
$ aws cloudformation create-stack \
      --stack-name sample-batch \
      --template-body file://aws-batch.yml \
      --capabilities CAPABILITY_IAM \
```

### 確認
作成されたことを確認します。

```
$ aws cloudformation describe-stacks --stack-name sample-batch
```

`"StackStatus": "CREATE_COMPLETE",` になっていることを確認してください。

### 削除
削除する場合は以下のコマンドを実行してください。

```
$ aws cloudformation delete-stack --stack-name sample-batch
```

## jobを実行する
今回は、awscliを手動で実行してトリガーとします。

まず、以下のコマンドで、jobの設定ファイルとなるjsonのテンプレートを生成します。

```
$ aws batch submit-job --generate-cli-skeleton > submit-job.json
```

生成されたjsonファイルを、以下のように編集します。
パラメータや、環境変数を設定することができます。

```json:submit.json
{
    "jobName": "sample-batch-job",
    "jobQueue": "sample-batch-queue",
    "jobDefinition": "sample-batch-definition",
    "parameters": {
        "KeyName": "string"
    },
    "containerOverrides": {
        "environment": [
            {
                "name": "access_token",
                "value": "abcdef1234"
            }
        ]
    }
}
```

| 項目 | 設定値 |
| --- | --- |
| jobName | job名 |
| jobQueue | 作成したJobQueueを指定 |
| jobDefinition | 作成したJobDefinitionを指定 |
| parameters | パラメータ |
| environment | 環境変数 |


以下のコマンドでjobを実行します。

```
$ aws batch submit-job --cli-input-json file://submit-job.json
```

response

```json
{
    "jobId": "a94985d8-52d6-4ea0-95ac-2ccb0dcabc25",
    "jobName": "sample-batch-job"
}
```

## job実行
jobが実行されると、以下の順でjobが移動していきます。
`submitted` -> `runnable` -> `starting` -> `running` -> `succeeded`

<img width="988" alt="Screen Shot 2017-12-03 at 17.50.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/105936/79ac951c-5659-49a4-d0aa-8ae1efce0444.png">


また、runnableのタブに入ったまま、runningへ移動しない場合は、ECSの起動周りで問題が起きている可能性が高いので、EC2インスタンスにsshでログインして、原因を調査してください。

## EC2インスタンス確認
EC2インスタンス一覧の画面で、

`Batch Instance - sample`という名前でインスタンスが起動していることが確認できます。

<img width="1004" alt="Screen Shot 2017-12-03 at 16.28.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/105936/45d9c9b2-8b48-d584-a406-550c09e2b9a7.png">


## 実行結果Statusを確認
AWS BatchのJob画面で、実行結果のStatusを確認できます。
以下のように、succeededのタブにjobが移動していれば成功です。
3つ結果がありますが、今回の結果は一番下の選択されているものです。

<img width="992" alt="Screen Shot 2017-12-03 at 18.20.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/105936/663b0563-8da0-f7d2-9e1a-eb91cb4e13bd.png">


もし、failedのタブに入れば、失敗しているので、failedタブのJob IDをクリックして、エラーの原因を調査してください。


## Cloudwatchで出力結果の確認
出力結果がある場合は、Cloudwatch の `/aws/batch/job` ロググループで確認することができます。

<img width="1001" alt="Screen Shot 2017-12-03 at 18.16.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/105936/9d4e56e0-1340-d5f3-07fe-fac59d2a06d5.png">


## Batch完了後
自動でインスタンスがterminateされます。

<img width="1004" alt="Screen Shot 2017-12-03 at 16.44.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/105936/5c15f2b1-3c60-929c-a5ff-53ab7f9015fc.png">

## まとめ
一通り触ってみて、バッチ処理を実行することはできました。
jobが登録されてからEC2インスタンスが立ち上がるので、リアルタイムで実行する処理には向いていませんが、非同期で時間のかかる処理を実行するのには向いてそうです。

別途、オートスケーリングの検証などもしてみたいと思います。

以上

## 参考
- [AWS Black Belt Online Seminar 2017 AWS Batch](https://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-online-seminar-2017-aws-batch)
- [AWS Batchを使って5分以上かかる処理を実行してみる](https://dev.classmethod.jp/cloud/aws-batch-5min-over/)
- [AWS Batch Preview Getting Started](https://qiita.com/shibataka000/items/5d1b4f2161d8cb4c60a9)
- [AWS Batchのプレビュー版を触ってみる #reinvent](https://dev.classmethod.jp/cloud/aws/aws-batch-getting-started/)
- [AWS BatchのジョブがRUNNABLEのまま進まない](https://qiita.com/sekitaka_1214/items/dbfc16d26168efc39900)
- [Cloudformationを使ってAWS Batch環境を構築してみる](https://dev.classmethod.jp/cloud/cloudformation-aws-batch/)
- [【AWS Batch】１コマンドでdockerを定期実行する](https://qiita.com/Yarimizu14/items/8516287b0a9fac90f318)
- [初めてのAWS Batch – 必要な各コンポーネントを理解する –](https://dev.classmethod.jp/cloud/aws-batch-component/)