AWS CLIのコマンドが動くECSバッチ実行環境を作るCloudFormationテンプレート

お知らせ
Table of Contents

はじめに

この記事ではAWS公式が出しているAWS Batch実行環境を一発で用意する方法1を説明します。

知っていれば簡単にできると思うのですが、当初自分で一からcloudformationのyamlを用意しようとしたらコンテナ実行環境を用意するのが案外面倒で、結構時間がかかってしまいました。そこで AWSの公式レポジトリを引っ張ってきて必要な部分だけ改造したら過不足なく動いた、というお話です。

以下の図は1から引っ張ってきたものです。今回作りたいものはawsコンソールから手動でバッチ実行するので2,3,4,5,7の矢印はありません。
file

なお、時間がある人は詳しいハンズオン記事2とかを読むのがいいと思われます。

前提

  • cloudshellとかで簡単なテスト実行をして動作確認ができたシェルスクリプトをそのままコンテナ環境に移植して長時間バッチ実行したい
  • マイグレーションのロジックは単純だが、要所要所でネットワークアクセスが必要だったりして時間がかかる(具体的には並列化しても一週間以上かかる)
  • AWS内部のサービス (cognito等)に入っているデータを、加工してdynamo dbにマイグレーションしたい

必要なもの

手順

githubからレポジトリをclone

https://github.com/aws-samples/aws-batch-processing-job-repo をクローンします。この記事では 023cbeaaba3ef5eb1d0be2670199ef672743f6bb 時点での情報を参照しています。

Dockerfileを確認

以下の通り、公式ではpythonスクリプトが動くdockerイメージになっているので、これをaws cliを動くようなイメージに差し替えます。aws cliを動かすスクリプト本体の名前は exec.sh としています。

# ベースイメージとして公式の Amazon Linux を使用
FROM amazonlinux:latest

# 必要なツールのインストール(AWS CLI, jqなど)
RUN yum update -y && \
    yum install -y \
    aws-cli \
    jq \
    bash

RUN yum install -y findutils

# スクリプトをコンテナ内にコピー
COPY *.sh .

# デフォルトで実行するコマンド(必要に応じて変更)
CMD ["bash", "exec.sh"]

実行スクリプトを確認

exec.sh の中身は例えば以下のような感じで、cognitoユーザーの情報を出力し情報を加工してdynamo dbに入れ直します。

#!/bin/bash
# データをCognitoから取得する
user_pool_id=ap-northeast-1_XXXXXXXXXX
dynamodb_table_name=YourDynamoDBTableName

# Cognitoからユーザーデータを取得
user_data=$(aws cognito-idp list-users --user-pool-id $user_pool_id --query 'Users')

# データを加工してDynamoDBに挿入
for row in $(echo "${user_data}" | jq -c '.[]'); do
  # 必要なフィールドを抽出
  username=$(echo $row | jq -r '.Username')
  email=$(echo $row | jq -r '.Attributes[] | select(.Name=="email").Value')

  # DynamoDBにデータを挿入
  aws dynamodb put-item \
    --table-name $dynamodb_table_name \
    --item '{
      "Username": {"S": "'$username'"},
      "Email": {"S": "'$email'"}
    }'
done

template.yaml を確認

今回はdynamo dbアクセス機能は使いますが、他の部分で使わない機能がかなりたくさんあります。そういった機能は削っていきます。CodeCommitなど新規作成ができないリソース以外に関しては、使わないだけでデプロイしても動きますが、無駄に課金することになるので削ります。

以下、削ったリソースのリストです。

  • LambdaExecutionRole
  • BatchProcessS3Bucket: もとのレポジトリでは「s3にcsv配置したらそのタイミングでlambdaでバッチ開始」みたいなことをやっているのですが、そこまで高度なことはやらないので今回は削ります。
  • BatchProcessBucketPermission: 同上
  • BatchProcessingLambdaInvokeFunction: 同上
  • CodeCommitRepository: codecommitは新規アカウントでの利用ができなくなっています。今回参考にしたレポジトリでは更新が追いついていませんでした。
  • CodeBuildProject
  • CloudWatchEventsCodeBuildRole
  • CloudWatchEventCodeBuildEventRule
  • BatchProcessingDynamoDBTable: dynamo dbのリソースは使いましたが、ここで作られるものではなく既存環境にあるものを使いました。
  • S3VPCEndpoint: シェルスクリプトからs3アクセスはなかったので削除。

以下、変更したリソースのリストです。

  • DynamoDBVPCEndpoint: 対象のテーブル名を変更
  • CodeBuildRole: CodeCommit関連のポリシーを削除
  • BatchTaskExecutionRole: PolicyName: !Sub ${StackName}-ecs-task-dynamo-policyにおける対象のテーブル名を変更

以下、outputsから削除

  • BucketName
  • LambdaName

最終的には以下のようになりました。

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Orchestrating an Application Process with AWS Batch in Fargate using CloudFormation'
Parameters:
  StackName:
    Type: String
    #Default: batch-processing-job
    Description: The name of the application stack 
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
  InternetGateway:
    Type: AWS::EC2::InternetGateway
  PublicSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPC
  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
        Ref: VPC
      InternetGatewayId:
        Ref: InternetGateway
  NATEIP:
    DependsOn: VPCGatewayAttachment
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc        
  NATGateway:
      DependsOn: VPCGatewayAttachment
      Type: AWS::EC2::NatGateway
      Properties:
        AllocationId:
          Fn::GetAtt:
          - NATEIP
          - AllocationId
        SubnetId:
          Ref: PublicSubnet    
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2 Security Group for instances launched in the VPC by Batch
      VpcId:
        Ref: VPC
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.0.0/24
      VpcId:
        Ref: VPC
      MapPublicIpOnLaunch: 'True'
      Tags:
        - Key: Name
          Value: !Sub ${StackName} Public Subnet  
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.1.0/24
      VpcId:
        Ref: VPC
      Tags:
        - Key: Name
          Value: !Sub ${StackName} Private Subnet   
  PublicSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: VPC
      Tags:
        - Key: Name
          Value: !Sub ${StackName} Public Route        
        - Key: Project
          Value: AWS Batch in Fargate         
  PublicSubnetRoute:
    Type: AWS::EC2::Route
    DependsOn: VPCGatewayAttachment
    Properties:
      RouteTableId: 
        Ref: PublicSubnetRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: InternetGateway
  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId:
        Ref: PublicSubnet
      RouteTableId:
        Ref: PublicSubnetRouteTable
  PrivateSubnetRouteTable:
      Type: AWS::EC2::RouteTable
      Properties:
        VpcId:
          Ref: VPC
        Tags:
          - Key: Name
            Value: !Sub ${StackName} Private Route    
          - Key: Project
            Value: AWS Batch in Fargate                         
  PrivateSubnetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref 'PrivateSubnetRouteTable'
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: NATGateway
  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId:
        Ref: PrivateSubnet
      RouteTableId:
        Ref: PrivateSubnetRouteTable        

  BatchServiceRole:
    Type: AWS::IAM::Role
    Properties:
      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

  BatchProcessingJobDefinition:
    Type: AWS::Batch::JobDefinition
    Properties:
      Type: container
      PropagateTags: true
      JobDefinitionName: BatchJobDefinition
      ContainerProperties:
        Image:
          Fn::Join:
          - ''
          - - Ref: AWS::AccountId
            - .dkr.ecr.
            - Ref: AWS::Region
            - !Sub '.amazonaws.com/${StackName}-repository:latest'
        FargatePlatformConfiguration:
          PlatformVersion: LATEST
        ResourceRequirements:
          - Value: 1
            Type: VCPU
          - Value: 2048
            Type: MEMORY
        JobRoleArn:  !GetAtt 'BatchTaskExecutionRole.Arn'
        ExecutionRoleArn:  !GetAtt 'BatchTaskExecutionRole.Arn'
        LogConfiguration:
          LogDriver:  awslogs
          Options:
            awslogs-group: !Ref 'BatchLogGroup'
            awslogs-region: !Ref AWS::Region
            awslogs-stream-prefix: !Sub ${StackName}-logs
        Command:
        - bash
        - exec.sh
      PlatformCapabilities:
      - FARGATE
      Tags:
        Service: Batch
        Name: JobDefinitionTag
        Expected: MergeTag

  BatchLogGroup:
    Type: AWS::Logs::LogGroup
    Properties: 
      LogGroupName: !Sub ${StackName}-awslogs
      RetentionInDays: 7
  BatchTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${StackName}-taskexec-role
      AssumeRolePolicyDocument:
        Statement:
        - Effect: Allow
          Principal:
            Service: [ecs-tasks.amazonaws.com]
          Action: ['sts:AssumeRole']
      Path: /
      Policies:
        - PolicyName: AmazonECSTaskExecutionRolePolicy
          PolicyDocument:
            Statement:
            - Effect: Allow
              Action:
                - 'ecr:GetAuthorizationToken'
                - 'ecr:BatchCheckLayerAvailability'
                - 'ecr:GetDownloadUrlForLayer'
                - 'ecr:BatchGetImage'
                - 'logs:CreateLogStream'
                - 'logs:PutLogEvents'
              Resource: '*'
        - PolicyName: !Sub ${StackName}-ecs-task-s3-get-policy
          PolicyDocument:
            Statement:
            - Effect: Allow
              Action:
                - s3:PutObject
                - s3:GetObject
                - s3:ListBucket
              Resource: 
                - !Join
                  - ''
                  - - 'arn:aws:s3:::'
                    - !Sub "${StackName}-${AWS::AccountId}"
                - !Join
                  - ''
                  - - 'arn:aws:s3:::'
                    - !Sub "${StackName}-${AWS::AccountId}"
                    - /*
        - PolicyName: !Sub ${StackName}-ecs-task-dynamo-policy
          PolicyDocument:
            Statement:
            - Effect: Allow
              Action:
                - 'dynamodb:PutItem'
                - 'dynamodb:Query'
                - 'dynamodb:UpdateItem'
              Resource:
                - !Join
                  - ''
                  - - 'arn:aws:dynamodb:'
                    - !Sub "${AWS::Region}:${AWS::AccountId}:table/*"
                - !Join
                  - ''
                  - - 'arn:aws:dynamodb:'
                    - !Sub "${AWS::Region}:${AWS::AccountId}:table/*/index/*"

  BatchProcessingJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      JobQueueName: !Sub "${StackName}-queue"
      State: ENABLED
      Priority: 1
      ComputeEnvironmentOrder:
      - Order: 1
        ComputeEnvironment:
          Ref: ComputeEnvironment
  ComputeEnvironment:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      Type: MANAGED
      State: ENABLED
      ComputeResources:
        Type: FARGATE
        MaxvCpus: 40
        Subnets:
        - Ref: PrivateSubnet
        SecurityGroupIds:
        - Ref: SecurityGroup
      ServiceRole:
        Ref: BatchServiceRole

  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess 
      # - arn:aws:iam::aws:policy/AWSCodeCommitFullAccess   
      AssumeRolePolicyDocument:
        Statement:
        - Action: ['sts:AssumeRole']
          Effect: Allow
          Principal:
            Service: [codebuild.amazonaws.com]
        Version: '2012-10-17'
      Path: /
      Policies:
        - PolicyName: CodeBuildAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                - 'logs:*'
                - 'ec2:CreateNetworkInterface'
                - 'ec2:DescribeNetworkInterfaces'
                - 'ec2:DeleteNetworkInterface'
                - 'ec2:DescribeSubnets'
                - 'ec2:DescribeSecurityGroups'
                - 'ec2:DescribeDhcpOptions'
                - 'ec2:DescribeVpcs'
                - 'ec2:CreateNetworkInterfacePermission'
                Effect: Allow
                Resource: '*'
  BatchProcessRepository: 
    Type: AWS::ECR::Repository
    Properties: 
      RepositoryName: !Sub ${StackName}-repository

  DynamoDBVPCEndpoint:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      RouteTableIds:
        - !Ref PrivateSubnetRouteTable
      ServiceName:
        !Sub "com.amazonaws.${AWS::Region}.dynamodb"
      VpcId: !Ref VPC
      PolicyDocument:
        Statement:
        - Effect: Allow
          Principal: '*'
          Action:
            - 'dynamodb:*'
          Resource:
            - !Join
              - ''
              - - 'arn:aws:dynamodb:'
                - !Sub "${AWS::Region}:${AWS::AccountId}:table/*"
            - !Join
              - ''
              - - 'arn:aws:dynamodb:'
                - !Sub "${AWS::Region}:${AWS::AccountId}:table/*/index/*"

  EcrApiVPCEndpoint:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      VpcEndpointType: Interface
      ServiceName:
        !Sub "com.amazonaws.${AWS::Region}.ecr.api"
      VpcId: !Ref VPC
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: '*'
            Action:
              - 'ecr:*'
            Resource:
              - !Join
                - ''
                - - 'arn:aws:ecr:'
                  - !Sub "${AWS::Region}:${AWS::AccountId}:repository/${StackName}"

  EcrDkrVPCEndpoint:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      VpcEndpointType: Interface
      ServiceName:
        !Sub "com.amazonaws.${AWS::Region}.ecr.dkr"
      VpcId: !Ref VPC
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: '*'
            Action:
              - 'ecr:*'
            Resource:
              - !Join
                - ''
                - - 'arn:aws:ecr:'
                  - !Sub "${AWS::Region}:${AWS::AccountId}:repository/${StackName}"           

Outputs:
  ComputeEnvironmentArn:
    Value:
      Ref: ComputeEnvironment
  BatchProcessingJobQueueArn:
    Value:
      Ref: BatchProcessingJobQueue
  BatchProcessingJobDefinitionArn:
    Value:
      Ref: BatchProcessingJobDefinition

デプロイ

githubのreadmeに書いてあるとおりデプロイします。

手動バッチ実行

awsコンソールのバッチ実行のページを開きます。バッチの新規作成をクリックします。ほとんどの項目はそのままでokですが、環境変数とコンテナのentrypointだけは必要に応じて修正します。修正できたらそのまま手動でバッチ実行できます。

感想

今回はIAMのクレデンシャルを環境変数としてジョブ実行時に設定し、コンテナ内ではawsコマンドをインストールしておくことで、awsコマンドがコンテナ内で自由に扱えるようになるという野蛮な方法を取りました(セキュリティ的に推奨されていないと思います)。
普通にec2インスタンスを最初から使えばよかったのではという気もしますが、単発バッチ実行の手順が全部コードで管理できるのは便利なので良しとします。

参考文献


  1. Orchestrating an Application Process with AWS Batch using AWS CloudFormation https://github.com/aws-samples/aws-batch-processing-job-repo 

  2. CloudFormationを使ってAWS Batchを(ほぼ)全部AWS CLIで構築してみるハンズオン! https://qiita.com/nokonoko_1203/items/7e499f92780469a28237  

タイトルとURLをコピーしました