コンテナイメージを利用した AWS Lambda 関数が Inactive 状態になる実例と対策

AWS

2024.3.8

Topics

はじめに

みなさん、こんにちは。データ分析基盤開発チームのdut-liuです。
社内・社外プロジェクトにて多くの方がAWS Lambdaを利用していると思います。
今回は社内プロジェクトを通じて発見した、AWS Lambdaのある特性について共有させていただきます。

タイトルを見た人はおそらくこのような疑問を感じると思います。

  • コンテナイメージを利用したAWS Lambdaとはなにか
  • なぜAWS Lambdaにコンテナイメージを利用するのか
  • Inactive状態とはどのような状態か

上記の疑問の説明とその状態に関する調査内容について共有します。
本記事では、Dockerを使用していますが、Docker自体の説明は省略させていただきます。
なお、AWS CloudFormationおよびAWS Step Functionsに関するサンプルコードがあるため、それらの知識を持っていることを前提として読んでいただければ嬉しいです。

1.コンテナイメージを利用したAWS Lambdaとは

これから、AWS Lambdaのデプロイパッケージの種類、およびコンテナイメージについて説明します。

AWS Lambdaのデプロイパッケージは【.zipファイルアーカイブ】と【コンテナイメージ】の2種類があります。
.zipファイルアーカイブの場合は、AWSコンソール上でAWS Lambdaのコードを直接変更することができます。
一方、コンテナイメージの場合は、.zipファイルアーカイブのようにAWSコンソール上で直接に変更することができません。その代わりに、ローカル環境でコンテナイメージ内のコードを修正し、CLIコマンドを使って更新します。

.zip形式では最大ボリューム250MBまでデプロイ可能なのに比べて、コンテナ形式では最大10GBというメリットがあるため、開発の際にはコンテナイメージを利用したAWS Lambda関数を採用しています。
コンテナイメージを利用したAWS Lambdaのデプロイパッケージを作成する方法については、以下のサイトをご参照ください。
(英文ドキュメント)
Working with Lambda container images

次に、コンテナイメージを利用したAWS Lambdaの【Inactive】状態の説明です。

数週間にわたって関数が呼び出されない場合、AWS Lambda は最適化されたバージョンを再利用し、関数は Inactive 状態に移行します。関数を再度アクティブにするには、関数を呼び出す必要があります。AWS Lambda は最初の呼び出しを拒否し、関数はAWS Lambda がイメージを再最適化するまで Pending 状態に入ります。その後、関数は Active 状態に戻ります。

引用:コンテナイメージとして定義された関数の呼び出し – AWS Lambda (amazon.com)

上記のドキュメントの内容から、コンテナイメージを利用したAWS Lambda関数がInactive状態に入る場合、実行が失敗になることがわかります。AWS Lambda関数を動作させるためには、Active状態に戻す必要があります。
次のセッションでは、前述した社内プロジェクトで発生したエラーについて紹介します。

2.発生したエラーを再現させるためのサンプルリソース構築

2023年11月、AWS Lambda関数において、.zip形式からコンテナイメージ形式に移行する社内プロジェクトを行いました。コンテナイメージ形式に移行したAWS Lambda関数は、月に1回実行されるバッチ処理に組み込まれていたため、次の自動実行のタイミングまで1か月の期間が空いてしまいました。
再び自動実行すると、下記の図のように、Lambda.AWSLambdaExceptionという例外が発生し、実行に失敗してしまうAWS Lambda関数があることが確認できました。
AWS公式サイトに記載されているLambda.AWSLambdaExceptionについての解決方法が曖昧で、具体的な対処方法の調査に時間がかかりましたが、調査の結果、移行したAWS Lambda関数の中に月に1回だけ実行するAWS Lambda関数が多数あって、それによって前述したInactive状態になってしまったことが原因だとわかりました。

そこでこの記事では、定期実行のケースを想定して用意したサンプルAWS Step FunctionsのステートマシンをベースにInactive状態や対策について解説していきます。

ここからは、上記のエラーを再現していきます。

エラーを再現させるために、以下のサービスを利用しました。

  • AWS Lambda
  • AWS CloudFormation
  • Amazon ECR
  • AWS Step Functions
  • Amazon S3
  • Amazon SNS

設計図は下記のようになります。流れをまとめました。

  1. AWS Lambdaコードの用意

  2. AWS CloudFormationのYAMLパッケージの用意
    (AWS Lambda関連ロール・AWS Lambda本体)

  3. Amazon ECRレポジトリ作成・Dockerイメージ作成・Amazon ECRにプッシュ

  4. AWS CloudFormationスタック作成・AWS Lambdaコードに紐づけ

  5. AWS Step Functionsの作成・実行

まずは下記のAWS Lambda関数のコードを作成します。

 
import boto3
from typing import Any
import os


def lambda_handler(event: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
    bucket_name = os.environ["BUCKET_NAME"]
    output_key = os.environ["OUTPUT_KEY"]
    
    s3_contents = event.get("contents", "")
    s3_client = boto3.client("s3")
    s3_client.put_object(Body=s3_contents, Bucket=bucket_name, Key=output_key)
    return {"statusCode": 200}

今度はAWS CloudFormationのテンプレート、YAMLファイルを作成します。下記のコンテンツを含みます。

  • AWS Lambda(前記作成したコードを含むコンテナイメージ形式)および関連ロール

以下はYAMLファイルの一例です。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: test for lambda image layer

Parameters:

  BucketName:
    Type: String
    Description: Output bucket of writing contents to S3 file
    Default: 【適切な名前を定義してください】

  OutputKey:
    Type: String
    Description: Output key of writing contents to S3 file
    Default: 【テキストファイルの名前*.txtを定義してください】

  CommonLambdaImageRepositoryUri:
    Type: String
    Description: ECR Repository URI for docker image for lambda function
    Default: 【次のステップで生成されたAmazon ECRのURIを記入してください】

Resources:
### IAM resources ###
  LambdaExecutionRole:
    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/AmazonS3FullAccess
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        

  ### Lambda resources ###
  S3MessageHandlerFunction:
    Type: AWS::Serverless::Function
    Properties:
      Description: "Send contents to S3 file"
      PackageType: Image
      ImageUri: !Sub "${CommonLambdaImageRepositoryUri}:latest"
      ImageConfig:
        Command: ["s3_writer.lambda_handler"]
      Role: !GetAtt LambdaExecutionRole.Arn
      FunctionName: "s3-test-function"
      Timeout: 300
      MemorySize: 1024
      Environment:
        Variables:
          BUCKET_NAME: !Ref BucketName
          OUTPUT_KEY: !Ref OutputKey
      

AWS CloudFormationをデプロイするために、Amazon ECRのイメージを作成することが必要です。
次のステップでAmazon ECRの本体を作成し、Dockerイメージを本体にアタッチします。
まずはAmazon ECRの作成です。下記のコマンドを実施して、Amazon ECRを新規作成します。

 
STACK_NAME=test-blog 
COMMON_REPOSITORY_NAME=${STACK_NAME}-commonlambdaimagerepository 
aws ecr create-repository --repository-name ${COMMON_REPOSITORY_NAME} --region ap-northeast-1 

これでAmazon ECRの作成が完了しました。続いてDockerイメージを作成し、Amazon ECRにアタッチします。
下記のフォルダーにDocker環境用のファイルを定義しました。dockerフォルダーの下にフォルダーを設置し、3つのファイルを配置します。

 
docker
 |---liu-folder
   |---Dockerfile
   |---requirements_lambda.txt
   |---s3_writer.py    
 

DockerFileはコンテナイメージ形式のAWS Lambdaを実行します。コマンドは下記の通りです。

FROM public.ecr.aws/lambda/python:3.11
USER root

ARG LAMBDA_TASK_ROOT=/var/task
WORKDIR ${LAMBDA_TASK_ROOT}
COPY ./docker/liu-folder/requirements_lambda.txt .
RUN pip3 install -r requirements_lambda.txt --target ${LAMBDA_TASK_ROOT}

COPY ./docker/liu-folder/s3_writer.py .
CMD ["s3_writer.lambda_handler"]

requirements_lambda.txtはAWS Lambda関数のpython環境を配置します。コマンドは下記の通りです。

slack_sdk==3.27.0
pandas==2.2.1
pyarrow==15.0.0
s3fs==2024.2.0
botocore==1.34.2
boto3==1.34.2

s3_writer.pyはAWS Lambda関数コードを格納します。コードは前述のAWS Lambda関数のコードと同じです。

Dockerの起動コマンド【sudo service docker start】を入力してから、Dockerイメージの作成及びAmazon ECRにプッシュするコマンドを実行しました。コマンドは下記のとおりです。
COMMON_REPOSITORY_NAMEの定義は前述のAmazon ECR作成時のコマンドをご参照ください。

AWS_ACCOUNT_ID=[YOUR ACCOUNT ID]
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
ECR_REPOSITORY_URI_COMMON=$(aws ecr describe-repositories --repository-names ${COMMON_REPOSITORY_NAME} --output json | jq -r ".repositories[0].repositoryUri")
DOCKERFILE_PATHS_COMMON=./docker/test-common/Dockerfile
LOCAL_IMAGE_TAG_COMMON=$(echo "$ECR_REPOSITORY_URI_COMMON" | sed -r 's!.*/(.*)$!\1!')
docker build -f $DOCKERFILE_PATHS_COMMON -t $LOCAL_IMAGE_TAG_COMMON .
docker tag $LOCAL_IMAGE_TAG_COMMON $ECR_REPOSITORY_URI_COMMON
docker push $ECR_REPOSITORY_URI_COMMON

これでAWS Lambdaのコンテナイメージ形式に依存するAmazon ECRの環境が整いました。
次はAWS CloudFormationのデプロイ作業となります。

AWS CloudFormationをデプロイし、AWS Lambdaなどのリソースをアクティブ化します。
下記のコマンドを実行します。STACK_NAMEの定義は前述のAmazon ECR作成時のコマンドと同じです。


sam deploy   \
--template 上記AWS CloudFormationのYAMLファイルの場所  \
 --stack-name ${STACK_NAME}  \
--guided

そのままEnterキーを押して、【Deploy changes】のところに【y】を入力して、デプロイを成功させます。

 
Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

次は、AWS Step Functionsの作成と実行のステップになります。

まずはAWS Step Functionsの作成です。
ステートマシン作成の画面に従い、AWS Step Functionsの作成ページに移動します。
コード形式を載せておきます。

 
{
    "Comment": "A description of my state machine",
    "StartAt": "Lambda Invoke",
    "States": {
        "Lambda Invoke": {
            "Type": "Task",
            "Resource": "arn:aws:states:::lambda:invoke",
            "OutputPath": "$.Payload",
            "Parameters": {
                "Payload.$": "$",
                "FunctionName": 【前記AWS Lambda関数のARNを載せてください】
            },
            "Next": "SNS Publish",
            "Catch": [
              {
                "ErrorEquals": [
                  "States.ALL"
              ],
                "Next": "Publish Error Message"
              }
           ]            
        },
        "SNS Publish": {
            "Type": "Task",
            "Resource": "arn:aws:states:::sns:publish",
            "Parameters": {
                "TopicArn": 【自分のメールにつながるAmazon SNSトピックARNを載せてください】,
                "Message.$": "$"
            },
            "End": true
        }
    }
}

これで再現に関する構築が終わり、再現が可能になりました。
以下、エラーを再現した手順と原因を紹介します。

3.エラー再現の流れと原因の確認

前記のAWS Step Functionsを利用し、下記のように入力してテストしました。

 
{"contents": "Hello world!"}

下記のようにS3バケット内にHello worldのファイルが生成され、無事に実行できました。

翌月の同じ日付に実行してみると、今度はAWS Lambda関数の実行が失敗します。
原因としては、AWS Lambda関数がInactive状態になったため、Active状態までに復旧の時間がかかり、LambdaExceptionの例外が発生したからです。
一度Inactive状態になったAWS Lambda関数は、Active状態になるまでは実行ができません。

対象のAWS Lambda関数のアクティブ化が必要、といった内容のアラームが挙がっていることをAWS Lambdaのコンソール画面から確認できました。

4.対策

対策について説明します。

4-1.実行頻度調整

Inactive状態になる可能性があるコンテナイメージ形式のAWS Lambda関数に対して、まずはコンテナイメージ形式のAWS Lambda関数は月に何回実行するかを確認します。
上で紹介したドキュメントでは、数週間にわたって関数が呼び出されないとInactive状態に移行すると言及されていたので、数週間ぶりあるいは月1回程度の実行間隔は避ける必要があります。
一例としては、月に10日間隔を開けて3回実行すると、Inactiveのエラーイシューがなくなります。

4-2.リトライ設定の導入

一方、実行コストの関係などから実行頻度を抑えたい場合は、待機とリトライの手法があります。
待機手法とは、AWS Lambda関数がActive状態になるまで、AWS Step Functionsが失敗せず、一定の時間Waitステートに転移させる方法です。
リトライ手法とは、AWS Lambda関数がActive状態になるまで、一定時間リトライを続ける方法です。
実装難易度を比較すると、前者の方はWaitステートの出入りが必要であるため、条件式で済む後者のほうが簡単です。
Inactive状態からActive状態に戻すには、およそ30秒前後かかります。
コンテナイメージのサイズ次第ではさらに時間がかかるケースがあるので、最大60秒くらい想定すれば良いと思います。
以下は【IntervalSeconds(再試行時間間隔)】を30秒、【MaxAttempts(最大試行回数)】を3回にして、リトライを続ける時間を60秒以上に設定した例です。
BackoffRateとは、リトライ再実行前の間隔時間です。
リトライの詳細説明は公式ドキュメントを参照してください。

 
            "Retry": [
                {
                    "ErrorEquals": [
                        "Lambda.ServiceException",
                        "Lambda.AWSLambdaException",
                        "Lambda.SdkClientException",
                        "Lambda.TooManyRequestsException"
                    ],
                    "IntervalSeconds": 30,
                    "MaxAttempts": 3,
                    "BackoffRate": 2
                }
            ],

上記の例の場合は、再試行を実行している間にAWS Lambda関数がActive状態になり、AWS Step Functionの実行が成功しました。

まとめ

今回のエラーイシューでは、実行頻度が少ないコンテナイメージ形式のAWS Lambda関数に対して、しっかり注意を払う必要があることを実感しました。
本記事では、コンテナイメージ形式のAWS Lambda関数のInactive状態を解決する案を紹介しました。
今後別のサービスでも特殊な例外処理が発生したら、原因を特定して効率的な解決案を紹介したいと思います。

最後まで読んでいただき、ありがとうございました。

サンプルコード

以下、AWS Step Functionsを作成するために利用された AWS CloudFormationとAWS Lambda関数、AWS Step Functionsのコードを載せておきます。

AWS CloudFormation全般仕様

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: test for lambda image layer

Parameters:

  BucketName:
    Type: String
    Description: Output bucket of writing contents to S3 file
    Default: 【適切な名前を定義してください】

  OutputKey:
    Type: String
    Description: Output key of writing contents to S3 file
    Default: 【テキストファイルの名前*.txtを定義してください】

  CommonLambdaImageRepositoryUri:
    Type: String
    Description: ECR Repository URI for docker image for lambda function
    Default: 【ECR生成ステップにて、Amazon ECRのURIを記入してください】

Resources:
### Secrets resources ###

  LambdaExecutionRole:
    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/AmazonS3FullAccess
      - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        

  ### AWS Lambda resources ###
  S3MessageHandlerFunction:
    Type: AWS::Serverless::Function
    Properties:
      Description: "Send contents to S3 file"
      PackageType: Image
      ImageUri: !Sub "${CommonLambdaImageRepositoryUri}:latest"
      ImageConfig:
        Command: ["s3_writer.lambda_handler"]
      Role: !GetAtt LambdaExecutionRole.Arn
      FunctionName: "s3-test-function"
      Timeout: 300
      MemorySize: 1024
      Environment:
        Variables:
          BUCKET_NAME: !Ref BucketName
          OUTPUT_KEY: !Ref OutputKey
      

AWS Lambda関数全般仕様

#Name: s3_writer.py

import boto3
from typing import Any
import os


def lambda_handler(event: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
    bucket_name = os.environ["BUCKET_NAME"]
    output_key = os.environ["OUTPUT_KEY"]
    
    s3_contents = event.get("contents", "")
    s3_client = boto3.client("s3")
    s3_client.put_object(Body=s3_contents, Bucket=bucket_name, Key=output_key)
    return {"statusCode": 200}

AWS Step Functions全般仕様

 
{
  "Comment": "A description of my state machine",
  "StartAt": "Lambda Invoke",
  "States": {
    "Lambda Invoke": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "OutputPath": "$.Payload",
      "Parameters": {
        "Payload.$": "$",
        "FunctionName": 【前記したAWS Lambda関数のARN】
      },
      "Retry": [
        {
          "ErrorEquals": [
            "Lambda.ServiceException",
            "Lambda.AWSLambdaException",
            "Lambda.SdkClientException",
            "Lambda.TooManyRequestsException"
          ],
          //before
          "IntervalSeconds": 5,
          "MaxAttempts": 1,
          "BackoffRate": 2
          //after
          "IntervalSeconds": 30,
          "MaxAttempts": 3,
          "BackoffRate": 2
        }
      ],
      "Next": "SNS Publish",
      "Catch": [
        {
          "ErrorEquals": [
            "States.ALL"
          ],
          "Next": "Publish Error Message"
        }
      ]
    },
    "Publish Error Message": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
        "Message.$": "$",
        "TopicArn": 【自分のメールにつながるAmazon SNSトピックのARN】
      },
      "End": true
    },
    "SNS Publish": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish",
      "Parameters": {
    "Message.$": "$",
        "TopicArn": 【自分のメールにつながるAmazon SNSトピックのARN】
      },
      "End": true
    }
  }
}
dut_liu

データ基盤分析開発チームのりゅうです。コードの作業がメインで、インフラなどはコツコツ勉強中。目指すのはコードとインフラの二刀流。

Recommends

こちらもおすすめ

Special Topics

注目記事はこちら