CloudFormationを使ってAWS Batchを（ほぼ）全部AWS CLIで構築してみるハンズオン！

## 初めに

みなさんAWS Batchは活用していますか？

AWS Batchはキューに登録したジョブを非同期で処理してくれるAWSのサービスになります。

軽量タスクをサーバーレスで行いたい場合はLambdaなどが第一の選択肢になってきますが、**Lambdaは実行時間が15分**という制限があるため、大容量データをいくつも処理したい時にLambdaは利用できません。
そんな時に活躍するのがAWS Batchになります。

DockerイメージをAWSのECRというサービスにプッシュしておけば、そのコンテナがそのままAWS Batchで利用することができます。

イメージとしてはSQSとFargateが統合されたサービスで、AWS Batch内部でキューとコンピューティングリソースがどちらも含まれていますので他のサービスと連携することなく利用できます。

LambdaとAWS Batchの細かい比較についてはこちらが参考になりました。
[AWSでバッチ処理を実装する際の選択肢とサービス比較](https://zenn.dev/faycute/articles/fb310e3ccd783f)

弊社はこちらに代表されるような、5GBを超えるファイルの変換なども頻繁に行いますので、15分だとぎりぎりだったり、全然足りなかったりします。
[Leafmap/Open3Dを使って掛川城の大規模点群データ（5GB）をPythonで可視化してみよう！](https://qiita.com/nokonoko_1203/items/63e00d279cf7efb5521d)

そんなAWS Batchですが、**単一で利用できる**、と言いつつもVPCやセキュリティグループ、ロールの設定、あとはDockerイメージをECRにプッシュしておく必要があるなど、Lambdaよりは構築が手間ではあります。
（Lambdaもまともに活用としようとすると同じような設定が必要ですが…）

なので今回は（ほぼ）CloudFormationのみを利用して、さらにシェルスクリプトを順に実行していくだけでバッチが構築できるCloudFormationテンプレート集を作成しました！

## AWS Batchに関わる簡単な用語説明

- AWS Batch
    - **定義されたジョブをジョブキューに登録することで、ジョブキューに紐づいたコンピューティング環境でジョブ実行するサービス**
    - 内部的にはECRのコンテナをECSでEC2かFargateで動かすサービス
- ジョブ
    - 何らかの処理のこと
    - AWS Batchでは作業単位のことで、コンテナ化されたアプリケーションを呼び出すこと
    - ジョブ定義をすると、それをジョブとしてジョブキューに送信できる
- ジョブ定義
    - 使用するイメージ・CPUなど・コマンド・環境変数など、コンテナ実行時の各種パラメーターを設定する
- キュー
    - FIFO（先入れ先出し）を実現するデータ構造
    - つまり、キューに登録した順に処理される
    - ただ、SQSとかはデフォルトでランダム実行になっていそう（つまり厳密にはキューじゃない）
- ジョブキュー
    - キューでジョブを管理するもの
    - AWS Batchでは接続するコンピューティング環境だけ設定すれば良い
- コンピューティング環境
    - ECSの設定・インスタンスタイプ・インスタンスの台数などを設定

ということで、実際に構築していきましょう！

## .envを作成

まず`.env`ファイルを作成しましょう。
各種項目の意味は以下になります。

- `AWS_ACCOUNT_ID`: 利用するアカウントのID
- `AWS_PROFILE_ADMIN`: CloudFormationのサービスロールを作成するstack実行時に必要なIAMロール作成権限を持つユーザー
- `AWS_PROFILE`: 後述のCloudFormation実行の権限を持ったユーザー
- `AWS_DEFAULT_REGION`: 利用するリージョン
- `PJ_PREFIX`: 自由にプロジェクト名を設定（このprefixがほとんどのリソース名の先頭に利用される）
- `SEPARATOR`: 固定で「-」
- `CONTAINER_NAME`: コンテナ名
- `IMAGE_NAME`: イメージ名
- `〇〇_SUFFIX`: 各種スタック名のsuffix
- `CLOUDFORMATION_SERVICE_ROLE_ARN`: 後述のCloudFormationのサービスロールのarn

```
# AWS settings
AWS_ACCOUNT_ID=xxxxx
AWS_PROFILE_ADMIN=xxxxx
AWS_PROFILE=xxxxx
AWS_DEFAULT_REGION=ap-northeast-1
PJ_PREFIX=batch-sample
SEPARATOR=-
CONTAINER_NAME=container
IMAGE_NAME=image
REPOSITORY_SUFFIX=repository
NETWORK_SUFFIX=network
SECURITY_GROUP_SUFFIX=security-group
IAM_ROLE_SUFFIX=iam-role
BATCH_SUFFIX=aws-batch
CLOUDFORMATION_SERVICE_ROLE_ARN=xxxxx
```

## CloudFormationの実行ユーザーを作成する

次に、CloudFormationの実行ユーザーを作成しましょう。

これはAWS CLIからCloudFormationを実行するためだけのユーザーです。たくさんの権限を持ったユーザーを作成し、アクセスキーを発行するとセキュリティ的に問題がありますので、なるべく権限を絞ったユーザーを作成します。

`.env`の`AWS_PROFILE`に設定するCloudFormation実行用のユーザーを以下のような権限でAWSコンソールなどから作成し、`~/.aws/credentials`に設定しましょう。
（セキュリティの都合でユーザーの作成（アクセスキーの発行）が許可されない場合は、以下のyamlのような権限を持つサービスロールを作成してもらいましょう）

```yaml
Resources:
	CloudformationExecuteUser:
    Type: "AWS::IAM::User"
    Properties:
      Path: "/"
      UserName: "batch-sample-CloudformationExecuteUser"
      ManagedPolicyArns:
        - arn:aws:iam::616902636047:policy/cloudfromation-stack-operate-policy
        - arn:aws:iam::616902636047:policy/ecr-image-push-policy
        - arn:aws:iam::616902636047:policy/ecs-deploy-with-docker-compose-policy
```

- ~/.aws/credentials

```yaml
[batch-sample-CloudformationExecuteUser]
aws_access_key_id=xxxxx
aws_secret_access_key=xxxxx
```

その後、`.env`の`AWS_PROFILE`に記載します。

```
# AWS settings
...
AWS_PROFILE=batch-sample-CloudformationExecuteUser
...
```

## CloudFormationのサービスロールを作成

IAMユーザーの作成権限が必要なので、こちらもセキュリティの都合で権限が与えられない場合は`generate_cloudformation_service_role.yml`を参考にロールを作成してもらってください。

権限がある方はまずそのユーザーを`~/.aws/credentials`のプロファイルを編集した上で`.env`の`AWS_PROFILE_ADMIN`に追記しましょう

- ~/.aws/credentials

```
[batch-sample-IAMRoleCreateUser]
aws_access_key_id=xxxxx
aws_secret_access_key=xxxxx
```

- .env

```
# AWS settings
...
AWS_PROFILE_ADMIN=batch-sample-IAMRoleCreateUser
...
```

そのうえで、以下のyamlを使ってCloudFormationでサービスロールを作成します。

- generate_cloudformation_service_role.yml

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Make CloudFormation service role

Parameters:
  PJPrefix:
    Description: Input a project prefix. ex) my-project
    Type: String
    Default: "my-project"

Resources:
  # ------------------------------------------------------------#
  # IAM
  # ------------------------------------------------------------#
  CloudFormationServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PJPrefix}-CloudFormationServiceRole"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - cloudformation.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: "/service-role/"

  CloudFormationServiceRolePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub "${PJPrefix}-CloudFormationServiceRolePolicy"
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - "batch:DescribeJobQueues"
              - "batch:CreateComputeEnvironment"
              - "batch:DeleteComputeEnvironment"
              - "batch:SubmitJob"
              - "batch:UpdateComputeEnvironment"
              - "batch:DescribeComputeEnvironments"
              - "batch:CreateJobQueue"
              - "batch:RegisterJobDefinition"
              - "batch:DescribeJobDefinitions"
              - "batch:DeleteJobQueue"
              - "batch:UpdateJobQueue"
              - "batch:DeregisterJobDefinition"
              - "logs:CreateLogGroup"
              - "logs:PutLogEvents"
              - "logs:CreateLogStream"
            Resource: "*"
          - Effect: Allow
            Action:
              - "iam:GetRole"
              - "iam:PassRole"
              - "iam:DetachRolePolicy"
              - "iam:UntagRole"
              - "iam:ListRoleTags"
              - "iam:DeleteRolePolicy"
              - "iam:TagRole"
              - "iam:CreateRole"
              - "iam:DeleteRole"
              - "iam:AttachRolePolicy"
              - "iam:PutRolePolicy"
              - "route53:AssociateVPCWithHostedZone"
            Resource:
              - "arn:aws:iam::616902636047:role/*"
              - "arn:aws:route53:::hostedzone/*"
              - "arn:aws:ec2:ap-northeast-1:616902636047:vpc/*"
          - Effect: Allow
            Action:
              - "rds:*"
              - "ec2:*"
            Resource: "*"
          - Effect: Allow
            Action:
              - 'ecr:CreateRepository'
              - 'ecr:BatchGetImage'
              - 'ecr:CompleteLayerUpload'
              - 'ecr:GetAuthorizationToken'
              - 'ecr:UploadLayerPart'
              - 'ecr:InitiateLayerUpload'
              - 'ecr:BatchCheckLayerAvailability'
              - 'ecr:PutImage'
            Resource: '*'
          - Effect: Allow
            Action:
              - 'ecr:CreateRepository'
              - 'ecr:DeleteRepository'
            Resource: '*'
      Roles:
        - !Ref CloudFormationServiceRole

Outputs:
  CloudFormationServiceRole:
    Value: !GetAtt CloudFormationServiceRole.Arn
    Export:
      Name: !Sub "${PJPrefix}-CloudFormationServiceRole"
```

- generate_cloudformation_service_role.json

```json
[
  {
    "ParameterKey": "PJPrefix",
    "ParameterValue": "batch-sample"
  }
]
```

- generate_cloudformation_service_role.sh

```bash
source .env

export AWS_PROFILE="$AWS_PROFILE_ADMIN"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"

export STACK_NAME="$PJ_PREFIX""$SEPARATOR""$GENERATE_CLOUDFORMATION_SERVICE_ROLE_SUFFIX"

aws cloudformation create-stack \
  --stack-name "$STACK_NAME" \
  --template-body file://generate_cloudformation_service_role.yml \
  --parameters file://generate_cloudformation_service_role.json \
  --capabilities CAPABILITY_NAMED_IAM
```

各種ファイルを作成したらシェルスクリプトを実行しましょう！

```bash
bash generate_cloudformation_service_role.sh
```

作成後にAWSコンソールなどで作成したサービスロールのarnを確認して`.env`の`CLOUDFORMATION_SERVICE_ROLE_ARN`追記します。

```
# AWS settings
...
CLOUDFORMATION_SERVICE_ROLE_ARN=arn:aws:iam::xxxxxxxxxxxx:role/service-role/batch-sample-CloudFormationServiceRole
```

## AWS Batchで動作させるためのサンプルのdocker imageを作成

AWS BatchはDockerイメージを利用しますので、適当なイメージを作成します。

- dockerfile

```docker
FROM amazonlinux:2

SHELL ["/bin/bash", "-c"]

ARG PROJECT_DIR
WORKDIR $PROJECT_DIR

CMD ["/bin/bash"]
```

- docker-compose.yml

```yaml
version: '3.5'

services:
  batch:
    container_name: ${PJ_PREFIX}${SEPARATOR}${CONTAINER_NAME}
    image: ${PJ_PREFIX}${SEPARATOR}${IMAGE_NAME}
    build:
      context: ./
      dockerfile: Dockerfile
      args:
        - PROJECT_DIR=/var/app
    env_file:
      .env
    working_dir: /var/app
    tty: true
```

### dockerでコンテナを立ち上げる

作成したDockerfileからイメージをビルドしていきましょう。
docker-compose.ymlに変数を利用しているので、—env-fileの指定が必要になります。

```bash
docker compose --env-file .env build
```

イメージを確認しましょう。
作成されていますね！

```bash
% docker image ls | grep batch
batch-sample-image          latest           xxxxxxxxxxxx   31 seconds ago   164MB
```

コンテナでコマンドを実行できることを確認しておきましょう。

```bash
% docker container run batch-sample-image pwd   
/var/app
```

### docker composeでコンテナを立ち上げる

docker compose起動用のスクリプトを作成して、毎回同じコマンドで起動できるようにしておきましょう。

- docker_up.sh

```bash
docker compose down -v

source .env

docker compose --env-file .env up -d --build
```

以下のコマンドで起動できるようになります。

```bash
bash docker_up.sh
```

同じようにコンテナでコマンドを実行してみましょう！
コマンドが正常に実行されることが確認できると思います。

```bash
% docker compose exec batch pwd                 
/var/app
```

## ECRのリポジトリをCloudFormationで作成

ECRはDockerイメージをAWS上にホストしておけるサービスで、FargateやECS、AWS Batchで必須のサービスになります。
こちらにイメージ格納用のリポジトリを作成していきましょう。

以下のファイル群を作成していきます。

- generate_ecr_repository.yml

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Make ECR repository

Parameters:
  PJPrefix:
    Description: Input a project prefix. ex) my-project
    Type: String
    Default: "my-project"

Resources:
  MyECRRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Sub "${PJPrefix}-image"

Outputs:
  MyECRRepository:
    Value: !Ref MyECRRepository
    Export:
      Name: !Sub "${PJPrefix}-image"
```

- generate_ecr_repository.json

```json
[
  {
    "ParameterKey": "PJPrefix",
    "ParameterValue": "batch-sample"
  }
]
```

- generate_ecr_repository.sh

```bash
source .env

export AWS_PROFILE="$AWS_PROFILE"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"

export STACK_NAME="$PJ_PREFIX""$SEPARATOR""$REPOSITORY_SUFFIX"

aws cloudformation create-stack \
  --stack-name "$STACK_NAME" \
  --template-body file://generate_ecr_repository.yml \
  --parameters file://generate_ecr_repository.json \
  --capabilities CAPABILITY_NAMED_IAM \
  --role-arn "$CLOUDFORMATION_SERVICE_ROLE_ARN"
```

シェルスクリプトを実行します。

```bash
bash generate_ecr_repository.sh
```

リポジトリが作成されたか確認します。
コンソールで確認することもできますが、今回はAWS CLIで確認してみます。
ECRのリポジトリ操作権限を持つアクセスキーがない場合は、コンソールか管理者に確認してもらいましょう。
コマンド中の`--profile admin_user`は操作権限を持つユーザーに各自置き換えてください。

```bash
% aws --profile admin_user --region ap-northeast-1 ecr describe-repositories --output json | jq -re ".repositories[].repositoryName" 
batch-sample-image
```

## ECRへイメージをプッシュ

次にECRへ作成したイメージをプッシュするためのスクリプトを書きましょう。

- ecr_image_push.sh

```bash
docker compose down -v

source .env

export AWS_PROFILE="$AWS_PROFILE"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"

export REPOSITORY_NAME="$PJ_PREFIX""$SEPARATOR""$IMAGE_NAME"

aws ecr get-login-password | docker login --username AWS --password-stdin "$AWS_ACCOUNT_ID".dkr.ecr.ap-northeast-1.amazonaws.com

docker compose --env-file .env build

docker tag "$REPOSITORY_NAME":latest "$AWS_ACCOUNT_ID".dkr.ecr.ap-northeast-1.amazonaws.com/"$REPOSITORY_NAME":latest
docker push "$AWS_ACCOUNT_ID".dkr.ecr.ap-northeast-1.amazonaws.com/"$REPOSITORY_NAME":latest
```

スクリプトを実行してイメージをプッシュします。

```bash
bash ecr_image_push.sh
```

### ECRのイメージをpullできるか確認

スクリプトを用意します。

- ecr_image_pull.sh

```bash
docker compose down -v

source .env

export AWS_PROFILE="$AWS_PROFILE"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"

export REPOSITORY_NAME="$PJ_PREFIX""$SEPARATOR""$IMAGE_NAME"

aws ecr get-login-password | docker login --username AWS --password-stdin "$AWS_ACCOUNT_ID".dkr.ecr.ap-northeast-1.amazonaws.com

docker pull "$AWS_ACCOUNT_ID".dkr.ecr."$AWS_DEFAULT_REGION".amazonaws.com/"$REPOSITORY_NAME":latest
```

実行します。

```bash
bash ecr_image_pull.sh
```

正常にpullできているか確認しましょう。

```bash
% docker image ls | grep amazonaws.com
xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/batch-sample-image   latest           xxxxxxxxxxxx   6 days ago     164MB
```

pullしたイメージを利用してコンテナを立ち上げ、コマンドを実行してみましょう

```bash
% docker container run --rm xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/batch-sample-image ls -al
total 8
drwxr-xr-x 2 root root 4096 Jun 14 06:49 .
drwxr-xr-x 1 root root 4096 Jun 14 06:49 ..
```

## ネットワークの作成

AWS Batchに必要なリソースを作成していきましょう。
ここからはとにかくCloudFormation用のテンプレートファイル（yaml）を作成、それに与えるパラメーター（json）を作成、実行のためのシェルスクリプトを作成、そして実行！という流れになります。
CloudFormationのスタックの作成自体はCLIで行なっていきますが、エラーが発生するかもしれないので、コンソールを見れる方は見ながら作業するのをお勧めします！

- generate_network.yml

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Build VPC network

Parameters:
  PJPrefix:
    Description: Input a project prefix. ex) my-project
    Type: String
    Default: "my-project"

  VpcCidrBlock:
    Description: Input a VPC IPv4 CidrBlock. ex) 192.168.0.0/16
    Type: String
    Default: "192.168.0.0/16"

  AZ1:
    Description: Input a AZ will be created.
    Type: AWS::EC2::AvailabilityZone::Name

  AZ2:
    Description: Input a AZ will be created.
    Type: AWS::EC2::AvailabilityZone::Name

  PublicSubnetCidrBlock1:
    Description: Input a Public Subnet IPv4 CidrBlock. ex) 192.168.1.0/24
    Type: String
    Default: "192.168.1.0/24"

  PublicSubnetCidrBlock2:
    Description: Input a Public Subnet IPv4 CidrBlock. ex) 192.168.2.0/24
    Type: String
    Default: "192.168.2.0/24"

  PrivateSubnetCidrBlock1:
    Description: Input a Private Subnet IPv4 CidrBlock. ex) 192.168.3.0/24
    Type: String
    Default: "192.168.3.0/24"

  PrivateSubnetCidrBlock2:
    Description: Input a Private Subnet IPv4 CidrBlock. ex) 192.168.4.0/24
    Type: String
    Default: "192.168.4.0/24"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: Project Configuration
        Parameters:
          - PJPrefix
      - Label:
          default: Network Configuration
        Parameters:
          - VpcCidrBlock
          - AZ1
          - AZ2
          - PublicSubnetCidrBlock1
          - PublicSubnetCidrBlock2
          - PrivateSubnetCidrBlock1
          - PrivateSubnetCidrBlock2

Resources:
  # ------------------------------------------------------------#
  #  VPC
  # ------------------------------------------------------------#
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidrBlock
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-vpc"

  # ------------------------------------------------------------#
  #  Public Subnet
  # ------------------------------------------------------------#
  MyPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1
      CidrBlock: !Ref PublicSubnetCidrBlock1
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-public-subnet-1"
      VpcId: !Ref MyVPC

  MyPublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ2
      CidrBlock: !Ref PublicSubnetCidrBlock2
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-public-subnet-2"
      VpcId: !Ref MyVPC

  # ------------------------------------------------------------#
  #  Private Subnet
  # ------------------------------------------------------------#
  MyPrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1
      CidrBlock: !Ref PrivateSubnetCidrBlock1
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-private-subnet-1"
      VpcId: !Ref MyVPC

  MyPrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ2
      CidrBlock: !Ref PrivateSubnetCidrBlock2
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-private-subnet-2"
      VpcId: !Ref MyVPC

  # ------------------------------------------------------------#
  #  Internet Gateway
  # ------------------------------------------------------------#
  MyIGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-igw"

  MyVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref MyIGW
      VpcId: !Ref MyVPC

  # ------------------------------------------------------------#
  #  PubLic Route Table
  # ------------------------------------------------------------#
  MyPublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-public-route-table"
      VpcId: !Ref MyVPC

  MyPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref MyIGW
      RouteTableId: !Ref MyPublicRouteTable

  MyPublicSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref MyPublicRouteTable
      SubnetId: !Ref MyPublicSubnet1

  MyPublicSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref MyPublicRouteTable
      SubnetId: !Ref MyPublicSubnet2

  # ------------------------------------------------------------#
  #  Private Route Table
  # ------------------------------------------------------------#
  MyPrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${PJPrefix}-private-route-table"
      VpcId: !Ref MyVPC

  MyPrivateSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref MyPrivateRouteTable
      SubnetId: !Ref MyPrivateSubnet1

  MyPrivateSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref MyPrivateRouteTable
      SubnetId: !Ref MyPrivateSubnet2

Outputs:
  MyVPC:
    Value: !Ref MyVPC
    Export:
      Name: !Sub "${PJPrefix}-vpc"

  MyPublicSubnet1:
    Value: !Ref MyPublicSubnet1
    Export:
      Name: !Sub "${PJPrefix}-public-subnet-1"

  MyPublicSubnet2:
    Value: !Ref MyPublicSubnet2
    Export:
      Name: !Sub "${PJPrefix}-public-subnet-2"

  MyPrivateSubnet1:
    Value: !Ref MyPrivateSubnet1
    Export:
      Name: !Sub "${PJPrefix}-private-subnet-1"

  MyPrivateSubnet2:
    Value: !Ref MyPrivateSubnet2
    Export:
      Name: !Sub "${PJPrefix}-private-subnet-2"

  MyPublicRouteTable:
    Value: !Ref MyPublicRouteTable
    Export:
      Name: !Sub "${PJPrefix}-public-route-table"

  MyPrivateRouteTable:
    Value: !Ref MyPrivateRouteTable
    Export:
      Name: !Sub "${PJPrefix}-private-route-table"
```

- generate_network.json

```json
[
  {
    "ParameterKey": "PJPrefix",
    "ParameterValue": "batch-sample"
  },
  {
    "ParameterKey": "VpcCidrBlock",
    "ParameterValue": "192.168.0.0/20"
  },
  {
    "ParameterKey": "AZ1",
    "ParameterValue": "ap-northeast-1a"
  },
  {
    "ParameterKey": "AZ2",
    "ParameterValue": "ap-northeast-1c"
  },
  {
    "ParameterKey": "PublicSubnetCidrBlock1",
    "ParameterValue": "192.168.0.0/24"
  },
  {
    "ParameterKey": "PublicSubnetCidrBlock2",
    "ParameterValue": "192.168.1.0/24"
  },
  {
    "ParameterKey": "PrivateSubnetCidrBlock1",
    "ParameterValue": "192.168.2.0/24"
  },
  {
    "ParameterKey": "PrivateSubnetCidrBlock2",
    "ParameterValue": "192.168.3.0/24"
  }
]
```

- generate_network.sh

```bash
source .env

export AWS_PROFILE="$AWS_PROFILE"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"

export STACK_NAME="$PJ_PREFIX""$SEPARATOR""$NETWORK_SUFFIX"

aws cloudformation create-stack \
  --stack-name "$STACK_NAME" \
  --template-body file://generate_network.yml \
  --parameters file://generate_network.json \
  --capabilities CAPABILITY_NAMED_IAM \
  --role-arn "$CLOUDFORMATION_SERVICE_ROLE_ARN"
```

- スクリプトを実行

```bash
bash generate_network.sh
```

## セキュリティグループの作成

大変ですが、ガシガシリソースを作っていきましょう！

- generate_security_group.yml

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Build SecurityGroup

Parameters:
  PJPrefix:
    Description: Input a project prefix. ex) my-project
    Type: String
    Default: "my-project"

  SGDescription:
    Description: Input a SecurityGroupDescription.
    Type: String
    Default: "my security-group description"

  VPCSGDescription:
    Description: Input a SecurityGroupDescription.
    Type: String
    Default: "my vpc-endpoint-security-group description"

Resources:
  # ------------------------------------------------------------#
  #  Security Group
  # ------------------------------------------------------------#
  MySecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${PJPrefix}-security-group"
      GroupDescription: !Ref SGDescription
      VpcId:
        Fn::ImportValue: !Sub "${PJPrefix}-vpc"

  SecurityGroupEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      GroupId: !Ref MySecurityGroup
      IpProtocol: -1
      CidrIp: 0.0.0.0/0

  # ------------------------------------------------------------#
  #  VPC Endpoint Security Group
  # ------------------------------------------------------------#
  MyVPCEndpointSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub "${PJPrefix}-vpc-endpoint-security-group"
      GroupDescription: !Ref VPCSGDescription
      VpcId:
        Fn::ImportValue: !Sub "${PJPrefix}-vpc"

  VPCEndpointSecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref MyVPCEndpointSecurityGroup
      IpProtocol: tcp
      ToPort: 443
      FromPort: 443
      SourceSecurityGroupId:
        !Ref MySecurityGroup

  VPCEndpointSecurityGroupEgress:
    Type: AWS::EC2::SecurityGroupEgress
    Properties:
      GroupId: !Ref MyVPCEndpointSecurityGroup
      IpProtocol: -1
      CidrIp: 0.0.0.0/0

Outputs:
  MySecurityGroup:
    Value: !Ref MySecurityGroup
    Export:
      Name: !Sub "${PJPrefix}-security-group"

  MyVPCEndpointSecurityGroup:
    Value: !Ref MyVPCEndpointSecurityGroup
    Export:
      Name: !Sub "${PJPrefix}-vpc-endpoint-security-group"
```

- generate_security_group.json

```json
[
  {
    "ParameterKey": "PJPrefix",
    "ParameterValue": "batch-sample"
  },
  {
    "ParameterKey": "SGDescription",
    "ParameterValue": "batch-sample security-group description"
  },
  {
    "ParameterKey": "VPCSGDescription",
    "ParameterValue": "batch-sample vpc-endpoint-security-group description"
  }
]
```

- generate_security_group.sh

```bash
source .env

export AWS_PROFILE="$AWS_PROFILE"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"

export STACK_NAME="$PJ_PREFIX""$SEPARATOR""$SECURITY_GROUP_SUFFIX"

aws cloudformation create-stack \
  --stack-name "$STACK_NAME" \
  --template-body file://generate_security_group.yml \
  --parameters file://generate_security_group.json \
  --capabilities CAPABILITY_NAMED_IAM \
  --role-arn "$CLOUDFORMATION_SERVICE_ROLE_ARN"
```

スクリプトを実行

```bash
bash generate_security_group.sh
```

## AWS Batchに必要なIAMロールを作成する

- generate_iam_role.yml

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Make IAM role

Parameters:
  PJPrefix:
    Description: Input a project prefix. ex) my-project
    Type: String
    Default: "my-project"

Resources:
  # ------------------------------------------------------------#
  # IAM
  # ------------------------------------------------------------#
  # example: https://docs.amazonaws.cn/en_us/batch/latest/userguide/service_IAM_role.html
  AWSBatchServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PJPrefix}-AWSBatchServiceRole"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - batch.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole
      Path: "/service-role/"

  # example: https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_execution_IAM_role.html
  ecsTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PJPrefix}-ecsTaskExecutionRole"
      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
      Path: "/service-role/"

Outputs:
  AWSBatchServiceRole:
    Value: !GetAtt AWSBatchServiceRole.Arn
    Export:
      Name: !Sub "${PJPrefix}-AWSBatchServiceRole"

  ecsTaskExecutionRole:
    Value: !GetAtt ecsTaskExecutionRole.Arn
    Export:
      Name: !Sub "${PJPrefix}-ecsTaskExecutionRole"
```

- generate_iam_role.json

```json
[
  {
    "ParameterKey": "PJPrefix",
    "ParameterValue": "batch-sample"
  }
]
```

- generate_iam_role.sh

```bash
source .env

export AWS_PROFILE="$AWS_PROFILE"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"

export STACK_NAME="$PJ_PREFIX""$SEPARATOR""$IAM_ROLE_SUFFIX"

aws cloudformation create-stack \
  --stack-name "$STACK_NAME" \
  --template-body file://generate_iam_role.yml \
  --parameters file://generate_iam_role.json \
  --capabilities CAPABILITY_NAMED_IAM \
  --role-arn "$CLOUDFORMATION_SERVICE_ROLE_ARN"
```

スクリプトを実行

```bash
bash generate_iam_role.sh
```

## AWS Batchのテンプレートを作成

いよいよAWS Batchを作成していきます！

- generate_aws_batch.yml

```yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Build AWS Batch environment

Parameters:
  PJPrefix:
    Description: Input a project prefix. ex) my-project
    Type: String
    Default: "my-project"

Resources:
  # ------------------------------------------------------------#
  #  ComputeEnvironment
  # ------------------------------------------------------------#
  MyComputeEnv:
    Type: "AWS::Batch::ComputeEnvironment"
    Properties:
      Type: MANAGED
      State: ENABLED
      ComputeEnvironmentName: !Sub "${PJPrefix}-compute-environment"
      ServiceRole:
        Fn::ImportValue: !Sub "${PJPrefix}-AWSBatchServiceRole"
      ComputeResources:
        MaxvCpus: 256
        SecurityGroupIds:
          - Fn::ImportValue: !Sub "${PJPrefix}-security-group"
        Type: FARGATE
        Subnets:
          - Fn::ImportValue: !Sub "${PJPrefix}-public-subnet-1"
          - Fn::ImportValue: !Sub "${PJPrefix}-public-subnet-2"

  # ------------------------------------------------------------#
  #  JobQueue
  # ------------------------------------------------------------#
  MyJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - Order: 1
          ComputeEnvironment: !Ref MyComputeEnv
      State: ENABLED
      Priority: 1
      JobQueueName: !Sub "${PJPrefix}-job-queue"

  # ------------------------------------------------------------#
  #  JobDefinition
  # ------------------------------------------------------------#
  MyJobDefinition:
    Type: AWS::Batch::JobDefinition
    Properties:
      JobDefinitionName: !Sub "${PJPrefix}-job-definition"
      Type: container
      RetryStrategy:
        Attempts: 1
      Timeout:
        AttemptDurationSeconds: 300
      PlatformCapabilities:
        - FARGATE
      ContainerProperties:
        Image: !Sub
          - "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RepositoryName}"
          - RepositoryName: { 'Fn::ImportValue': !Sub '${PJPrefix}-image' }
        NetworkConfiguration:
          AssignPublicIp: ENABLED
        FargatePlatformConfiguration:
          PlatformVersion: 1.4.0
        ResourceRequirements:
          - Type: VCPU
            Value: 4
          - Type: MEMORY
            Value: 8192
        ExecutionRoleArn:
          Fn::ImportValue: !Sub "${PJPrefix}-ecsTaskExecutionRole"
        Command:
          - 'pwd'
```

- generate_aws_batch.json

```json
[
  {
    "ParameterKey": "PJPrefix",
    "ParameterValue": "batch-sample"
  }
]
```

- generate_aws_batch.sh

```bash
source .env

export AWS_PROFILE="$AWS_PROFILE"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"

export STACK_NAME="$PJ_PREFIX""$SEPARATOR""$BATCH_SUFFIX"

aws cloudformation create-stack \
  --stack-name "$STACK_NAME" \
  --template-body file://generate_aws_batch.yml \
  --parameters file://generate_aws_batch.json \
  --capabilities CAPABILITY_NAMED_IAM \
  --role-arn "$CLOUDFORMATION_SERVICE_ROLE_ARN"
```

- スクリプトを実行

```bash
bash generate_aws_batch.sh
```

## 作成したBatchを実行してみる

AWS CLIを利用して以下のようなコマンドでジョブを登録してみましょう。
job-nameは適当に設定、job-queueは作成したジョブキューを指定、job-definitionも作成したジョブ定義を指定しましょう。
もしかするとジョブ登録に必要な権限を持っていないかもしれないので、適切に付与しましょう。

- job_registration.sh

```sh
source .env

export AWS_PROFILE="$AWS_PROFILE_ADMIN"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"

export PJ_PREFIX="$PJ_PREFIX"

aws batch submit-job \
--job-name "$PJ_PREFIX"-job \
--job-queue "$PJ_PREFIX"-job-queue \
--job-definition "$PJ_PREFIX"-job-definition
```

スクリプトを実行します。

```bash
bash job_registration.sh
```

AWS Batchはコンテナ起動のオーバーヘッドが少し大きいので、ジョブ登録から1分程度立った後に、こんなスクリプトも作成して、jobのステータスをのぞいてみましょう！

```sh
source .env

export AWS_PROFILE="$AWS_PROFILE_ADMIN"
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"

export PJ_PREFIX="$PJ_PREFIX"
export JOB_NAME="$PJ_PREFIX"-job

JOB_LOG=$( aws logs describe-log-streams \
  --log-group-name /aws/batch/job \
  | jq '.logStreams' \
  | jq "map(select(.logStreamName | index(\"${JOB_NAME}\")))" \
) && echo ${JOB_LOG}

aws logs get-log-events \
  --log-group-name /aws/batch/job \
  --log-stream-name $( echo ${JOB_LOG} \
    | jq -r '.[].logStreamName' ) \
  | jq '.events | map({time: (.timestamp / 1000 | strftime("%Y/%m/%d %H:%M:%S")), message: .message})'
```

スクリプトを実行します。
問題なくジョブが動作していれば以下のようなレスポンスが得られます！
`"message": "/var/app"`となっており、ジョブ定義の通り、pwdコマンド実行の結果が返ってきました！成功です！

```bash
% bash job_status.sh
[ { "logStreamName": "batch-sample-job-definition/default/ab7dbced4e9e445b99e5cb3fa073f74b", "creationTime": 1655901962342, "firstEventTimestamp": 1655901969204, "lastIngestionTime": 1655901973881, "uploadSequenceToken": "xxxxxxxx", "arn": "arn:aws:logs:ap-northeast-1:xxxxxxxxxxxx:log-group:/aws/batch/job:log-stream:batch-sample-job-definition/default/ab7dbced4e9e445b99e5cb3fa073f74b", "storedBytes": 0 } ]
[
  {
    "time": "2022/06/22 12:46:09",
    "message": "/var/app"
  }
]
```

## 終わりに

ということで、それなりのボリュームになってしまったので、「Lambdaよりもめちゃくちゃめんどくさいじゃないか！」って思った方もいるかもしれませんが、テンプレート化することにより、イメージを入れ替えるだけでどんなバッチも作れてしまいます！

最初は大変かもしれませんが、ぜひ活用してみてください！