top of page
検索
  • 執筆者の写真Naoya Yamashita

複数Lambdaの構築方法及びデプロイ

はじめに

この記事では一つのプロジェクトで複数のLambdaが必要なとき、どのようなプロジェクト構成にしたら良いのかについて提案します。先日の記事でLambda Layerに自作のコードを置かないことを提案しました。複数Lambdaでコードを共有するというのがLambda Layerの一つのユースケースのため、それを禁止した場合にどのような構成で開発すると良いのかについて解説します。


フォルダ構成

このフォルダ構成を提案します。左が先日の記事の構成、それを複数Lambdaの構成に変化させたのが右です。


(※なおb1というのはバックエンドのバージョン1という意味です)



サンプルバケットの準備バケットを作成し、適当にファイルを作ります。今回はライトにCLIから作成します。

aws s3 mb s3://dev-multiple-lambda-sample-script
date | aws s3 cp - s3://dev-multiple-lambda-sample-script/date1.txt
date | aws s3 cp - s3://dev-multiple-lambda-sample-script/date2.txt
date | aws s3 cp - s3://dev-multiple-lambda-sample-script/date3.txt

確認します。

$ aws s3 ls s3://dev-multiple-lambda-sample-script --recursive
2023-11-15 10:27:30         29 date1.txt
2023-11-15 10:27:34         29 date2.txt
2023-11-15 10:27:38         29 date3.txt

3ファイルできていることが確認できました。


Pydanticによる環境変数管理

サンプルなので何を実装しても良いのですが、複数のLambdaで共有する例として環境変数の管理をするモジュールを書きます。

環境変数の管理にもpydanticが利用できます。ただし、別パッケージに切り出されているため、利用するためにはインストールする必要があります。

poetry add pydantic-settings

そして app/function/b1/src/config.py を以下の内容で保存します。

import pydantic_settings

class Settings(pydantic_settings.BaseSettings):
    model_config = pydantic_settings.SettingsConfigDict(env_file='.env')

    bucket_script: str

settings = Settings()


if __name__ == '__main__':
    print(settings.model_dump_json(indent=2))

pydantic-settingsはローカル実行のために.envを読むことができます。.envを以下の内容で用意します

BUCKET_SCRIPT=dev-multiple-lambda-sample-script

以下のコマンドでこのモジュールだけを実行できます

$ poetry run python -m app.function.b1.src.config
{
  "script_bucket": "dev-multiple-lambda-sample-script"
}

.envがロードされ、意図通りに動作していることが確認できました。


共通モジュールの実装

さらに実践的な例としてutilフォルダも作ってみます。ここは雑多な共通関数置き場です。以下の内容で app/function/b1/util/lambda-decorator.py を用意します。

from typing import Any, Callable, ParamSpec, TypeVar
import logging

logger = logging.getLogger(__name__)


P = ParamSpec('P')
R = TypeVar('R')
def lambda_decorator(fn: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        logger.info(f'[lambda_decorator: start] args: {args}')
        res = fn(*args, **kwargs)
        logger.info(f'[lambda_decorator: end] res: {res}')
        return res

    return wrapper

if __name__ == '__main__':
    logger.setLevel(logging.INFO)
    logging_handler = logging.StreamHandler()
    logging_handler.setLevel(logging.INFO)
    logger.addHandler(logging_handler)

    @lambda_decorator
    def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
        return {
            "status": "ok",
        }

    handler({"a": "1"}, None)

以下のコマンドで実行できます

$ poetry run python -m app.function.b1.util.lambda_decorator
[lambda_decorator: start] args: ({'a': '1'}, None)
[lambda_decorator: end] res: {'status': 'ok'}

意図通りに動作しています。これでLambdaの入出力をロギングできるようになりました。このデコレータは非常に有用です。

お約束としてapp/function/b1/util/__init__.pyを以下の内容で用意します。

from . import lambda_decorator as lambda_decorator

Lambdaの実装

Lambdaを実装します。以前のLambdaについてもmainフォルダの中に移動させます。

mkdir app/function/b1/src/main
mv app/function/b1/src/main.py app/function/b1/src/main/print_yaml.py

boto3を使用するのでboto3をインストールします

poetry add boto3
poetry add -D 'boto3-stubs[s3]'

app/function/b1/src/main/s3_list_files.py で以下の内容を保存します。

import logging
from typing import Any

import boto3

from .. import util
from ..config import settings

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logging_handler = logging.StreamHandler()
logging_handler.setLevel(logging.INFO)
logger.addHandler(logging_handler)

@util.lambda_decorator.lambda_decorator
def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
    s3_handler = boto3.client('s3')
    response = s3_handler.list_objects_v2(Bucket=settings.script_bucket)
    return {
        "files": [
            elm['Key'] for elm in response['Contents']
        ]
    }

if __name__ == '__main__':
    logger = logging.getLogger('app')
    logger.setLevel(logging.INFO)
    logging_handler = logging.StreamHandler()
    logging_handler.setLevel(logging.INFO)
    logger.addHandler(logging_handler)

    handler({}, None)

こちらもローカルで起動できます。

$ poetry run python -m app.function.b1.src.main.s3_list_files
[lambda_decorator: start] args: ({}, None)
[lambda_decorator: end] res: {'files': ['date1.txt', 'date2.txt', 'date3.txt']}

とくにLambda内ではロギング等を実装していないのですが、デコレータのおかげで最低限、入出力は出力されています。

Lambda Layerの更新デプロイの前に、Lambda Layerを更新する必要があります。まず、現在のルートのpyproject.tomlを確認しましょう。

[tool.poetry]
name = "aws-lambda-layer-sample"
version = "0.1.0"
description = ""
authors = ["yamashita-optarc <yamashita@optarc.net>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "~3.11.0"
pyyaml = "^6.0.1"
pydantic-settings = "^2.1.0"
boto3 = "^1.29.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.2"
boto3-stubs = {extras = ["s3"], version = "^1.29.0"}

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

そしてLambda Layerのpyproject.tomlはこちらです。

[tool.poetry]
name = "b1"
version = "0.1.0"
description = ""
authors = ["yamashita-optarc <yamashita@optarc.net>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "~3.11.0"
pyyaml = "^6.0.1"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

dev.dependencyについてはローカルで使用するモジュールのため、Lambdaにデプロイする必要はありません。Lambda Layerには容量制限がありますし、大きなLayerを作成するとコールドスタートの時間が増えます。必要ないモジュールはバンドルしないようにしましょう。

さて、結局必要なのはpydantic-settingsとboto3ですね。これをインストールしましょう。

cd app/function-layer/b1/
poetry add pydantic-settings boto3

レイヤーの再作成はmakeから可能です。

make clean
make init
cd 

Lambdaのデプロイこのプロジェクトをデプロイするためには全てのLambdaについて同じソースで、エントリーポイントだけ違うという設定にしなければいけません。samにはこの構成でも自然にデプロイできます。

GlobalにCodeUriが設定できるため、このsamテンプレート全体で同じソースを参照するように設定できます。

AWSTemplateFormatVersion: "2010-09-09"

Transform: AWS::Serverless-2016-10-31

Parameters:
  Prefix: { Type: String }

Globals:
  Function:
    Runtime: python3.11
    CodeUri: ./app/function/b1

Resources:
  FunctionLayerB1:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: !Sub ${Prefix}-b1
      ContentUri: ./app/function-layer/b1/dist

  FunctionB1PrintYaml:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${Prefix}-B1PrintYaml
      Handler: src.main.print_yaml.handler
      Layers:
        - !Ref FunctionLayerB1

  FunctionB1S3ListFiles:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${Prefix}-B1ListFiles
      Handler: src.main.s3_list_files.handler
      Layers:
        - !Ref FunctionLayerB1

デプロイはいつもの通り以下のコマンドです。

sam deploy --stack-name dev-multiple-lambda-sample --capabilities CAPABILITY_NAMED_IAM --resolve-s3 --parameter-overrides Prefix=dev-multiple-lambda-sample

実行

Lambdaコンソールからdev-multiple-lambda-sample-B1ListFilesを実行します。そうすると以下のようなエラーが出ます。

Pydanticのエラーです。たしかに環境変数の設定をしていませんでした。この様になにかおかしい状態になったら速やかに終了するという原則をpydanticを使うことで自然に構築できます。

Parameters:
  Prefix: { Type: String }
  BucketScript: { Type: String }

Globals:
  Function:
    Runtime: python3.11
    CodeUri: ./app/function/b1
    Environment:
      Variables:
        BUCKET_SCRIPT: !Ref BucketScript

ParametersにBucketScriptを追加し、Globalsに参照を追加します。改めてデプロイします。BucketScriptのパラメータが増えたので、その値も与える必要があります。

sam deploy --stack-name dev-multiple-lambda-sample --capabilities CAPABILITY_NAMED_IAM --resolve-s3 --parameter-overrides Prefix=dev-multiple-lambda-sample BucketScript=dev-multiple-lambda-sample-script

改めて実行しますが、今度はs3の権限がないようです。



対象の権限を見つけるのは難しいですが、代表的な権限についてはsamのテンプレートが参考になります。ただしこのポリシーテンプレート機能を使うのではなく、これを参考にして全てベタ書きしたほうが柔軟性が上がると思います。

Resources:
  PolicyFunctionB1:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - s3:GetObject
              - s3:ListBucket
              - s3:GetBucketLocation
              - s3:GetObjectVersion
              - s3:GetLifecycleConfiguration
            Resource:
              - !Sub arn:aws:s3:::${BucketScript}
              - !Sub arn:aws:s3:::${BucketScript}/*

  FunctionB1S3ListFiles:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${Prefix}-B1ListFiles
      Handler: src.main.s3_list_files.handler
      Layers:
        - !Ref FunctionLayerB1
      Policies:
        - !Ref PolicyFunctionB1

改めてデプロイすると今度こそ実行に成功します。



print_yaml Lambda実装

print_yamlのLambdaもlambda_decoratorを使うように改修します。

import logging
from typing import Any

import yaml

from .. import util

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

src_logger = logging.getLogger('src')
src_logger.setLevel(logging.INFO)

def yaml_loads(target: str) -> dict[str, Any]:
    return yaml.safe_load(target)

@util.lambda_decorator.lambda_decorator
def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
    target_yaml = '''\
a: 1
b: 2
c: 3
'''
    print(yaml_loads(target_yaml))

    return {
        "status": "ok",
    }

デプロイして実行すると意図通りに動作します。



まとめ

Lambda Layerを使わずに複数Lambdaで共通のコードを利用する方法について解説しました。全てのLambdaが同じコードを持っていることについて違和感がある方もいると思いますが、速度とメンテナンス性を考えたときにこの構造をおすすめします。Lambdaの構築方法に悩んだ際は思い出していただけると幸いです。

閲覧数:195回0件のコメント

最新記事

すべて表示

Amazon CognitoからAmazon SESを使ってメールを送信する

はじめに Amazon Cognito user poolsではユーザー登録時の招待メールやパスワードリセット時など、いくつかのタイミングでユーザーに対してメールを発出します。 このメールの送信機能は「Cognitoのデフォルト機能による送信」と「Amazon SESによる送信」のいずれかを選ぶことができます。 この2つの違いは以下のとおりです。 ※1 自分が受信できるメールアドレスであることが必

Comments


bottom of page