2️⃣

【CloudFormation/S3/Lambda】S3へのファイルアップロードをトリガーにLambdaを実行する<デプロイ編>

2024/09/21に公開

1.はじめに

今回は実装編で作成したLambda関数をAWS環境にデプロイします。
加えて、各リソースをテンプレート化して、CloudFormationでまとめてデプロ
します。

本記事では実装編の内容を前提としていますので、まだ見ていない方は以下記事にも目を通していただけると嬉しいです。

https://zenn.dev/is0383kk/articles/bf31ded54cbcb5


この記事は実装編とデプロイ編の2部構成となっています。

  • 実装編:PythonでのLambda関数実装が中心
  • デプロイ編:「template.yaml」の記述方法・デプロイ後の動作確認方法が中心

今回はデプロイ編ということでtemplate.yamlの記述方法やデプロイ時の動作確認方法の説明をします。
本記事で実装したものは以下リポジトリに上げています。
https://github.com/is0383kk/Python-Lambda-Sample

2.準備

本記事では以下を前提とします。

■ ディレクトリ構成

本記事では以下のディレクトリ構成で進めます。

親ディレクトリ
┣ samconfig.toml ← デプロイ編で作成
┣ template.yaml ← デプロイ編で作成
┣ common_layer
┃ ┗ requirements.txt ← 実装編で作成
┗ s3_lambda_functions
 ┗ lambda_function.py ← 実装編で作成

3.template.yamlの作成

今回は「template.yaml」を元にCloudFormationから各リソースを作成します。
作成するリソースとしては以下の通りです。

作成するリソース 説明
S3バケット ファイル格納先となるS3バケットです
Lambda関数 S3へのファイル格納をトリガーとして実行する関数
S3に格納されたファイル内容を取得します

作成する「template.yaml」全体としては以下の通りです。

template.yamlの全体
template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: S3 Lambda Application

# SAM テンプレートのバージョンを指定し、サーバーレスアプリケーションを定義
Globals:
  Function:
    Timeout: 5              # Lambda 関数のデフォルトのタイムアウト(5秒)
    MemorySize: 128          # デフォルトのメモリサイズ(128MB)
    Runtime: python3.11      # Python 3.11 をランタイムとして使用
    Architectures:
      - arm64                # Lambda 関数を ARM64 アーキテクチャで実行
    LoggingConfig:
      LogFormat: JSON        # ログフォーマットを JSON 形式に設定
      ApplicationLogLevel: INFO # ログレベルを INFO に設定
    Environment:
      Variables:
        POWERTOOLS_SERVICE_NAME: S3Application  # Powertools のサービス名を指定
        AWS_REGION_NAME: ap-northeast-1         # 使用する AWS リージョン

# パラメータセクション:デプロイ時に渡されるカスタムパラメータ
Parameters:
  OverridesParam:
    Type: String            # パラメータの型を String に設定
    Description: OverridesParam # パラメータの説明

# リソースセクション:Lambda 関数、S3 バケット、レイヤーなどの AWS リソースを定義
Resources:
  # 共通の Lambda Layer を定義
  CommonLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: CommonLayer               # レイヤーの名前を定義
      Description: Common Layer            # レイヤーの説明
      ContentUri: common_layer/            # レイヤーのソースコードの場所
      CompatibleRuntimes:
        - python3.11                      # Python 3.11 ランタイムに対応
      RetentionPolicy: Delete              # 古いレイヤーバージョンを自動削除
    Metadata:
      BuildMethod: python3.11              # SAM によるビルド設定
      BuildArchitecture: arm64             # ARM64 アーキテクチャでビルド

  # S3 と連携する Lambda 関数を定義
  S3LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: lambda_function.lambda_handler  # Lambda 関数のエントリーポイント(ハンドラ)
      CodeUri: src/s3_lambda_functions/        # 関数コードのパス
      Description: S3 Lambda function          # 関数の説明
      Layers:
        - !Ref CommonLayer                    # 共通レイヤーを関数に適用
      Policies:
        - Statement:                          # S3 バケットへのアクセス許可を設定
            Effect: Allow
            Action:                           # 許可されるアクション
              - s3:ListBucket                 # バケットのリストを取得
              - s3:GetObject                  # オブジェクトの取得を許可
            Resource:                         # 許可するリソース(S3 バケット)
              - !Sub "arn:aws:s3:::lambda-bucket-${OverridesParam}"
              - !Sub "arn:aws:s3:::lambda-bucket-${OverridesParam}/*"
      Events:
        S3Event:                              # S3 イベントトリガーを設定
          Type: S3                            # S3 イベントタイプ
          Properties:
            Bucket: !Ref S3LambdaBucket       # イベントがトリガーされるバケットを指定
            Events: s3:ObjectCreated:*        # オブジェクト作成時にトリガー
            Filter:                           # イベントフィルター(特定のプレフィックスに限定)
              S3Key:
                Rules:
                  - Name: prefix
                    Value: public/            # "public/" プレフィックスで始まるオブジェクトのみ対象
      Tags:
        Name: "s3-lambda-function"            # Lambda 関数に "s3-lambda-function" タグを追加
      Timeout: 300                            # タイムアウト(300秒)

  # S3 が Lambda 関数をトリガーするための権限を設定
  LambdaInvokePermissionForS3:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt S3LambdaFunction.Arn  # Lambda 関数の ARN を取得
      Action: "lambda:InvokeFunction"             # 関数の実行権限を付与
      Principal: "s3.amazonaws.com"               # 実行元を S3 に制限
      SourceArn: !GetAtt S3LambdaBucket.Arn       # 実行権限を与える S3 バケットの ARN

  # S3 バケットを定義
  S3LambdaBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "lambda-bucket-${OverridesParam}" # パラメータに基づいたバケット名を指定

■ 手順1:Globalsの定義

それではリソース作成のための「template.yaml」を作成します。

まず、「Globals」を定義します。
「Globals」は、テンプレート内で定義されるリソースへの共通設定を定義できます。
これにより、Lambda関数が複数あった場合でも、共通の設定を一括して適用することができます。

Globalsの定義
Globals:
  Function:
    Timeout: 5
    MemorySize: 128
    Runtime: python3.11
    Architectures:
      - arm64
    LoggingConfig:
      LogFormat: JSON
      ApplicationLogLevel: INFO
    Environment:
      Variables:
        POWERTOOLS_SERVICE_NAME: S3Application
        AWS_REGION_NAME: ap-northeast-1
上位階層 項目 説明 設定値
Function Lambda関数の共通設定
Timeout Lambda関数の最大実行時間(秒) 5秒に設定
MemorySize Lambda関数に割り当てるメモリサイズ(MB) 128MBに設定。
Runtime 使用するランタイム python3.11を指定
Architectures Lambda関数のアーキテクチャ arm64を指定
LoggingConfig Lambda関数のログに関わる設定
LogFormat CloudWatchに出力されるログ形式 JSON形式に指定
ApplicationLogLevel ログの出力レベル INFOで指定
Environment 環境設定
POWERTOOLS_SERVICE_NAME Lambdaのサービス名 S3Application
AWS_REGION_NAME AWSのリージョン ap-northeast-1を指定

■ 手順2:Parametersの定義

「Parameters」を定義します。
「Parameters」は、テンプレートにカスタムパラメータを設定することができます。
カスタムパラメータは「samconfig.toml」から参照させることができます。

Parametersの定義
Parameters:
  OverridesParam:
    Type: String
    Description: OverridesParam
上位階層 項目 説明 設定値
Parameters 変数として定義するパラメータ群
OverridesParam 文字列型のパラメータとして定義しています
今回はS3バケット名の生成に使用しています
「samconfig.toml」から設定を呼び出します
Type: String

■ 手順3:Resourcesの定義

「Resources」を定義します。
「Resources」は、AWSのリソース(Lambda関数・S3バケット等)を定義できます。

CommonLayer

「CommonLayer」ではLambda関数で使用する共通ライブラリを定義します。
実装編で「common_layer」配下に「requirements.txt」を作成しましたが、
Lambda関数がデプロイされた際に「common_layer」配下の「requirements.txt」からライブラリをインストールさせるために下記リソースを定義しています。

CommonLayerの定義
 CommonLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: CommonLayer
      Description: Common Layer
      ContentUri: common_layer/
      CompatibleRuntimes:
        - python3.11
      RetentionPolicy: Delete
    Metadata:
      BuildMethod: python3.11
      BuildArchitecture: arm64
上位階層 項目 説明 設定値
Type Lambda関数での共通ライブラリをレイヤーとして利用 AWS::Serverless::LayerVersion
Properties
LayerName レイヤーの名前 CommonLayer
ContentUri レイヤーに含めるファイルがあるディレクトリを指定 common_layer/
CompatibleRuntimes 互換性のあるランタイムを指定 python3.11
RetentionPolicy 削除ポリシー
Deleteの場合、リソース削除時に削除されます
Delete

S3LambdaFunction

実装編で作成したS3からのトリガーを受けて実行されるLambda関数の定義です。

S3LambdaFunctionの定義
  S3LambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: lambda_function.lambda_handler
      CodeUri: src/s3_lambda_functions/
      Description: S3 Lambda function
      Layers:
        - !Ref CommonLayer
      Policies:
        - Statement:
            Effect: Allow
            Action:
              - s3:ListBucket
              - s3:GetObject
            Resource:
              - !Sub "arn:aws:s3:::lambda-bucket-${OverridesParam}"
              - !Sub "arn:aws:s3:::lambda-bucket-${OverridesParam}/*"
      Events:
        S3Event:
          Type: S3
          Properties:
            Bucket: !Ref S3LambdaBucket
            Events: s3:ObjectCreated:*
            Filter:
              S3Key:
                Rules:
                  - Name: prefix
                    Value: public/
      Tags:
        Name: "s3-lambda-function"
      Timeout: 300
上位階層 項目 説明 設定値
Type Lambda関数を定義 AWS::Serverless::Function
Properties
Handler Lambda関数のエントリポイント lambda_function.lambda_handler
「ファイルパス.関数名」で指定します。
CodeUri Lambda関数のコードがある場所 「src/s3_lambda_functions/」を指定
Layers 先ほど定義した CommonLayer を参照して使用します !Ref CommonLayer
Policies Lambda関数のポリシー
Statement S3の「ListBucket」と「GetObject」の権限を付与
設定名のS3バケットにアクセスできる
Effect: Allow
Action:「ListBucket」と「GetObject」
Resource:!Sub "arn:aws:s3:::lambda-bucket-${OverridesParam}"
Events Lamnda関数の実行条件
S3Event S3へのファイル格納をトリガーとして設定
※「S3バケット/public/」配下へのファイル格納をトリガー対象としています
Events: s3:ObjectCreated:*
Rules:
- Name: prefix
Value: public/

LambdaInvokePermissionForS3

S3がLambda関数を呼び出すための権限設定です。

LambdaInvokePermissionForS3の定義
  LambdaInvokePermissionForS3:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt S3LambdaFunction.Arn
      Action: "lambda:InvokeFunction"
      Principal: "s3.amazonaws.com"
      SourceArn: !GetAtt S3LambdaBucket.Arn
上位階層 項目 説明 設定値
Type S3が指定のLambda関数を呼び出すための権限を設定 AWS::Lambda::Permission
Properties
FunctionName S3LambdaFunctionのARNを取得し指定 !GetAtt S3LambdaFunction.Arn
Action Lambda関数の実行を許可 lambda:InvokeFunction
Principal s3.amazonaws.comが呼び出し元であることを指定 s3.amazonaws.com
SourceArn S3LambdaBucketのARNを指定し、特定のバケットからのイベントのみを許可 !GetAtt S3LambdaBucket.Arn

S3LambdaBucket

Lambda関数で使用するS3バケットです。

S3LambdaBucketの定義
  S3LambdaBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "lambda-bucket-${OverridesParam}"
上位階層 項目 説明 設定値
Type S3バケットを定義 AWS::S3::Bucket
Properties
BucketName バケット名
${OverridesParam}パラメータを使って動的に設定
BucketName: !Sub "lambda-bucket-${OverridesParam}"

4.samconfig.tomlの作成

続いて「samconfig.toml」を作成します。
「samconfig.toml」はAWS SAM (Serverless Application Model) を使用する際に、SAM CLI コマンドの設定を管理するためのファイルです。
このファイルにより、デプロイ・ビルド時のオプションや設定を明示的に指定することができます。

主な用途としては以下が挙げられます。

  • デプロイ設定の自動化:「samconfig.toml」にデプロイ設定を保存しておくことで、簡単に同じ設定を使ってデプロイを行えます。
  • 環境ごとの設定:開発環境・本番環境など、異なる環境に応じた設定を分けて管理できます。

作成する「samconfig.toml」全体としては以下の通りです。

samconfig.tomlの全体
samconfig.toml
# More information about the configuration file can be found here:
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
version = 0.1

[default]
[default.global.parameters]
stack_name = "s3-application"

[default.build.parameters]
cached = true
parallel = true

[default.validate.parameters]
lint = true

[default.deploy.parameters]
capabilities = "CAPABILITY_IAM"
confirm_changeset = true
resolve_s3 = true
parameter_overrides = ["OverridesParam='20240919'"]

[default.package.parameters]
resolve_s3 = true

[default.sync.parameters]
watch = true

[default.local_start_api.parameters]
warm_containers = "EAGER"

[default.local_start_lambda.parameters]
warm_containers = "EAGER"

■ S3バケット名のためのパラメータをsamconfig.tomlで定義

samconfig.tomlの説明は、「S3バケット名のためのパラメータを定義」部分のみ行います。

template.yaml上でS3バケット名に「lambda-bucket-${OverridesParam}」と定義しています。
この「${OverridesParam}」の部分はsamconfig.tomlの以下部分から呼び出されます。

parameter_overrides = ["OverridesParam='20240919'"]

したがって上記の場合、S3バケット名は「lambda-bucket-20240919」となります。

5.ビルド&デプロイ

それでは、実装物のビルドとデプロイを行います。
※AWS SAM CLIが使用できることを前提としています

以下のコマンドを実行することでビルドとAWS環境へのデプロイができます。

$ sam build
$ sam deploy --config-file samconfig.toml

デプロイが正常終了したことを確認します。
AWSのマネジメントコンソールを開き以下画面に遷移します。

  • CloudFormation → スタック → s3-application

ステータスが「CREATE_COMPLETE」となっていることを確認します。

また、リソースタブでtemplate.yaml上で定義した各リソースの確認ができます。

※スタックを削除したい場合は以下コマンドでスタックを削除できます。

aws cloudformation delete-stack --stack-name s3-lambda-app

6.動作確認

Lambda関数の動作確認を行います。
以下が確認できたら成功です。

  • S3バケット(lambda-bucket-20240919)に「public」フォルダを作成する
  • 「public」フォルダの中にファイルをアップロードする
  • Lambda関数が実行されることを確認する
  • CloudWatchのログにファイル情報が出力されることを確認する

■ S3バケットに「public」フォルダを作成する

S3バケット(lambda-bucket-20240919)に「public」フォルダを作成します。
AWSのマネジメントコンソールを開き以下画面に遷移します。

  • Amazon S3 → バケット → lambda-bucket-20240919

「フォルダの作成」から「public」フォルダを作成します。

「public」フォルダを作成する理由は、template.yaml上にLambda関数実行のトリガーとして、「public」フォルダを指定しているためです。

      Events:
        S3Event:
          Type: S3
          Properties:
            Bucket: !Ref S3LambdaBucket
            Events: s3:ObjectCreated:*
            Filter:
              S3Key:
                Rules:
                  - Name: prefix
                    Value: public/ # 

■ 「public」フォルダにファイルをアップロードする

「public」フォルダの中にファイルをアップロードします。
今回はテキストファイルをアップロードしてみます。

■ Lambda関数が実行されることを確認する

Lambda関数が実行されることを確認します。
AWSのマネジメントコンソールを開き以下画面に遷移します。

  • Lambda → 関数 → s3-application-S3LambdaFunction-XXXXXX
  • 「モニタリング」タブ → 「CloudWatch ログを表示」をクリック

以下のようにログストリームが出力されていればLambda関数が実行されています。

■ CloudWatchのログにファイル情報が出力されることを確認する

CloudWatchのログにファイル情報が出力されることを確認します。

対象のログストリームをクリックすると、以下画面が表示されます。
赤枠部分がLambda関数の「logger.info()」によって出力されたログとなります。

7.ログを見てみる

ソースコード上の各loggerで出力されたログを確認します。

ソースコードの全体
lambda_function.py
import json
from urllib.parse import unquote_plus

import boto3
from aws_lambda_powertools import Logger

logger = Logger()


def lambda_handler(event, context):
    logger.info(json.dumps(event))

    # バケット名
    bucket_name = event["Records"][0]["s3"]["bucket"]["name"]
    logger.info(bucket_name)

    # オブジェクトキー
    object_key = unquote_plus(event["Records"][0]["s3"]["object"]["key"])
    logger.info(object_key)

    # ETag
    etag = event["Records"][0]["s3"]["object"]["eTag"]
    logger.info(etag)

    # サイズ
    size = event["Records"][0]["s3"]["object"]["size"]
    logger.info(size)

    # S3にアップロードされたファイル内容を取得
    s3_client = boto3.client("s3")
    response = s3_client.get_object(Bucket=bucket_name, Key=object_key)
    logger.info(response)
    file_content = response["Body"].read()
    logger.info(file_content)

■ eventログ:logger.info(json.dumps(event))

はじめにeventログを確認します。
ソースコード上は以下部分です。

def lambda_handler(event, context):
    logger.info(json.dumps(event))

eventログを見ると以下のようなJSON形式で出力されていることが分かります。
これは、template.yaml上で「LogFormat: JSON」と指定しているためです。

eventの中身全体
eventの中身
{
    "level": "INFO",
    "location": "lambda_handler:11",
    "message": {
        "Records": [
            {
                "eventVersion": "2.1",
                "eventSource": "aws:s3",
                "awsRegion": "ap-northeast-1",
                "eventTime": "2024-09-19T15:23:44.430Z",
                "eventName": "ObjectCreated:Put",
                "userIdentity": {
                    "principalId": "ダミー"
                },
                "requestParameters": {
                    "sourceIPAddress": "ダミー"
                },
                "responseElements": {
                    "x-amz-request-id": "ダミー"
                    "x-amz-id-2": "ダミー"
                },
                "s3": {
                    "s3SchemaVersion": "1.0",
                    "configurationId": "ダミー",
                    "bucket": {
                        "name": "lambda-bucket-20240919",
                        "ownerIdentity": {
                            "principalId": "ダミー"
                        },
                        "arn": "arn:aws:s3:::lambda-bucket-20240919"
                    },
                    "object": {
                        "key": "public/test.txt",
                        "size": 23,
                        "eTag": "qawsedrftghyujikolp123456789",
                        "sequencer": "0123456789ABCDEFG"
                    }
                }
            }
        ]
    },
    "timestamp": "2024-09-19 15:23:46,659+0000",
    "service": "S3Application",
    "xray_trace_id": "ダミー"
}

また、「s3」部分には、バケット名やオブジェクトキーなども出力されていることが分かります。

"s3": {
    "s3SchemaVersion": "1.0",
    "configurationId": "ダミー",
    "bucket": {
        "name": "lambda-bucket-20240919",
        "ownerIdentity": {
            "principalId": "ダミー"
        },
        "arn": "arn:aws:s3:::lambda-bucket-20240919"
    },
    "object": {
        "key": "public/test.txt",
        "size": 23,
        "eTag": "qawsedrftghyujikolp123456789",
        "sequencer": "0123456789ABCDEFG"
    }
}

そして、S3ログの各要素には以下のようにアクセスできます。

lambda_function.py
# バケット名
bucket_name = event["Records"][0]["s3"]["bucket"]["name"]
logger.info(bucket_name)

# オブジェクトキー
object_key = unquote_plus(event["Records"][0]["s3"]["object"]["key"])
logger.info(object_key)

# ETag
etag = event["Records"][0]["s3"]["object"]["eTag"]
logger.info(etag)

# サイズ
size = event["Records"][0]["s3"]["object"]["size"]
logger.info(size)

■ ファイル内容ログ

最後にboto3で取得したファイル内容ログを確認します。
ソースコード上は以下部分です。

s3_client = boto3.client("s3")
response = s3_client.get_object(Bucket=bucket_name, Key=object_key)
logger.info(response)
file_content = response["Body"].read()
logger.info(file_content)

まず「logger.info(response)」を確認します。

responseの中身全体
responseの中身
{
    "level": "INFO",
    "location": "lambda_handler:32",
    "message": {
        "ResponseMetadata": {
            "RequestId": "ダミー",
            "HostId": "ダミー",
            "HTTPStatusCode": 200,
            "HTTPHeaders": {
                "x-amz-id-2": "ダミー",
                "x-amz-request-id": "ダミー",
                "date": "Thu, 19 Sep 2024 16:18:25 GMT",
                "last-modified": "Thu, 19 Sep 2024 16:18:21 GMT",
                "etag": "\"qawsedrftghyujikolp123456789\"",
                "x-amz-server-side-encryption": "AES256",
                "accept-ranges": "bytes",
                "content-type": "text/plain",
                "server": "AmazonS3",
                "content-length": "23"
            },
            "RetryAttempts": 0
        },
        "AcceptRanges": "bytes",
        "LastModified": "2024-09-19 16:18:21+00:00",
        "ContentLength": 23,
        "ETag": "\"qawsedrftghyujikolp123456789\"",
        "ContentType": "text/plain",
        "ServerSideEncryption": "AES256",
        "Metadata": {},
        "Body": "<botocore.response.StreamingBody object at 0xffff813ab820>"
    },
    "timestamp": "2024-09-19 16:18:24,746+0000",
    "service": "S3Application",
    "xray_trace_id": "ダミー"
}

「Body」部分を見ると、StreamingBodyオブジェクトが取得されていることが分かります。
このオブジェクトは、ファイルのデータをストリームとして読み出せるようにしています。
"Body": "<botocore.response.StreamingBody object at 0xffff813ab820>"

そして、「Body」を以下のように参照しログを出力すると、

file_content = response["Body"].read()
logger.info(file_content)

以下のようにファイル内容(Hello World!)が出力されていることが分かります。

bodyの中身
{
    "level": "INFO",
    "location": "lambda_handler:34",
    "message": "b'Hello World!'",
    "timestamp": "2024-09-19 16:18:24,747+0000",
    "service": "S3Application",
    "xray_trace_id": "ダミー"
}

8.おわりに

今回のデプロイ編では、実装編の成果物をAWS環境にデプロイする手順について説明しました。
また、S3にファイルをアップロードすることでLambda関数がトリガーされ、CloudWatchログを見ながらLambda関数の処理結果を確認しました。

実装編とデプロイ編を合わせて以下一連の流れを説明していますので、
見ていない方は実装編も見ていただけると幸いです。

  • Lambda関数の実装方法
  • SAMを用いたAWSリソースの自動デプロイ
  • S3イベントによる自動処理の一連のフローを実装する方法

https://zenn.dev/is0383kk/articles/bf31ded54cbcb5

付録

スタックを削除する際、S3バケットが空でないと削除できません。
以下コマンドでS3バケット内を再帰的に削除できます。

S3バケットの中身を削除する
aws s3 rm s3://バケット名 --recursive

S3バケットを空にした上で以下コマンドでスタックの削除ができます。

スタックを削除する
aws cloudformation delete-stack --stack-name スタック名

以下コマンドでスタックのステータスの確認ができます。

スタックのステータスを確認する
aws cloudformation describe-stacks --stack-name スタック名

削除に成功した場合、以下のようにメッセージが表示されます。

An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id s3-application does not exist

Discussion