AWS Lambda

Lambdaにコンテナ形式でデプロイしてみた

概要

こんにちは、ディーネットの牛山です。

AWS Lambdaにおいて、コンテナー形式がサポートされていることは知れていることかもしれませんが、筆者自身、従来のZip形式のデプロイ経験しかないため、学習ガテラ紹介します。

前置き

以下、コンテナー形式とZip形式の主だった違いを表で示します。

形式 ストレージ場所 ストレージサイズ上限 アーティファクトサイズ上限 Layer 対応 コード署名
Zip S3 75 GB 50 MB (展開前), 250 MB (展開後) 対応 対応
コンテナー ECR テラバイト 10 GB 未対応 未対応

Zip形式の場合、デプロイサイズが50MBに制限されますが、コンテナー形式では10GBまでサポートされるため、依存関係の多いパッケージ等をそのままでデプロイすることが可能になります。

以下のような形で開発者がコードを作成し、コンテナイメージを作成後、ECRにアップロードし、AWS LambdaはECRにアップロードされたコンテナイメージをロードし、実行する形となります。

architecture

それでは、実際にコードを準備し、コンテナイメージを作成 ~ 実行までやってみます。

前提条件

  • AWS CLI IAMユーザをセットアップ済みであること。
  • AWS CLI認証情報を設定済みであること。
  • python-lambda-localインストール済みであること。
  • dockerがインストール済みであること。

プロジェクト作成

docker関連やLambda実行用プログラムなどをまとめたいので任意のディレクトリに好きな名前のディレクトリを作成します。

mkdir python-lambda-rds

Lambdaプログラム作成

Amazon RDSサービスから作成済みのRDS DBインスタンスより指定されたタグが付与されたRDS DB識別子を取得し、json形式で返す処理を以下の通りのファイル名で、先ほど作成したプロジェクト直下に作成します。

今回は、RDS DBインスタンスタグに「キー:Env、値:Prod」が指定されているRDS DBインスタンスを取得します。

vi python_lambda_rds.py

import pprint
import json
import boto3

my_rds = boto3.client( 'rds' )
my_pprint = pprint.PrettyPrinter( indent = 4 )

def lambda_handler( event, context ):
    ret = rds_db_instance_identifier( 'Env', 'Prod' )
    ret_message = ret

    if len( ret_message ) == 0:
        ret_message = '指定タグが付与されているRDSはありませんでした。'

    return {
        "statusCode": 200,
        "body": json.dumps(
            {
                "message": ret_message,
            },
            ensure_ascii = False
        ),
    }

def rds_describe_db_instances_arn( ):
    ret = [ ]
    rds_describe_db_instances = my_rds.describe_db_instances( )

    for db_instances in rds_describe_db_instances[ 'DBInstances' ]:
        ret.append( db_instances[ 'DBInstanceArn' ] )

    return ret

def rds_list_tags_search_match_arn( _tag_key_name: str, _tag_key_value: str ):
    ret = [ ]
    arn_list = rds_describe_db_instances_arn( )

    for index, arn in enumerate( arn_list ):
        tag = my_rds.list_tags_for_resource( ResourceName = arn )

        for tag_y in tag[ 'TagList' ]:
            if tag_y[ 'Key' ] == _tag_key_name and tag_y[ 'Value' ] == _tag_key_value:
                ret.append( arn )

    return ret

def rds_db_instance_identifier( _tag_key_name: str, _tag_key_value: str ):
    ret = [ ]
    arn_list = rds_list_tags_search_match_arn( _tag_key_name, _tag_key_value )
    rds_describe_db_instances = my_rds.describe_db_instances( )

    for db_instances in rds_describe_db_instances[ 'DBInstances' ]:
        for index, arn in enumerate( arn_list ):
            if arn == db_instances[ 'DBInstanceArn' ]:
                ret.append( db_instances[ 'DBInstanceIdentifier' ] )

    return ret

Lambdaプログラムローカルテスト

Lambdaプログラムをいきなりデプロイするのは怖いので事前に動作確認します。

python-lambda-localによるテストと「AWS Lambda Runtime Interface Emulator (RIE)」による2パターンのテストを実施します。

手始めにpython-lambda-localによるテストを実施します。

事前準備1

テストに必要なファイルを作成します。

以下、ファイル名でプロジェクト直下へ、空のjsonファイルを作成します。

vi event.json

{
}

以下、コマンドでテスト用のAmazon RDS DBインスタンスを作成します。

aws rds describe-db-subnet-groups | jq '.DBSubnetGroups[].DBSubnetGroupName' コマンド、 aws ec2 describe-security-groups | jq コマンドを使用して、 {DB サブネットグループ}{DB サブネットグループに紐付いたセキュリティID} 部分を各自の環境に合わせて置換し実行します。

パスワード、ユーザ名についても任意で設定します。

aws rds create-db-cluster --db-cluster-identifier test-hoge-rds \
--engine aurora-mysql \
--engine-version 8.0 --master-username {ユーザ名} --master-user-password {パスワード} \
--db-subnet-group-name {DB サブネットグループ} --vpc-security-group-ids {DB サブネットグループに紐付いたセキュリティID} \
--tags Key=Env,Value=Prod
aws rds create-db-instance --db-instance-identifier test-hoge-rds \
--db-instance-class db.t3.medium \
--engine aurora-mysql \
--tags Key=Env,Value=Prod \
--db-cluster-identifier test-hoge-rds

Amazon RDSインスタンスのデプロイには時間が掛かりますので数分程度待ちます。

ローカルテスト1

以下の通り、実行し、結果に「test-hoge-rds」が出てくることを確認します。

python-lambda-local -f lambda_handler python_lambda_rds.py event.json

・
・[中略]
・
{'statusCode': 200, 'body': '{"message": ["test-hoge-rds"]}'}

事前準備2

dockerイメージを作成したいので、Dockerfileを作成します。

「{AWSリージョン}」箇所は、Amazon RDSインスタンスがデプロイされているリージョンに変更してください。

ECRのAWS公式の公開イメージを使用するようにしていますが、「amazon/aws-lambda-python:3.8」のようにdocker hubのイメージを参照することも可能になっております。

vi Dockerfile

FROM public.ecr.aws/lambda/python:3.8
ENV AWS_DEFAULT_REGION {AWSリージョン}
COPY python_lambda_rds.py ./
CMD ["python_lambda_rds.lambda_handler"]

Lambdaイメージをエミュレートするにあたり、認証情報を読み込ませる必要があるので以下、隠しファイルで定義します。

アクセスキーおよびシークレットキーを任意で置換します。

vi .credential

AWS_ACCESS_KEY_ID={AWSアクセスキー}
AWS_SECRET_ACCESS_KEY={AWSシークレットキー}

ここまでで以下ファイルが作成されていれば問題ありません。

python-lambda-rds
├── .credential
├── Dockerfile
├── event.json
└── python_lambda_rds.py

「lambda_container_demo」というコンテナーイメージ名でDockerfileをビルドします。

docker build -t lambda_container_demo .

Sending build context to Docker daemon  9.728kB
Step 1/4 : FROM public.ecr.aws/lambda/python:3.8
 ---> 3e09d1c79c78
Step 2/4 : ENV AWS_DEFAULT_REGION {AWSリージョン}
 ---> Using cache
 ---> ebfb573e5258
Step 3/4 : COPY python_lambda_rds.py ./
 ---> 53cb46096055
Step 4/4 : CMD ["python_lambda_rds.lambda_handler"]
 ---> Running in e3f454761dfd
Removing intermediate container e3f454761dfd
 ---> 8dbdb3397899
Successfully built 8dbdb3397899
Successfully tagged lambda_container_demo:latest

以下、コマンドでコンテナー内部に入ることが可能なので興味のある方は見てみてください。

docker run -it --entrypoint '' lambda_container_demo:latest /bin/sh

ローカルテスト2

以下、コマンドでビルドしたイメージをエミュレートします。

docker run --env-file ./.credential -p 9000:8080 lambda_container_demo:latest

別タブを開き、エミュレート中のコンテナーに対してcurlアクセスし、「test-hoge-rds」がでることを確認します。

curl -sS -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' | jq

{
  "statusCode": 200,
  "body": "{\"message\": [\"test-hoge-rds\"]}"
}

別タブでエミュレート中のコンテナーにcurlアクセス時のログが赤枠で囲った部分に出ていますね。

container_curl

Lambdaデプロイ

ローカルテストで問題ないことが確認できましたので実際に、Amazon ECRにコンテナーイメージをアップロードし、Lambdaからイメージをロードし実行してみます。

Amazon ECRにアップロード時、使用する設定を変数に入れておきます。

REGION=$(aws configure get region)
 ⇒後ほど使用するので変数にリージョンを格納します。

ACCOUNTID=$(aws sts get-caller-identity --output text --query Account)
 ⇒後ほど使用するので変数に、アカウントIDを格納します。

IAMロール・ポリシー

デプロイ前にIAMロール等を作成し付与する必要がありますので以下の通り実施します。

aws iam create-role --role-name python_lambda_rds-role \
--assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'

 ⇒python_lambda_rds-roleロールをLambdaサービスを信頼する形で作成します。

aws iam attach-role-policy --role-name python_lambda_rds-role --policy-arn "arn:aws:iam::aws:policy/AmazonRDSReadOnlyAccess"
 ⇒作成した「python_lambda_rds-role」にAmazon RDSのリードオンリーポリシーを付与します。

ROLE_ARN=arn:aws:iam::${ACCOUNTID}:role/python_lambda_rds-role
 ⇒「python_lambda_rds-role」ロールをARN形式で変数に入れておきます。

レポジトリ作成・アップロード

Amazon ECRに「lambda_container_demo」という名前でレポジトリを作成します。

aws ecr create-repository --repository-name lambda_container_demo

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:{AWSリージョン}:{AWSアカウントID}:repository/lambda_container_demo",
        "registryId": "{AWSアカウントID}",
        "repositoryName": "lambda_container_demo",
        "repositoryUri": "{AWSアカウントID}.dkr.ecr.{AWSリージョン}.amazonaws.com/lambda_container_demo",
        "createdAt": "2022-10-02T23:30:33+09:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}

タグ付与

docker images

REPOSITORY                                         TAG        IMAGE ID       CREATED         SIZE
lambda_container_demo                              latest     8dbdb3397899   5 hours ago     652MB

 ⇒「lambda_container_demo:latest」でコンテナイメージが定義されています。

docker tag lambda_container_demo:latest \
  ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/lambda_container_demo:latest

 ⇒「lambda_container_demo:latest」をAmazon ECR形式のタグで作成します。

docker images

REPOSITORY                                                                TAG        IMAGE ID       CREATED         SIZE
{AWSアカウントID}.dkr.ecr.{AWSリージョン}.amazonaws.com/lambda_container_demo   latest     8dbdb3397899   5 hours ago     652MB
lambda_container_demo                                                     latest     8dbdb3397899   5 hours ago     652MB

 ⇒先ほど、作成したECR形式タグのコンテナーイメージが作成されたことを確認します。

アップロード

aws ecr get-login-password | docker login --username AWS --password-stdin ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com
 ⇒Amazon ECRにログインします、「Login Succeeded」になることを確認します。

Amazon RCRにローカルのコンテナーイメージをアップロードします。

push後に「digest」が出力されるので後ほど変数に入れます。

docker push ${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/lambda_container_demo:latest

The push refers to repository [{AWSアカウントID}.dkr.ecr.{AWSリージョン}.amazonaws.com/lambda_container_demo]
ae840b67d85a: Pushed 
8c5eab38375e: Pushed 
f2f11a6d76dc: Pushed 
c64645c521e9: Pushed 
34f81a365368: Pushed 
415afd144173: Pushed 
1718126460e1: Pushed 
latest: digest: sha256:xxx size: 1789

Lambda関数作成・実行

DIGEST=$(aws ecr list-images --repository-name lambda_container_demo --out text --query 'imageIds[?imageTag==latest].imageDigest')
 ⇒ダイジェストを格納します。

Lambda関数作成

architecturesの項目は、dockerイメージビルド環境が、arm64環境でしたので、各自、ビルド環境に合わせて作成します。

aws lambda create-function \
--function-name lambda-container-demo  \
--package-type Image \
--code ImageUri=${ACCOUNTID}.dkr.ecr.${REGION}.amazonaws.com/lambda_container_demo@${DIGEST} \
--role ${ROLE_ARN} \
--architectures arm64

上記、コマンド実施後、以下出力のようになることを確認します。

{
    "FunctionName": "lambda-container-demo",
    "FunctionArn": "arn:aws:lambda:{AWSリージョン}:{AWSアカウントID}:function:lambda-container-demo",
    "Role": "arn:aws:iam::{AWSアカウントID}:role/python_lambda_rds-role",
    "CodeSize": 0,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2022-10-02T14:54:03.670+0000",
    "CodeSha256": "xxx",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "9500c55c-7626-4de6-bf7e-f0a474815cdd",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Image",
    "Architectures": [
        "arm64"
    ],
    "EphemeralStorage": {
        "Size": 512
    }
}

Lambda関数実行

いよいよ、AWS上にデプロイしたLambda関数を実行します。

aws lambda invoke --function-name lambda-container-demo output ; cat output

{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "body": "{\"message\": [\"test-hoge-rds\"]}"}

 ⇒ローカルテストした時と同様に、「test-hoge-rds」が出力されます。

テスト用として作成したAmazon RDSインスタンスとクラスターを削除します。

「キー:Env、値:Prod」のRDSインスタンスタグのRDSインスタンスがないときの挙動として「"指定タグが付与されているRDSはありませんでした。」となることを確認しましょう。

aws rds delete-db-instance --db-instance-identifier test-hoge-rds
 ⇒Amazon RDSインスタンスの削除。

aws rds delete-db-cluster --db-cluster-identifier test-hoge-rds --skip-final-snapshot
 ⇒Amazon RDSクラスターの削除。

削除後の挙動も想定したものとなっていることが確認できました。

aws lambda invoke --function-name lambda-container-demo output ; cat output | jq

{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
{
  "statusCode": 200,
  "body": "{\"message\": \"指定タグが付与されているRDSはありませんでした。\"}"
}

まとめ

コンテナー形式のLambdaは試したことがなかったので勉強になりました。

環境の差違など吸収できるので便利ですね。

返信を残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA