AWS Batchでサーバレスバッチの夢を見ることができるのか

こんにちは。

初老サーバエンジニアの望月です。

現在重量級のバッチ処理をPythonで開発しておりますが、そこで採用しているAWS Batchの話をしてみたいと思います。

AWS Batchとは

簡単に言ってしまえば、スケーラブルかつサーバレスなバッチ実行環境としてありがちな「SQS + ECS + EC2」の構成をラップしてくれるサービスです。

具体的には、下記の特徴を持っています。

  • ECSの設定を行ってくれるため、ECS方面の知識が不要
  • 処理性能の増減は、CPUコア数を設定するだけなため非常に楽チン
  • Dockerコンテナ上で動作するため、開発言語、フレームワークを自由に選定することが可能
  • キューイングの仕組みが実装されているため、キュー回りを独自に開発する必要がない
  • 勝手にログファイルがCloudWatchLogsに転送される
  • ある程度(ある程度です)のジョブの実行順や、並列実行の制御が可能

AWS Batch自体の利用料は無料で、実行リソース(EC2等)の料金のみで利用することが可能です。バッチ処理がスポットインスタンス上での実行でも耐えられる実装であれば、さらにAWS料金が下がる可能性があります。

f:id:teco_mochi:20200301225413p:plain
AWS Batchの構成イメージ

前提

  • 以降の操作は東京リージョンを前提としています。別リージョンでの構築は適宜読み替えてください。
  • 所々、Python風味を感じる何かが残っていますが、気にしないであげてください。

AWS Batchのデプロイ方法

AWS BatchはECSを利用して動作するため、バッチ処理の実行環境はECRにDocker imageをpushする形式となります。

AWS Batchの設定でECRのimageを指定する箇所があるため、事前にDocker imageをpushしておくのが吉です。

ECRにDocker imageをpushする

Docker imageを作成する環境や手順については以下の記事を参考してください。

qiita.com

ECRへのpush手順 docs.aws.amazon.com

ここではとりあえずAWS Batchを動作させるために必要なDocker imageを作成するためのDockerfileと、ECRにpushするコマンドを記載します。

Dockerfile

FROM amazonlinux:2

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

ARG PROJECT_DIR
WORKDIR $PROJECT_DIR

CMD ["/bin/bash"]

docker-compose.yml

version: '3.5'

services:
  python:
    container_name: test-image
    image: test-image
    build:
      context: ./
      dockerfile: Dockerfile
      args:
        - PROJECT_DIR=/var/app
    environment:
      AWS_ACCESS_KEY_ID: [Your batch aws_access_key_id]
      AWS_SECRET_ACCESS_KEY: [Your batch aws_secret_access_key]
      AWS_DEFAULT_REGION: ap-northeast-1
      AWS_DEFAULT_OUTPUT: json
    working_dir: /var/app
    tty: true

ecr_deploy.sh

WORK_DIR="$HOME/work"
DOCKER_DIR="$WORK_DIR/docker"
DOCKER_IMAGE_NAME="test-image"
DOCKER_IMAGE_REVISION="latest"
DOCKER_COMPOSE_FILE="docker-compose.yml"
ECR_ENDPOINT=[Your AWS account].dkr.ecr.ap-northeast-1.amazonaws.com
ECR_REPOSITORY_NAME=[ECR repository name]
AWS_ACCESS_KEY_ID=[Your ECR push  aws access key id]
AWS_SECRET_ACCESS_KEY=[Your ECR push secret access key]

# docker build
cd ${DOCKER_DIR}
docker-compose -f ${DOCKER_COMPOSE_FILE} build

# ecr push
cd ${WORK_DIR}
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
aws ecr get-login --no-include-email --region ap-northeast-1 > ecr-login.sh
chmod +x ecr-login.sh
./ecr-login.sh
docker tag ${DOCKER_IMAGE_NAME}:${DOCKER_IMAGE_REVISION} ${ECR_ENDPOINT}/${ECR_REPOSITORY_NAME}
docker push ${ECR_ENDPOINT}/${ECR_REPOSITORY_NAME}
rm ecr-login.sh

なお、ECRにpushする際のクレデンシャルには、AmazonEC2ContainerRegistryPowerUserのポリシーを付与しています。

AWS Batchの設定

AWS Batchを利用するためにいくつか設定が必要となりますので、順に解説していきます。

コンピューティング環境の設定

ここでの設定のポイントはCPU数となります。

最小vCPUは、常時立ち上げておく最小のCPU数となりますので、ジョブ完了後にEC2インスタンスを落としたい場合は0を設定します。

また、必要なvCPUはジョブ実行時に利用するCPU数となり、デフォルトの0のままだとEC2インスタンスが起動されず、ジョブが実行できない状態となります。

サービスロールとインスタンスロールは、デフォルトで選択できるロールを設定しています。

f:id:teco_mochi:20200303212045p:plainf:id:teco_mochi:20200303212049p:plainf:id:teco_mochi:20200303212053p:plain

ジョブキューの設定

ジョブキューの設定は優先度とコンピューティング環境のみとなります。

前項で作成したコンピューティング環境を選択します。

f:id:teco_mochi:20200303213454p:plain

ジョブ定義の設定

ジョブ定義では、コンテナイメージを設定します。

前述でECRにpushしたコンテナイメージのURLを指定するだけです。

他にも実行タイムアウト値や環境変数等の設定も可能ですが、ここでは省略しています。

f:id:teco_mochi:20200303214305p:plain

これで、AWS Batchを実行する準備が整いました。

AWS Batchの実行

AWS Batchは、設定したジョブキューにキューを送信することで実行されます。

キューの送信方法は色々ありますが、後々の使い勝手がよいLambda関数からキューを送信してみます。

Lambda関数

# coding: utf-8
import boto3

# AWS Batch ARN
ARN = 'arn:aws:batch:ap-northeast-1:your aws account'
# JOB定義名
JOB_DEFINITION_NAME = 'test-job-env'
# キュー名
QUEUE_NAME = 'test-job-queue'
# 実行ジョブ名
JOB_NAME = 'test-job-name'


def lambda_handler(event, _context):

    # 実行コマンド
    command_array = ['echo', event['echo_string']]

    # ジョブキュー送信
    client = boto3.client('batch')
    params = get_job_params(client, event, command=command_array)
    return client.submit_job(**params)


def get_job_revision(client):
    """
    ジョブ定義の最新リビジョン番号を取得する

    Args:
        client: batchクライアント

    Returns:
      最新リビジョン番号
    """
    job_definitions = client.describe_job_definitions()['jobDefinitions']

    job_revision_num = 1
    for job_definition in job_definitions:
        if job_definition['jobDefinitionName'] == JOB_DEFINITION_NAME:
            if job_definition['revision'] > job_revision_num:
                job_revision_num = job_definition['revision']
    return str(job_revision_num)


def get_job_params(client, event, command):
    """
    AWS Batchのジョブパラメータを設定

    Args:
        client: batchクライアント
        event: パラメータ
        command: 実行コマンド

    Returns:
        ジョブパラメータ
    """
    # ジョブ名
    params = {'jobName': JOB_NAME}

    # ジョブ定義
    params['jobDefinition'] = ARN + ':job-definition/' + JOB_DEFINITION_NAME + ':' + get_job_revision(client)

    # 実行コマンド
    params['containerOverrides'] = {'command': command}

    # 送信先キュー
    params['jobQueue'] = ARN + ':job-queue/' + QUEUE_NAME

    return params

なお、Lambda関数の実行ロールには、下記のカスタムポリシーを設定しました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "batch:SubmitJob",
                "batch:DescribeJobDefinitions",
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

テスト実行してみる

ここまで準備が整ったら、テストデータを設定してAWS consoleからLambda関数をテスト実行してみましょう。

Lambdaテストデータ

{
  "echo_string": "Hello batch"
}

Lambda関数のテスト実行が正常終了したのを確認したら、AWS Batchの管理コンソールからジョブキューの進行状況を確認します。

f:id:teco_mochi:20200309112433p:plain

AWS Batchのダッシュボードでは、ジョブキューがSUBMITTEDからSUCCEEDEDに遷移するのを見守ります。

SUCCEEDED(あるいは残念ながらFAILED)に遷移したら、ジョブキューの実行結果を確認します。

f:id:teco_mochi:20200309113436p:plain

ジョブキューの実行結果画面にCloudWatchLogsのログへのショートカット(View logs)がありますので、そこから実行ログを確認します。

f:id:teco_mochi:20200309114221p:plain

無事に[echo ”Hello batch”]が実行されていることが確認できました。 これで、AWS Batchの動作確認は完了です。

f:id:teco_mochi:20200309114328p:plain

crontab的な実行方法

AWS Batchは、その名前から「定期実行できるんでしょ」と思われがち(自分もそう思っていました)ですが、残念ながらcrontab的な機能を持っていません。

その代わりという訳ではありませんが、CloudWatchEventsを利用してお手軽にバッチ処理の定期実行を行う方法があります。

CloudWatchEvents設定

先ほど作成したLambda関数を、月曜日から金曜日の12:00に実行するように設定してみます。

この例では、おじさんエンジニアですのでcron式で設定しました。

f:id:teco_mochi:20200311103618p:plain

AWSのcron式については、こちらのページを参考にしてみてください。

docs.aws.amazon.com

たったこれだけの設定で、バッチ処理の定期実行の設定させることができました。

バッチ処理のAPI化

バッチ処理をAPI化することも容易に可能です。

ここでも、キュー送信部の処理をLambda関数で実装していることで小回りが効き、API Gateway → Lambdaを設定するだけでAPI経由で実行できる使い勝手の良いバッチ処理となります。

API Gateway → Lambdaの設定は以下の記事を参考に設定してみてください。

qiita.com

やや残念なところ

お手軽なAWS Batchですが、いくつか残念なところもあります。

  • EC2インスタンスサイズが、large以上しか選択できない

    開発時はsmallインスタンスでケチケチやりたい、またはケチケチやっている態に見せたいのに

  • EC2のみでFargateが選択できない

    気にすることはないと思うのですが、なにがなんでもFargate派の方には残念ポイントかも

  • Lambda程ではないが、RDBとの相性はイマイチ

    容易に並列実行数を増やせてしまうことの弊害で、コネクションの枯渇や性能面でボトルネックになってしまう可能性があります

    やはり、DynamoDB等のスケーラブルな構成との相性が良さそうです

  • 設定する箇所が思いのほか多く、学習コストはそれなりに必要

ハマりポイント

AWS Batchを触ってみて、いくつかハマりポイントがありましたので、ご紹介しておきます。

キューがRUNNABLEから進まない

  • EC2インスタンスが起動しない

    コンピューティングリソース(vCPU数)が足りていなかった。

  • EC2インスタンスは起動するがRUNNABLEから進まない

    起動したEC2インスタンスがinternet接続できなかった。

    ECSからEC2にコマンド実行する際に、internet経由でコマンド実行しているため、EC2インスタンスが立ち上がってもコマンドが実行できない状況だった。

実行commandにセパレータを入れると思ったように動作しない

    cd /var/hoge; python3 fuga

AWS Batchからコマンド実行する際、Dockerコマンドパラメータでコンテナの実行コマンドを指定しているためなのか、セパレータで複数コマンドを実行しようとするとエラーとなります。

複数のコマンドを実行する場合は、shellを実行する形式に変更しましょう。

最後に

AWS Batchは、バッチ処理を実行させるための非常に簡便かつ強力なサービスでした。

複雑なジョブ連携には対応しきれない部分もありますが、crontabから実行させるようなシンプルなバッチ処理には積極的に導入を検討することをお勧めします。

tecotec.co.jp