はじめに
みなさんこんにちは、荒堀と申します。
サーバレスでタスク実行する際の選択肢に、AWS Fargate (以降、Fargate)とAWS Lambda (以降、Lambda)があります。 どちらもコンテナで動かせますので、同じコンテナイメージで動かしてみました。
概要
Fargateにある、Dockerfile内のエントリポイントとコマンドを上書きする機能を使います。
- コンテナイメージを作るときは、Lambda用のエントリポイントとコマンドを指定します。
- Fargateのタスク定義で、Fargateで動かせるようにエントリポイントとコマンドの上書き設定をします。
この記事では、以下を作っていきます。
- 文字列を標準出力するだけのコンテナを作ります。
- これをLambdaとFargateの両方で動かします。
- 文字列がCloudWatch Logsに出力されることを確認します。
- コンテナのベースイメージはUbuntuを使います。
- Lambda用のイメージではないです。
- PUSH用のECRを作ります。
- Fargate実行用にVPCを作ります。
Lambdaで動かすコンテナイメージの作り方について
はじめに少し、Lambda用のコンテナイメージの作り方について説明します。
まずベースイメージに、AWSが公開しているLambda用のイメージを使う方法と、独自のベースイメージを使う方法があります。 今回は独自のベースイメージを使う方法になります。
独自のベースイメージを使う場合は、コードからLambdaを操作するために Runtime Interface Client (RIC) をダウンロードしてエントリポイントに指定します。 以下はPython用のRICですが、各言語用に用意されています。
Pythonの場合はRICをダウンロードして、Dockerfileのエントリポイントとコマンドに以下のように指定します。
ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ] CMD [ "app.handler" ]
この仕組みのため、エントリポイントとコマンドを変更することでLambda、またはFargateで動かすように変更できます。
環境
言語はPythonになります。コマンド実行用に、Cloud9(t2.micro, Amazon Linux 2)を使います。
リージョンはバージニア北部(us-east-1)でやっています。東京リージョンでも問題なくできます。
コンテナイメージ作成
まずはprintするだけのコンテナイメージを作成します。
# プロジェクトディレクトリ作成 mkdir sample-common-container && cd sample-common-container # リージョンとアカウントIDを環境変数にセット REGION="us-east-1" ACCOUNTID=$(aws sts get-caller-identity --output text --query Account) IMAGENAME="image-common-container" touch app.py touch Dockerfile
app.pyの中身は以下です。Lambdaを実行するだけなら2行目まであればよいですが、Fargateでも実行できるように4行目以降を追加しています。
def handler(event, context): print("hogehoge") if __name__ == "__main__": handler(None,None)
Dockerfileの中身は以下です。Lambda用に作ります。
FROM ubuntu:20.04 RUN apt-get update && apt-get install -y python3-pip RUN mkdir /function && \ pip install --target /function awslambdaric COPY app.py /function/ WORKDIR /function ENTRYPOINT [ "/usr/bin/python3", "-m", "awslambdaric" ] CMD [ "app.handler" ]
Buildします。
docker build -t ${IMAGENAME} .
ECRにPUSH
CloudFormationでECRを作って、コンテナにPUSHします。
# ECRリポジトリ作成CFn touch createECRRepository.yaml
AWSTemplateFormatVersion: "2010-09-09" Parameters: RepositoryName: Type: String Resources: TestEcrPoc: Type: AWS::ECR::Repository Properties: RepositoryName: !Ref RepositoryName Outputs: Task1RepositoryUri: Value: !GetAtt TestEcrPoc.RepositoryUri
# ECRにレポジトリ作成 REPSTACKNAME="create-ecrrepo-common-container" REPOSITORYNAME="test-common-container" aws cloudformation create-stack --stack-name ${REPSTACKNAME} \ --template-body file://createECRRepository.yaml \ --region ${REGION} \ --parameters \ ParameterKey=RepositoryName,ParameterValue=${REPOSITORYNAME} # イメージにタグ付与 TAGNAME=`aws cloudformation describe-stacks --stack-name ${REPSTACKNAME} --query "Stacks[].Outputs[?OutputKey=='Task1RepositoryUri'].[OutputValue]" --output text`:latest docker tag ${IMAGENAME}:latest ${TAGNAME} # 認証 aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com # 作ったイメージをPUSH docker push ${TAGNAME}
Lambdaで動かす
PUSHしたイメージでLambdaを動かします。Lambdaと一緒にIAMロールとCloudWatchのロググループも作っています。
# Lambda作るCFn touch createLambda.yaml
AWSTemplateFormatVersion: "2010-09-09" Parameters: LambdaFunctionName: Type: String ImageUri: Type: String Resources: ######################################################## ### Log Group ######################################################## FunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${LambdaFunctionName}" RetentionInDays: 3653 ######################################################## ### IAM Role ######################################################## FunctionRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "for-lambdafunction-${LambdaFunctionName}" AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: '/service-role/' Policies: # CloudWatch - PolicyName: write-cloudwatchlogs PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaFunctionName}:*" ######################################################## ### Lambda Function ######################################################## TargetFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Ref LambdaFunctionName Role: !GetAtt FunctionRole.Arn PackageType: Image Code: ImageUri: !Ref ImageUri
CloudFormationで作成した後、Lambdaを実行します。
# CFnのパラメータに使う文字列を生成 DIGEST=`aws ecr list-images --repository-name ${REPOSITORYNAME} --region ${REGION} --out text --query "imageIds[?imageTag=='latest'].imageDigest"` IMAGEURI=`aws cloudformation describe-stacks --stack-name ${REPSTACKNAME} --query "Stacks[].Outputs[?OutputKey=='Task1RepositoryUri'].[OutputValue]" --output text`@${DIGEST} FUNCSTACKNAME="tempecrlambda" LAMBDANAME="func1-container" # Lambda作成。IAM作るのでcapabilities指定 aws cloudformation create-stack --stack-name ${FUNCSTACKNAME} \ --template-body file://createLambda.yaml \ --region ${REGION} \ --parameters \ ParameterKey=LambdaFunctionName,ParameterValue=${LAMBDANAME} \ ParameterKey=ImageUri,ParameterValue=${IMAGEURI} \ --capabilities CAPABILITY_NAMED_IAM # 実行 aws lambda invoke --function-name ${LAMBDANAME} --region ${REGION} output
CloudWatchに出力されていることを確認します。
printに指定した文字列が出力されていました。
Fargateで動かす
ECRにPUSHしたイメージは変えず、そのままFargateで実行します。
VPC作成
まずVPCとパブリックサブネット、セキュリティグループを作ります。 パブリックサブネットとセキュリティグループのIDは、ECSタスクを実行する際に使いますので出力しておきます。
touch createVpc.yaml
AWSTemplateFormatVersion: '2010-09-09' Parameters: VpcName: Type: String Default: forFargateTask Description: Name of the VPC VpcCIDR: Type: String Default: 10.70.0.0/16 Description: CIDR block for the VPC Resources: VPC: Type: 'AWS::EC2::VPC' Properties: CidrBlock: !Ref VpcCIDR EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Ref VpcName InternetGateway: Type: 'AWS::EC2::InternetGateway' Properties: Tags: - Key: Name Value: !Sub ${VpcName}-igw VPCGatewayAttachment: Type: 'AWS::EC2::VPCGatewayAttachment' Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway PublicSubnet: Type: 'AWS::EC2::Subnet' Properties: VpcId: !Ref VPC CidrBlock: !Select [ 0, !Cidr [ !Ref VpcCIDR, 24, 8 ] ] MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub ${VpcName}-subnet PublicRouteTable: Type: 'AWS::EC2::RouteTable' Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${VpcName}-rtb-public PublicRoute: Type: 'AWS::EC2::Route' DependsOn: VPCGatewayAttachment Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: '0.0.0.0/0' GatewayId: !Ref InternetGateway SubnetRouteTableAssociation: Type: 'AWS::EC2::SubnetRouteTableAssociation' Properties: SubnetId: !Ref PublicSubnet RouteTableId: !Ref PublicRouteTable SecurityGroup: Type: AWS::EC2::SecurityGroup Properties: VpcId: !Ref VPC GroupDescription: "for Fargate Task" Outputs: SubnetForFargateTask: Value: !Ref PublicSubnet SgForFargateTask: Value: !Ref SecurityGroup
# VPC作成 VPCSTACKNAME="create-vpc-common-container" VPCNAME="forCommonContainer" VPCCIDR="10.80.0.0/16" aws cloudformation create-stack --stack-name ${VPCSTACKNAME} \ --template-body file://createVpc.yaml \ --region ${REGION} \ --parameters \ ParameterKey=VpcName,ParameterValue=${VPCNAME} \ ParameterKey=VpcCIDR,ParameterValue=${VPCCIDR}
ECS作成
ECSクラスターとタスク定義を作ります。一緒にIAMロールとロググループも作っています。
touch createCommonContainerEcs.yaml
ENTRYPOINTとCMDの上書きは、タスク定義のところで行います。 パラメータ例は公式にあります。
AWSTemplateFormatVersion: "2010-09-09" Parameters: ClusterName: Type: String Default: clusterForStandalone TaskName: Type: String Default: taskForStandalone ImageUri: Type: String Resources: ######################################################## ### Ecs Cluster ######################################################## EcsCluster: Type: 'AWS::ECS::Cluster' Properties: ClusterName: !Ref ClusterName CapacityProviders: - FARGATE - FARGATE_SPOT ClusterSettings: - Name: containerInsights Value: disabled DefaultCapacityProviderStrategy: - CapacityProvider: FARGATE Weight: 1 - CapacityProvider: FARGATE_SPOT Weight: 1 Configuration: ExecuteCommandConfiguration: Logging: DEFAULT ServiceConnectDefaults: Namespace: !Ref ClusterName ######################################################## ### IAM Role ######################################################## TaskExecutionRole: 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 ######################################################## ### Log Group ######################################################## TaskLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/ecs/${TaskName}" RetentionInDays: 3653 ######################################################## ### Ecs Task ######################################################## EcsTask: Type: AWS::ECS::TaskDefinition Properties: ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn RequiresCompatibilities: - FARGATE NetworkMode: awsvpc Cpu: 256 Memory: 512 ContainerDefinitions: - Name: !Ref TaskName Image: !Ref ImageUri EntryPoint: - "/usr/bin/python3" Command: - "app.py" LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref TaskLogGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: ecs Outputs: EcsClusterArnForFargateTask: Value: !GetAtt EcsCluster.Arn EcsTaskDefArnForFargateTask: Value: !GetAtt EcsTask.TaskDefinitionArn
CloudForamtionを実行して作っていきます。
ECSSTACKNAME="create-ecs-common-container" CLUSTERNAME="clusterForCommonContainer" TASKNAME="taskForCommonContainer" IMAGEURI=`aws cloudformation describe-stacks --stack-name ${REPSTACKNAME} --query "Stacks[].Outputs[?OutputKey=='Task1RepositoryUri'].[OutputValue]" --output text`:latest aws cloudformation create-stack --stack-name ${ECSSTACKNAME} \ --template-body file://createCommonContainerEcs.yaml \ --region ${REGION} \ --parameters \ ParameterKey=ClusterName,ParameterValue=${CLUSTERNAME} \ ParameterKey=TaskName,ParameterValue=${TASKNAME} \ ParameterKey=ImageUri,ParameterValue=${IMAGEURI} \ --capabilities CAPABILITY_NAMED_IAM
アカウントで初めて作成しようとするときは、以下のようなエラーが出る模様です。CloudFormationスタックを削除してもう一度作り直ししてください。
Resource handler returned message: "Error occurred during operation 'CreateCluster SDK error: Service Unavailable. Please try again later. (Service: AmazonECS; Status Code: 500; Error Code: ServerException; Request ID: xxxxxxxx; Proxy: null)'."
ECSタスク実行
作成したECSタスクを実行します。
# ECSの情報 ECSCLUSTER_ARN=`aws cloudformation describe-stacks --stack-name ${ECSSTACKNAME} --query "Stacks[].Outputs[?OutputKey=='EcsClusterArnForFargateTask'].[OutputValue]" --output text` ECSTASKDEF_ARN=`aws cloudformation describe-stacks --stack-name ${ECSSTACKNAME} --query "Stacks[].Outputs[?OutputKey=='EcsTaskDefArnForFargateTask'].[OutputValue]" --output text` # VPCの情報 SUBNET_ID=`aws cloudformation describe-stacks --stack-name ${VPCSTACKNAME} --query "Stacks[].Outputs[?OutputKey=='SubnetForFargateTask'].[OutputValue]" --output text` SG_ID=`aws cloudformation describe-stacks --stack-name ${VPCSTACKNAME} --query "Stacks[].Outputs[?OutputKey=='SgForFargateTask'].[OutputValue]" --output text` NETWORK_CONFIG="awsvpcConfiguration={subnets=[${SUBNET_ID}],securityGroups=[${SG_ID}],assignPublicIp=ENABLED}" aws ecs run-task \ --cluster ${ECSCLUSTER_ARN} \ --task-definition ${ECSTASKDEF_ARN} \ --network-configuration "${NETWORK_CONFIG}" \ --launch-type FARGATE
作成したCloudWatchに出力されていることが確認できます。
コンソールからDocker設定を変更
エントリポイントとコマンドをCloudFormationで指定しましたが、コンソールでも設定できます。
既存の設定を変更する際は 新しいリビジョンの作成 で指定できます。
おわりに
同じコンテナイメージで、FargateとLambdaを実行してみました。 短い処理はLambdaで、長い処理はFargateで、というように分岐することがあるかと思います。 共通のコンテナイメージで実行できるので、コードの管理が容易になることが期待できます。
この記事がどなたかのお役に立てれば幸いです。