CFNカスタムリソースでLambdaレイヤーパッケージを準備する – Python版

CloudFormationカスタムリソースでLambdaレイヤーパッケージを準備する - Python版

CloudFormationカスタムリソースを使って、Python用のLambdaレイヤーパッケージを自動的に作成・配置する

以下のページでLambdaレイヤーの作成方法について取り上げました。

あわせて読みたい
CFNでLambdaレイヤー作成 【CloudFormationでLambdaレイヤー作成】 本ページでは、CloudFormationでLambdaレイヤーを作成する方法を確認します。 Lambda レイヤーは、Lambda 関数で使用できるラ...

上記のページでは、Lambdaレイヤー本体はCloudFormationで作成しますが、Lambdaレイヤー用のパッケージは事前に手動で作成していました。

今回はCloudFormationカスタムリソースを使用して、このパッケージの準備を自動化することを考えます。
具体的には、パッケージを作成し、S3バケットに設置するという処理を、カスタムリソースで自動化します。

なお対象とするランタイム環境はPython3.8で、pipを使用します。

構築する環境

Diagram of preparing Lambda Layer Package with CloudFormation Custom Resource - Python Version

2つのLambda関数を作成します。

1つ目の関数でLambdaレイヤーパッケージを作成し、S3バケットに設置します。
この関数がCloudFormationスタック作成時に自動的に実行されるように、CloudFormationカスタムリソースに関数を関連づけます。
pipを使ってレイヤーに含めるライブラリを用意しますが、SSMパラメータストアに登録されている値を参照してインストールします。

2つ目の関数はLambdaレイヤーの確認用として使用します。
Function URLを有効化します。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/064

テンプレートファイルのポイント解説

SSMパラメータストア

Resources:
  RequirementsParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Ref Prefix
      Type: String
      Value: |
        requests
        beautifulsoup4
Code language: YAML (yaml)

pipでインストールするライブラリをSSMパラメータストアに登録します。
後述しますが、この文字列からrequirements.txtを作成し、ライブラリを一括インストールします。

CloudFormationカスタムリソース

まずカスタムリソースで実行するLambda関数を確認します。

Resources:
  Function1:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          LAYER_PACKAGE: !Ref LayerPackage
          REGION: !Ref AWS::Region
          REQUIREMENTS_PARAMETER: !Ref RequirementsParameter
          S3_BUCKET: !Ref CodeS3Bucket
          S3_BUCKET_FOLDER: !Ref Prefix
      Code:
        ZipFile: |
          ...
      EphemeralStorage:
        Size: !Ref EphemeralStorageSize
      FunctionName: !Sub "${Prefix}-function1"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole1.Arn
      Timeout: !Ref Timeout
Code language: YAML (yaml)

ArchitectureおよびRuntimeプロパティがポイントです。
後述のLambdaレイヤーリソースで指定する値と合わせる必要があります。
今回は以下の通りに設定しました。

  • Architecture:arm64
  • Runtime:python3.8

EphemeralStorageおよびTimeoutプロパティも調整が必要なパラメータです。
Lambdaレイヤーに含めるライブラリの容量や数によっては、これらの値を大きめに設定する必要があります。
今回は以下の通りに設定しました。

  • EphemeralStorage:512
  • Timeout:300

Environmentプロパティで関数に渡すことができる環境変数を定義できます。
作成するパッケージのファイル名やSSMパラメータストアのパラメータ名、パッケージを設置するS3バケットに関する情報を渡します。

Lambda関数で実行するコードをインライン表記で定義します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CloudFormationでLambdaを作成する3パータン(S3/インライン/コンテナ) 【CloudFormationでLambdaを作成する】 CloudFormationでLambdaを作成する場合、大別すると以下の3パターンあります。 S3バケットにコードをアップロードする インライ...

以下が実行するコードです。

import boto3
import cfnresponse
import os
import pip
import shutil
import subprocess

layer_package = os.environ['LAYER_PACKAGE']
region = os.environ['REGION']
requirements_parameter = os.environ['REQUIREMENTS_PARAMETER']
s3_bucket = os.environ['S3_BUCKET']
s3_bucket_folder = os.environ['S3_BUCKET_FOLDER']

CREATE = 'Create'
response_data = {}

work_dir = '/tmp'
requirements_file = 'requirements.txt'
package_dir = 'python'

requirements_path = os.path.join(work_dir, requirements_file)
package_dir_path = os.path.join(work_dir, package_dir)
layer_package_path = os.path.join(work_dir, layer_package)

def lambda_handler(event, context):
  try:
    if event['RequestType'] == CREATE:
      ssm_client = boto3.client('ssm', region_name=region)
      ssm_response = ssm_client.get_parameter(Name=requirements_parameter)
      requirements = ssm_response['Parameter']['Value']

      with open(requirements_path, 'w') as file_data:
        print(requirements, file=file_data)

      pip.main(['install', '-t', package_dir_path, '-r', requirements_path])
      shutil.make_archive(
        os.path.splitext(layer_package_path)[0],
        format='zip',
        root_dir=work_dir,
        base_dir=package_dir
        )

      s3_resource = boto3.resource('s3')
      bucket = s3_resource.Bucket(s3_bucket)

      bucket.upload_file(
        layer_package_path,
        '/'.join([s3_bucket_folder, layer_package])
        )

    cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)

  except Exception as e:
    print(e)
    cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
Code language: Python (python)

cfnresponseモジュールを使用して、関数をLambda-backedカスタムリソースとして実装します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CloudFormationカスタムリソース入門 【CloudFormationカスタムリソースの挙動を確認する構成】 CloudFormationの機能の1つにカスタムリソースがあります。 カスタムリソースを使用すると、テンプレートにカ...

実行するコードの内容ですが、以下の通りです。

  1. CloudFormationテンプレートで定義した環境変数を、os.environにアクセスして取得する。
  2. Boto3でSSMパラメータストアの値を取得して、requirements.txtを作成する。
  3. pipでrequirements.txtを指定して、ライブラリを一括インストールする。
  4. インストールしたライブラリをshutil.make_archiveでZIPファイル化する。
  5. Boto3でZIPファイルをS3バケットにアップロードする。

ちなみに関数用のIAMロールは以下の通りです。

Resources:
  FunctionRole1:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: CreateLambdaLayerPackagePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:GetParameter
                Resource:
                  - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${RequirementsParameter}"
              - Effect: Allow
                Action:
                  - s3:PutObject
                Resource:
                  - !Sub "arn:aws:s3:::${CodeS3Bucket}/*"
Code language: YAML (yaml)

AWS管理ポリシーであるAWSLambdaVPCAccessExecutionRoleに加えて、SSMパラメータストアからパラメータを取得する権限と、S3バケットにオブジェクトをアップロードする権限を付与します。

続いてCloudFormationカスタムリソース本体を確認します。

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt Function1.Arn
Code language: YAML (yaml)

先述のLambda関数を指定します。

Lambdaレイヤー

Resources:
  LambdaLayer:
    Type: AWS::Lambda::LayerVersion
    DependsOn:
      - CustomResource
    Properties:
      CompatibleArchitectures:
        - !Ref Architecture
      CompatibleRuntimes:
        - !Ref Runtime
      Content:
        S3Bucket: !Ref CodeS3Bucket
        S3Key: !Ref LayerS3Key
      Description: !Ref Prefix
      LayerName: !Ref Prefix
Code language: YAML (yaml)

ポイントは3つあります。

1つ目はこのリソースが作成されるタイミングです。
先述のLambda関数が実行された後に、本リソースが作成される必要があります。
ですからDependsOnにカスタムリソースを指定します。

2つ目はCompatibleArchitecturesおよびCompatibleRuntimesプロパティです。
先述のLambda関数で指定したものと同一の値を設定します。

3つ目はContentプロパティです。
先述のLambda関数でアップロードしたZIPファイルを指定します。

(参照)確認用Lambda関数

Resources:
  Function2:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Code:
        ZipFile: |
          import json
          import requests
          from bs4 import BeautifulSoup

          url = 'https://news.google.com/rss'

          def lambda_handler(event, context):
            r = requests.get(url)
            soup = BeautifulSoup(r.text, 'lxml')

            titles = [item.find('title').getText() for item in soup.find_all('item')]

            return {
              'statusCode': 200,
              'body': json.dumps(titles, indent=2)
            }
      FunctionName: !Sub "${Prefix}-function2"
      Handler: !Ref Handler
      Layers:
        - !Ref LambdaLayer
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole2.Arn
Code language: YAML (yaml)

確認用の関数ですから、特別な設定は行いません。

Layersプロパティで、先述のLambdaレイヤーを指定します。
import文でレイヤーに含まれるライブラリをインポートします。
この関数が正常に動作すれば、レイヤーパッケージ作成・Lambdaレイヤーリソース作成が正常に完了したことが確認できます。

コードの内容ですが、レイヤーに含まれるrequestsおよびBeautifulSoupを使用して、GoogleニュースのRSSを取得し、各アイテムのタイトルを返すという内容です。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • Lambda関数1:fa-064-function1
  • Lambda関数2:fa-064-function2
  • Lambda関数2のFunction URL:https://xljafu2utaoovuvrgvqcxtl2540uafya.lambda-url.ap-northeast-1.on.aws/
  • SSMパラメータストアのパラメータ:fa-064
  • Lambdaレイヤー用パッケージを保存するS3バケットおよびフォルダ:awstut-bucket/fa-064

AWS Management Consoleから各リソースを確認します。
まずCloudFormationカスタムリソースを確認します。

CloudFormation Stack.

Lambda関数とカスタムリソース本体が正常に作成されていることがわかります。

次にSSMパラメータストアに保存されている値を確認します。

SSM Parameter Store.

requirements.txtの内容が保存されていることがわかります。

動作確認

準備が整いましたので、実際の動作を確認します。

Lambda関数の実行結果

まずCloudFormationカスタムリソースに関連づけたLambda関数の実行結果を、CloudWatch Logsのロググループで確認します。

Result of CloudFormation Custom Resource 1.

ログから、SSMパラメータストアに保存されていたrequirements.txtの内容に従って、ライブラリがインストールされていることがわかります。
つまりLambdaレイヤーパッケージが正常に作成されたということです。

そしてCloudFormationカスタムリソースとして、関数が「SUCCESS」を返していることもわかります。
つまり関数はカスタムリソースとして正常に動作したということです。

S3バケット

次にS3バケットにアクセスして、Lambdaレイヤーパッケージの設置状況を確認します。

Result of CloudFormation Custom Resource 2.

確かにS3バケットにLambdaレイヤーパッケージ(layer.zip)が設置されています。

Lambdaレイヤー

Lambdaレイヤーの作成状況を確認します。

Lambda Layer.

CloudFormationテンプレートで指定した通りに、アーキテクチャやランタイムが設定されています。
先述のS3バケット上のZIPファイルを参照して、このレイヤーが作成されているはずです。

確認用Lambda関数

確認用Lambda関数の作成状況を確認します。

Lambda Function 1.

Lambdaレイヤーが関連づいていることがわかります。

以下がレイヤーの詳細です。

Lambda Function 2.

先述のLambdaレイヤーが関連づいていることがわかります。

準備が整いましたので、この関数のFunction URLにアクセスします。
Function URLに関する詳細は、以下のページをご確認ください。

あわせて読みたい
CFNでLambda Function URL – 認証方式: NONE 【CloudFormationでLambda Function URLを作成する(NONEバージョン)】 2022年4月22日にLambda Function URLがリリースされました。 この新機能は、AWS Lambda サービス...
Result of Lambda Function with Lambda Layer.

GoogleニュースのRSS情報を取得できました。
この関数に関連づいているLambdaレイヤーに含まれているrequestsおよびBeautifulSoupが使えていることがわかります。

以上のことから、CloudFormationカスタムリソースを使用することによって、Python用のLambdaレイヤーパッケージを、自動的に用意できたことが確認できました。

まとめ

Python用のLambdaレイヤーパッケージを作成し、S3バケットに設置するという処理を、カスタムリソースで自動化する方法を確認しました。