こんにちは。
現在重量級のバッチ処理をPythonで開発しておりますが、そこで採用しているAWS Batchの話をしてみたいと思います。
AWS Batchとは
簡単に言ってしまえば、スケーラブルかつサーバレスなバッチ実行環境としてありがちな「SQS + ECS + EC2」の構成をラップしてくれるサービスです。
具体的には、下記の特徴を持っています。
- ECSの設定を行ってくれるため、ECS方面の知識が不要
- 処理性能の増減は、CPUコア数を設定するだけなため非常に楽チン
- Dockerコンテナ上で動作するため、開発言語、フレームワークを自由に選定することが可能
- キューイングの仕組みが実装されているため、キュー回りを独自に開発する必要がない
- 勝手にログファイルがCloudWatchLogsに転送される
- ある程度(ある程度です)のジョブの実行順や、並列実行の制御が可能
AWS Batch自体の利用料は無料で、実行リソース(EC2等)の料金のみで利用することが可能です。バッチ処理がスポットインスタンス上での実行でも耐えられる実装であれば、さらにAWS料金が下がる可能性があります。
前提
- 以降の操作は東京リージョンを前提としています。別リージョンでの構築は適宜読み替えてください。
- 所々、Python風味を感じる何かが残っていますが、気にしないであげてください。
AWS Batchのデプロイ方法
AWS BatchはECSを利用して動作するため、バッチ処理の実行環境はECRにDocker imageをpushする形式となります。
AWS Batchの設定でECRのimageを指定する箇所があるため、事前にDocker imageをpushしておくのが吉です。
ECRにDocker imageをpushする
Docker imageを作成する環境や手順については以下の記事を参考してください。
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インスタンスが起動されず、ジョブが実行できない状態となります。
サービスロールとインスタンスロールは、デフォルトで選択できるロールを設定しています。
ジョブキューの設定
ジョブキューの設定は優先度とコンピューティング環境のみとなります。
前項で作成したコンピューティング環境を選択します。
ジョブ定義の設定
ジョブ定義では、コンテナイメージを設定します。
前述でECRにpushしたコンテナイメージのURLを指定するだけです。
他にも実行タイムアウト値や環境変数等の設定も可能ですが、ここでは省略しています。
これで、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の管理コンソールからジョブキューの進行状況を確認します。
AWS Batchのダッシュボードでは、ジョブキューがSUBMITTEDからSUCCEEDEDに遷移するのを見守ります。
SUCCEEDED(あるいは残念ながらFAILED)に遷移したら、ジョブキューの実行結果を確認します。
ジョブキューの実行結果画面にCloudWatchLogsのログへのショートカット(View logs)がありますので、そこから実行ログを確認します。
無事に[echo ”Hello batch”]が実行されていることが確認できました。 これで、AWS Batchの動作確認は完了です。
crontab的な実行方法
AWS Batchは、その名前から「定期実行できるんでしょ」と思われがち(自分もそう思っていました)ですが、残念ながらcrontab的な機能を持っていません。
その代わりという訳ではありませんが、CloudWatchEventsを利用してお手軽にバッチ処理の定期実行を行う方法があります。
CloudWatchEvents設定
先ほど作成したLambda関数を、月曜日から金曜日の12:00に実行するように設定してみます。
この例では、おじさんエンジニアですのでcron式で設定しました。
AWSのcron式については、こちらのページを参考にしてみてください。
たったこれだけの設定で、バッチ処理の定期実行の設定させることができました。
バッチ処理のAPI化
バッチ処理をAPI化することも容易に可能です。
ここでも、キュー送信部の処理をLambda関数で実装していることで小回りが効き、API Gateway → Lambdaを設定するだけでAPI経由で実行できる使い勝手の良いバッチ処理となります。
API Gateway → Lambdaの設定は以下の記事を参考に設定してみてください。
やや残念なところ
お手軽な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から実行させるようなシンプルなバッチ処理には積極的に導入を検討することをお勧めします。