AWS FargateとAWS Lambdaで同じコンテナイメージを使う

記事タイトルとURLをコピーする

はじめに

みなさんこんにちは、荒堀と申します。

サーバレスでタスク実行する際の選択肢に、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ですが、各言語用に用意されています。

github.com

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の上書きは、タスク定義のところで行います。 パラメータ例は公式にあります。

docs.aws.amazon.com

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で、というように分岐することがあるかと思います。 共通のコンテナイメージで実行できるので、コードの管理が容易になることが期待できます。

この記事がどなたかのお役に立てれば幸いです。