Cloudformationを使ってAWS Batch環境を構築してみる

どうも!西村祐二@大阪です。

2017年8月にCloudformationにてAWS Batchがサポートされましたので、Cloudformationの勉強を兼ねて試してみたいと思います。

さっそくやっていきましょう!

使用できるリソースタイプを確認

ドキュメントよりテンプレート作成時に使用できるリソースタイプを確認します。
AWS Batchでは下記3つのリソースタイプが使用できるようです。

AWS::Batch::ComputeEnvironment
AWS::Batch::JobDefinition
AWS::Batch::JobQueue

環境

Mac: macOS Sierra 10.12.6
AWS CLI: aws-cli/1.11.145 Python/3.6.1 Darwin/16.7.0 botocore/1.7.3

事前準備

下記ブログで作成したAWS Batch環境を見本に今回作成しますので事前に確認をお願いします。

AWS Batchを使って5分以上かかる処理を実行してみる

aws-cliのアップデート 

最新のバージョンへアップデートしておいてください。

$ sudo pip install -U awscli

テンプレート作成

今回、AWS Batchの「Compute environments」「Job queues」「Job definitions」を構築するテンプレートを作成していきます。
いきなり、3つの環境を含めてたテンプレートを作成するのは大変なのでそれぞれ分割してテンプレートを作成し、あとで1つにまとめたいと思います。

Compute environmentsのテンプレートを作成

Compute environmentsを構築するためにはテンプレート内で
ServiceRole、SecurityGroupIds、Subnets、InstanceRoleを指定する必要があります。
そのため今回はIAMとVPCも含めたテンプレートを作成したいと思います。

IAMのManagedPolicyArns箇所は適時変更ください。
今回は事前準備のところで紹介した記事を参考にしているためInstance roleにCodeCommitとS3の権限を付与しています。(下記コード134-135行目)

IAMは名前を指定すると重複する可能性があるため、特に指定していません。
指定せずにスタック作成すると任意の文字列を付与してくれます。

今回ソースが長くなりすぎてしまうので、割愛していますが、
Compute environmentsのパラメータを
Parametersセッションとして外に出しておくと良いかと思います。

テンプレート作成時の必須の項目などはこちらのドキュメントを確認しながら作成するのが良いかと思います。

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

#========VPC========#
Resources:
# Create VPC
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      InstanceTenancy: default
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"vpc" ] ]
# Create Public RouteTable
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MyVPC
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"pub-route" ] ]
# Create Private RouteTable
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MyVPC
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"pri-route" ] ]
# Create Public Subnet A
  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.0.0/24
      MapPublicIpOnLaunch: 'true'
      AvailabilityZone: "ap-northeast-1a"
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"PublicSunetA" ] ]
  PubSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicRouteTable
# Create Public Subnet C
  PublicSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: 'true'
      AvailabilityZone: "ap-northeast-1c"
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"PublicSunetC" ] ]
  PubSubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicRouteTable
# Create Private Subnet A
  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.10.0/24
      AvailabilityZone: "ap-northeast-1a"
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"PrivateSubnetA" ] ]
  PriSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnetA
      RouteTableId: !Ref PrivateRouteTable
# Create Private Subnet C
  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.11.0/24
      AvailabilityZone: "ap-northeast-1c"
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"PrivateSubnetC" ] ]
  PriSubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnetC
      RouteTableId: !Ref PrivateRouteTable
# Create InternetGateway
  myInternetGateway:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"igw" ] ]
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref MyVPC
      InternetGatewayId: !Ref myInternetGateway
# Route for InternetGateway or VPNGateway
  myRoute:
    Type: AWS::EC2::Route
    DependsOn: myInternetGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref myInternetGateway
      
#=======IAM========#
# Create ecsInstanceRole
  ecsInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        ## start
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - ec2.amazonaws.com
          Action:
          - sts:AssumeRole
        ## end
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
        - arn:aws:iam::aws:policy/AWSCodeCommitFullAccess
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
      Path: "/"
# Set InstanceProfile
  ecsInstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Roles:
        - !Ref ecsInstanceRole

# Create AWSBatchServiceRole
  AWSBatchServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        ## start
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - batch.amazonaws.com
          Action:
          - sts:AssumeRole
        ## end
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole
      Path: "/service-role/"
###
### ---ComputeEnvironment---
###
  MyComputeEnv:
    Type: "AWS::Batch::ComputeEnvironment"
    Properties:
      Type: MANAGED
      #作成したIAMから取得
      ServiceRole: !GetAtt AWSBatchServiceRole.Arn 
      ComputeEnvironmentName: cfn-env
      ComputeResources:
        MaxvCpus: 256
        MinvCpus: 0
        DesiredvCpus: 0
        SecurityGroupIds:
          #作成したVPCから取得
          - !GetAtt MyVPC.DefaultSecurityGroup
        Type: EC2
        Subnets:
          #作成したVPCから取得
          - !Ref PrivateSubnetA
          - !Ref PublicSubnetA
        #作成したVPCから取得
        InstanceRole: !GetAtt ecsInstanceProfile.Arn
        InstanceTypes:
          - optimal
        Tags: {"Name": "Batch Instance - cfn test"}
      State: ENABLED

動作確認

▼スタック作成前に構文チェックを行いエラーがないことを確認します。

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

▼今回スタック名を「batch-env」として作成します。テンプレートにIAMリソースがある場合、capabilitiesオプションを付ける必要があります。

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

▼作成したスタックのステータスを確認します。
無事スタックの作成が完了したら下記コマンドより「CREATE_COMPLETE」が表示され、
テンプレートで設定した値で「cfn-env」という名前の「Compute environments」が作成されています。
失敗した場合は「ROLLBACK_COMPLETE」となりロールバックされます。

$ aws cloudformation describe-stacks --stack-name batch-env | awk '{print $NF}'

Job queuesのテンプレートを作成

同様にJob queuesのテンプレートを作成していきます。
ComputeEnvironmentを指定する必要があるので、先程作成した名前を指定しておきます。

###
AWSTemplateFormatVersion: 2010-09-09
Description: AWS Batch Job queues from cfn
###
###---Resources:JobQueue---
###
Resources:
  MyJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - Order: 1
          ComputeEnvironment: cfn-env
      State: ENABLED
      Priority: 1
      JobQueueName: cfn-queue

動作確認

▼スタック作成前に構文チェックを行いエラーがないことを確認します。

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

▼今回スタック名を「batch-queue」として作成します。

$ aws cloudformation create-stack --stack-name batch-queue --template-body file://batch-queue.yml

▼作成したスタックのステータスを確認します。
無事スタックの作成が完了したら下記コマンドより「CREATE_COMPLETE」が表示され、
テンプレートで設定した値で「cfn-queue」という名前の「Job queues」が作成されています。
失敗した場合は「ROLLBACK_COMPLETE」となりロールバックされます。

$ aws cloudformation describe-stacks --stack-name batch-queue | awk '{print $NF}'

Job definitionsのテンプレートを作成

同様にJob definitionsのテンプレートを作成していきます。
ドキュメントには必須とのは記載ありませんが
RetryStrategyの設定がないとエラーとなるようです。

指定するコマンド、コンテナイメージは下記ブログで作成したものを指定しています。

AWS Batchを使って5分以上かかる処理を実行してみる

###
AWSTemplateFormatVersion: 2010-09-09
Description: AWS Batch Job definitions from cfn
###
###---Resources:JobDefinition---
###
Resources:
  MyJobDefinition:
    Type: AWS::Batch::JobDefinition
    Properties:
      Type: container
      JobDefinitionName: cfn-def
      ContainerProperties:
        Command:
          - sh
          - /usr/local/init.sh
        Memory: 2000
        Vcpus: 2
        Image: 1234567890.dkr.ecr.ap-northeast-1.amazonaws.com/test-repo:latest
      RetryStrategy:
        Attempts: 1

動作確認

▼スタック作成前に構文チェックを行いエラーがないことを確認します。

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

▼今回スタック名を「batch-def」として作成します。

$ aws cloudformation create-stack --stack-name batch-def --template-body file://batch-def.yml

▼作成したスタックのステータスを確認します。
無事スタックの作成が完了したら下記コマンドより「CREATE_COMPLETE」が表示され、
テンプレートで設定した値で「cfn-def」という名前の「Job definitions」が作成されています。
失敗した場合は「ROLLBACK_COMPLETE」となりロールバックされます。

$ aws cloudformation describe-stacks --stack-name batch-def | awk '{print $NF}'

スタックの削除

次に作成したテンプレートを1つにまとめるため、作成したスタックを削除しておきます。
JobQueueではComputeEnvironmentを指定しているため
「batch-env」より先に「batch-queue」を削除してください。

$ aws cloudformation delete-stack --stack-name batch-queue
$ aws cloudformation delete-stack --stack-name batch-env
$ aws cloudformation delete-stack --stack-name batch-def

1つのテンプレートにまとめる

個別に作成した3つのテンプレートを1つのテンプレートにまとめてみます。
ポイントとして、「JobQueue」のリソースを作成する際に「ComputeEnvironment」を指定する必要があるので、!Ref MyComputerEnvとして、値を指定しています。

AWSTemplateFormatVersion: 2010-09-09
Description:  AWS Batch ALL

#========VPC========#
Resources:
# Create VPC
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      InstanceTenancy: default
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"vpc" ] ]
# Create Public RouteTable
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MyVPC
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"pub-route" ] ]
# Create Private RouteTable
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref MyVPC
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"pri-route" ] ]
# Create Public Subnet A
  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.0.0/24
      MapPublicIpOnLaunch: 'true'
      AvailabilityZone: "ap-northeast-1a"
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"PublicSunetA" ] ]
  PubSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetA
      RouteTableId: !Ref PublicRouteTable
# Create Public Subnet C
  PublicSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: 'true'
      AvailabilityZone: "ap-northeast-1c"
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"PublicSunetC" ] ]
  PubSubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetC
      RouteTableId: !Ref PublicRouteTable
# Create Private Subnet A
  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.10.0/24
      AvailabilityZone: "ap-northeast-1a"
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"PrivateSubnetA" ] ]
  PriSubnetARouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnetA
      RouteTableId: !Ref PrivateRouteTable
# Create Private Subnet C
  PrivateSubnetC:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.11.0/24
      AvailabilityZone: "ap-northeast-1c"
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"PrivateSubnetC" ] ]
  PriSubnetCRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnetC
      RouteTableId: !Ref PrivateRouteTable
# Create InternetGateway
  myInternetGateway:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
      - Key: Name
        Value: !Join [ "-", [ "cfn" ,"igw" ] ]
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref MyVPC
      InternetGatewayId: !Ref myInternetGateway
# Route for InternetGateway or VPNGateway
  myRoute:
    Type: AWS::EC2::Route
    DependsOn: myInternetGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref myInternetGateway

#=======IAM========#
# Create ecsInstanceRole
  ecsInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        ## start
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - ec2.amazonaws.com
          Action:
          - sts:AssumeRole
        ## end
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
        - arn:aws:iam::aws:policy/AWSCodeCommitFullAccess
        - arn:aws:iam::aws:policy/AmazonS3FullAccess
      Path: "/"
# Set InstanceProfile
  ecsInstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Roles:
        - !Ref ecsInstanceRole
    #InstanceProfileName:

# Create AWSBatchServiceRole
  AWSBatchServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        ## start
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - batch.amazonaws.com
          Action:
          - sts:AssumeRole
        ## end
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole
      Path: "/service-role/"


#========Batch========#
# Create ComputeEnvironment
  MyComputeEnv:
    Type: "AWS::Batch::ComputeEnvironment"
    Properties:
      Type: MANAGED
      ServiceRole: !GetAtt AWSBatchServiceRole.Arn
      ComputeEnvironmentName: cfn-env
      ComputeResources:
        MaxvCpus: 0
        MinvCpus: 0
        DesiredvCpus: 0
        SecurityGroupIds:
          - !GetAtt MyVPC.DefaultSecurityGroup
        Type: EC2
        Subnets:
          - !Ref PrivateSubnetA
          - !Ref PublicSubnetA
        InstanceRole: !GetAtt ecsInstanceProfile.Arn
        InstanceTypes:
          - optimal
        Tags: {"Name": "Batch Instance - cfn test"}
      State: ENABLED
# Create JobQueue
  MyJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      ComputeEnvironmentOrder:
        - Order: 1
          ComputeEnvironment: !Ref MyComputeEnv
      State: ENABLED
      Priority: 1
      JobQueueName: cfn-queue

# Create JobDefinition
  MyJobDefinition:
    Type: AWS::Batch::JobDefinition
    Properties:
      Type: container
      JobDefinitionName: cfn-def
      ContainerProperties:
        Command:
          - sh
          - /usr/local/init.sh
        Memory: 2000
        Vcpus: 2
        Image: 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/test-repo:latest
      RetryStrategy:
        Attempts: 1

動作確認

▼スタック作成前に構文チェックを行いエラーがないことを確認します。

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

▼今回スタック名を「batch」として作成します。
スタック作成完了までに多少時間がかかるため、完了までwaitしてくれるコマンドをつなげておきます。

$ aws cloudformation create-stack --stack-name batch --template-body file://batch.yml --capabilities CAPABILITY_IAM && aws cloudformation wait stack-create-complete --stack-name batch

▼作成したスタックのステータスを確認します。
無事スタックの作成が完了したら下記コマンドより「CREATE_COMPLETE」が表示され、
テンプレートで設定したAWS Batchの環境が作成されています。
失敗した場合は「ROLLBACK_COMPLETE」となりロールバックされます。

$ aws cloudformation describe-stacks --stack-name batch | awk '{print $NF}'

スタックの削除

AWS Batchの環境が必要なくなったらスタックを削除することで環境をまとめて削除することができます。
削除完了まで多少時間がかかるため、削除が完了するまでwaitしてくるコマンドをつなげておきます。

$ aws cloudformation delete-stack --stack-name batch && aws cloudformation wait stack-delete-complete --stack-name batch

さいごに

いかがだったでしょうか。
テンプレートの作成は慣れるまでなかなか大変ですが、
自在に扱えるようになればとても効率よく作業が行えるため
これからも積極的に利用していきたいと思います。

誰かの参考になれば幸いです。