LIFULLで技術マネージャーをしています。たまにはエンジニアぽいところを見せてほしいという社内からのプレッシャー激励に促されて、AWSにおける知見を整理して共有します。
はじめに
システムを運用する上で、日次や月次の決まったタイミングで動作するcronライクな定期バッチを必要とするケースがあると思います。定期実行なので、インフラリソースをそのタイミングだけ利用するサーバレスなバッチにするのが経済的にも地球環境的にも優しいですよね。
ということで、AWSのサービスを利用して実現しようというお話なんですが、選択肢の多さに結局どれを使えばいいんだっけ?ってことになりかねません。そこで考えられる選択肢として Lambda、 Fargate、 Batch の3つを取り上げて、定期バッチ環境を作る上でのメリット、デメリットをまとめたいと思います。
サービス比較
記事投稿時点での特徴を独断でまとめたものがこちらです。
Lambda | Fargate | Batch | |
---|---|---|---|
環境構築の容易さ | ◯ | △ | △ |
実行環境の拡張性 | × | ◯ | ◯ |
マシンリソースの拡張性 | × | △ | ◯ |
機密情報の扱いやすさ | △ | ◯ | × |
スケジューリング設定 | ◯ | ◯ | ◯ |
ロギング設定 | △ | ◯ | △ |
それでは、一つずつ説明していきます。
環境構築の容易さ
Lambda
はコードをアップロードさえすれば、実行環境が構築でき、その導入障壁の低さが魅力と言えます。
Fargate
とBatch
はコンテナベースのコンピューティングサービスであり、事前にバッチアプリケーションを含んだDockerイメージを用意しておく必要があります。
実行環境の拡張性
Lambda
は標準で使用できるランタイム(プログラム言語とバージョン)に限りがあります。ただ主要言語はほぼサポートされており、カスタムランタイムも作成できるため、さほど不自由さを感じないのではないでしょうか。
また、そのままだと標準ライブラリしか使えないため、 Serverless Framework や AWS SAM を使用して、ローカルの実行環境をパッケージ化してデプロイする運用が一般的かと思います。ただ、OS依存のネイティブライブラリの導入にはさらに手間がかかるなど、拡張性と利便性がトレードオフであるといえます。
Fargate
とBatch
は前述の通り、あらかじめ自前で用意したコンテナ上でアプリケーションを動作させるため、自由度の高い実行環境を構築することが可能です。
マシンリソースの拡張性
Lambda
は実行時間に制限があるというのは有名な話ですね。またメモリ割り当てなど、さまざまなリソースに制限が適用されており、変更することができません。
※ 詳しくは AWS Lambdaの制限 を参照してください。
Fargate
はCPUとメモリの割り当て可能な組み合わせを選択して利用します。現在の利用可能範囲は 0.25vCPU × 512MBメモリ 〜 4vCPU × 30GBメモリ となっています。
また、ストレージはコンテナのルートボリューム(10GB)とマウント利用するコンテナ間共有ボリューム(4GB)の2種類が利用可能ですが、この容量制限を変更することはできません。
※ 詳しくは AWS Fargte - タスクCPUとメモリ | AWS Fargte - タスクストレージ を参照してください。
Batch
はあらかじめ設定したコンピューティング環境でジョブを実行するサービスですが、そのリソースはEC2インスタンスになります。こちらはT系以外の全てのインスタンスを選択可能です。
また、ストレージはデフォルトでコンテナのルートボリューム(10GB)が利用可能とFargate
と同じです。これはDockerのベースサイズのデフォルト値が10GBであることに起因するのだと思いますが、この容量制限はFargate
と違い拡張することが可能です。拡張方法は Qiita - AWS Batch で使う EC2 インスタンスのストレージ容量を増やす の記事を参考にしてください。
機密情報の扱いやすさ
3サービス共通して、環境変数を設定してアプリケーション側で利用できる仕組みを提供しています。ただし、パスワードやAWSアクセスキーなどの機密情報を環境変数としてプレーンテキストで取り扱うことは推奨されていません。ここでは機密情報の扱いやすさという軸で比較してみます。
Lambda
は AWS KMS による環境変数の暗号化と復号化の仕組みを提供しています。ただし、暗号化の設定がCloudFormationに対応していなかったり、アプリケーション側に復号化の実装が必要だったりと使い勝手がいいとは決して言えません。
Fargate
は Parameter Store に登録したSecureStringをタスク起動時に復号化して、環境変数に設定してくれるという非常に便利な機能を提供してくれています。これだけでもFargateを選択する価値があると私は思っています。
※ 詳しくは Developers.IO - ECSでごっつ簡単に機密情報を環境変数に展開できるようになりました! を参照してください。
Batch
は機密情報を取り扱うための仕組みが今のところサポートされていないようです。自前でParameter Storeや AWS Secrets Manager に登録した機密情報をアプリケーション側で復号化して使用するといった作り込みが必要になります。
スケジューリング設定
3サービス共通して、CloudWatch Eventsと紐づけることで、定期実行するためのスケジューリング設定が可能です。またCloudFormationでのスケジューリング設定も可能です。(長らくBatch
はスケジュール設定がCFn未対応でしたが、本記事を執筆している時点では有難いことに対応していました)
ロギング設定
Lambda
はアプリケーションでの標準出力、エラー出力をCloudWatch Logsに出力することが可能です。ロググループは/aws/lambda/<function name>
固定となります。
Fargate
にはログドライバーの仕組みがあり、これによって柔軟にログ出力先を設定することができます。awslogsログドライバーを使用すれば、アプリケーションでの標準出力、エラー出力をCloudWatch Logsに出力することが可能です。CloudWatch Logsのロググループ名も任意に指定できます。
※ 詳しくは AWS Fargate - ログ記録 を参照してください。
Batch
はアプリケーションでの標準出力、エラー出力をCloudWatch Logsに出力することが可能です。ロググループは/aws/batch/job
固定となります。
まとめ
ユースケースに合わせてどのサービスを利用するかを決めるべきですが、上記のメリット、デメリットを踏まえて、定期バッチ環境の構築における取捨選択のポイントを独断でまとめてみました。
サービス | 取捨選択のポイント |
---|---|
Lambda | 使用するライブラリが限定的であり、小規模処理をとりあえず動かしたい場合 |
Fargate | 機密情報を必要とするような、多少手の込んだ中規模処理を動かしたい場合 |
Batch | 機械学習のようなマシンリソースが潤沢に必要な大規模処理を動かしたい場合 |
※ あくまで定期バッチをテーマにしたまとめとして取り扱ってください。(本記事では各サービスの備えている利点を全て取り上げた訳ではありません)
サンプル
定期バッチ環境を構築するサンプルコードを紹介して終わりにします。
今回共通して、AWS SAM用のテンプレートを使用します。(Lambda以外はCFnのテンプレートとほぼ同一です)
Lambdaサンプル
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'Lambda Sample Settings'
Parameters:
BaseName:
Type: String
Default: lambda-sample
LambdaEnv:
Type: String
AllowedValues: [dev, prod]
Default: dev
LambdaSubnetId:
Type : AWS::EC2::Subnet::Id
LambdaSecurityGroupId:
Type: AWS::EC2::SecurityGroup::Id
Resources:
LambdaFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub ${BaseName}-function
Handler: handler.sample
Runtime: ruby2.5
Role: !GetAtt LambdaIamRole.Arn
Environment:
Variables:
LAMBDA_ENV: !Ref LambdaEnv
VpcConfig:
SubnetIds:
- !Ref LambdaSubnetId
SecurityGroupIds:
- !Ref LambdaSecurityGroupId
Events:
SampleEvent:
Type: Schedule
Properties:
Name: !Sub ${BaseName}-event1
Schedule: cron(0 3 1 * ? *)
LambdaIamRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
LambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${LambdaFunction}
VPC内でLambda関数を実行するシンプルなサンプルです。 AWS::Logs::LogGroup を明示的に指定しなくても実行時にロググループを作成してくれますが、このようにCloudFormationのスタックと紐づけることで、個別に管理する必要がなくなるのでおすすめです。
以下の通り、事前にインストールしたsamコマンドを使用してデプロイします。サブネットIDやセキュリティグループのハードコーディングは避けたいので、parameter-overrides
オプションで渡しています。
$ sam package --template-file lambda_template.yml --output-template-file packaged-template.yml --s3-bucket sam-sample
$ sam deploy --template-file packaged-template.yml --stack-name lambda-sample --capabilities CAPABILITY_IAM --parameter-overrides LambdaSubnetId=subnet-***************** LambdaSecurityGroupId=sg-*****************
Fargateサンプル
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'Fargate Sample Settings'
Parameters:
BaseName:
Type: String
Default: fargate-sample
FargateEnv:
Type: String
AllowedValues: [dev, prod]
Default: dev
ECSSubnetId:
Type : AWS::EC2::Subnet::Id
ECSSecurityGroupId:
Type: AWS::EC2::SecurityGroup::Id
Resources:
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub ${BaseName}-cluster
ECSTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Cpu: 2048
Memory: 4096
Family: !Sub ${BaseName}-task
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
NetworkMode: awsvpc
ContainerDefinitions:
- Name: !Sub ${BaseName}-container
Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${BaseName}:latest
WorkingDirectory: /root/workspace/
Environment:
- Name: FARGATE_ENV
Value: !Ref FargateEnv
Secrets:
- Name: AWS_ACCESS_KEY
ValueFrom: /fargate-secrets/AWS_ACCESS_KEY
- Name: AWS_SECRET_KEY
ValueFrom: /fargate-secrets/AWS_SECRET_KEY
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref BaseName
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: !Ref BaseName
TaskScheduleEvents1:
Type: AWS::Events::Rule
Properties:
Name: !Sub ${BaseName}-event1
ScheduleExpression: cron(0 3 1 * ? *)
State: ENABLED
Targets:
- Id: !Sub ${BaseName}-target
Arn: !GetAtt ECSCluster.Arn
RoleArn: !GetAtt ECSEventRole.Arn
Input: !Sub '{"containerOverrides":[{"name":"${BaseName}-container","command":["ruby", "./handler.rb"]}]}'
EcsParameters:
TaskDefinitionArn: !Ref ECSTaskDefinition
TaskCount: 1
LaunchType: FARGATE
NetworkConfiguration:
AwsVpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- !Ref ECSSecurityGroupId
Subnets:
- !Ref ECSSubnetId
ECSTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
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
- arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess
ECSEventRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceEventsRole
FargateLogsGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Ref BaseName
コンテナのWORKDIR直下にあるhandler.rb
バッチを実行するサンプルです。
あらかじめ用意したDockerイメージのURIを AWS::ECS::TaskDefinition ContainerDefinition のImage
で設定しています。また、機密情報であるAWS_ACCESS_KEY
とAWS_SECRET_KEY
をParameter Storeに事前に設定して、環境変数として利用する想定です。
ポイント
バッチコマンドを AWS::Events::Rule Target のcontainerOverrides
で上書き設定しています。AWS::Events::Rule
を複数定義することで、コンテナ上にある様々な定期バッチのスケジューリングが同一テンプレートファイルで可能になります。
こちらも同様にsamコマンドを使用してデプロイします。
$ sam package --template-file fargate_template.yml --output-template-file packaged-template.yml --s3-bucket sam-sample
$ sam deploy --template-file packaged-template.yml --stack-name fargate-sample --capabilities CAPABILITY_IAM --parameter-overrides ECSSubnetId=subnet-***************** ECSSecurityGroupId=sg-*****************
Batchサンプル
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'Batch Sample Settings'
Parameters:
BaseName:
Type: String
Default: batch-sample
BatchEnv:
Type: String
AllowedValues: [dev, prod]
Default: dev
BatchSubnetId:
Type : AWS::EC2::Subnet::Id
BatchSecurityGroupId:
Type: AWS::EC2::SecurityGroup::Id
EC2KeyPair:
Type: AWS::EC2::KeyPair::KeyName
EC2AmiId:
Type: AWS::EC2::Image::Id
Resources:
BatchComputeEnv:
Type: AWS::Batch::ComputeEnvironment
Properties:
Type: MANAGED
ServiceRole: !GetAtt BatchServiceRole.Arn
ComputeEnvironmentName: !Sub ${BaseName}-compute-env
ComputeResources:
MaxvCpus: 2
MinvCpus: 0
Subnets:
- !Ref BatchSubnetId
SecurityGroupIds:
- !Ref BatchSecurityGroupId
InstanceRole: !GetAtt EcsInstanceProfile.Arn
Ec2KeyPair: !Ref EC2KeyPair
Type: EC2
InstanceTypes:
- r5.large
ImageId: !Ref EC2AmiId
State: ENABLED
BatchJobQueue:
Type: AWS::Batch::JobQueue
Properties:
JobQueueName: !Sub ${BaseName}-job-queue
ComputeEnvironmentOrder:
- Order: 1
ComputeEnvironment: !Ref BatchComputeEnv
State: ENABLED
Priority: 1
BatchJobDefinition:
Type: AWS::Batch::JobDefinition
Properties:
Type: container
JobDefinitionName: !Sub ${BaseName}-job-definition
ContainerProperties:
Command: ["Ref::SciptLang", "Ref::Command"]
Memory: 15000
Vcpus: 2
Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${BaseName}:latest
Environment:
- Name: BATCH_ENV
Value: !Ref BatchEnv
TaskScheduleEvents1:
Type: AWS::Events::Rule
Properties:
Name: !Sub ${BaseName}-event1
ScheduleExpression: cron(0 3 1 * ? *)
State: ENABLED
Targets:
- Id: !Sub ${BaseName}-target
Arn: !Ref BatchJobQueue
RoleArn: !GetAtt BatchEventRole.Arn
Input: '{"Parameters" : {"SciptLang": "ruby", "Command": "./handler.rb"}}'
BatchParameters:
JobDefinition: !Ref BatchJobDefinition
JobName: !Sub ${BaseName}-job
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
EcsInstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
EcsInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref EcsInstanceRole
BatchEventRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: events.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSBatchServiceEventTargetRole
Fargate
と同様にコンテナのWORKDIR直下にあるhandler.rb
バッチを実行するサンプルです。
あらかじめ用意したDockerイメージのURIを AWS::Batch::JobDefinition ContainerProperties のImage
で設定しています。また、マシンリソースが潤沢に必要な大規模処理を想定して、ストレージを拡張したAMI IDを AWS::Batch::ComputeEnvironment ComputeResources のImageId
で設定しています。
ポイント(1)
EC2 vCPUの最小数を AWS::Batch::ComputeEnvironment ComputeResources のMinvCpus
で0に設定しています。これによりバッチ実行時にEC2インスタンスが起動し、バッチ終了後に自動でEC2インスタンスが削除されるというサーバレスな運用を実現します。
ポイント(2)
バッチコマンドを AWS::Batch::JobDefinition ContainerProperties のCommand
で["Ref::SciptLang", "Ref::Command"]
のように参照形式で設定しています。これは AWS::Events::Rule Target のParameters
で実体を設定することで、コンテナ上にある複数の定期バッチのスケジューリングを同一テンプレートファイルで可能にしています。(Fargate
と同じようにcontainerOverrides
でシンプルにバッチコマンドを上書き設定できるかと思ってましたが、難しかったためこのようなハック的なやり方を採用しました)
こちらも同様にsamコマンドを使用してデプロイします。
$ sam package --template-file batch_template.yml --output-template-file packaged-template.yml --s3-bucket sam-sample
$ sam deploy --template-file packaged-template.yml --stack-name batch-sample --capabilities CAPABILITY_IAM --parameter-overrides BatchSubnetId=subnet-***************** BatchSecurityGroupId=sg-***************** EC2KeyPair=********** EC2AmiId=ami-*****************
さいごに
以上で終了です。AWSでサーバレスな定期バッチ環境を作るうえでの、お役に立てれば幸いです。
実際のところ、これまでBatch
の定期バッチ運用に関しては、スケジューリングしたLambda
経由でジョブをサブミットすることで実現してました。今回この記事を執筆するにあたり、CFnに対応しているやん!じゃあ試してみようとなったのですが、思いのほか茨の道で大変でした。なんとか動く状態になって良かったです。
では、またX年後にお会いしましょう。