AWS

CFNカスタムリソースを使ってLambdaレイヤーパッケージを準備する – 一般ファイル版

CFNカスタムリソースを使ってLambdaレイヤーパッケージを準備する – 一般ファイル版

以下のページで、Python用Lambdaレイヤーのパッケージを自動的に作成する方法をご紹介しました。

今回は言語に依らない一般的なファイルをレイヤー化して、関数から参照できるようにすることを考えます。
例えば証明書ファイル等を想定しています。

構築する環境

Diagram of preparing Lambda Layer Package with CloudFormation Custom Resources - General File Version.

基本的には、冒頭にご紹介したページと同様の構成です。

異なる点は2点です。

1点目はSSM Parameter Storeに保存する文字列です。
冒頭のページでは、インストールするPythonパッケージの名前のリストを登録していました。
今回の構成では、Lambdaレイヤーに含めるファイルのURLのリストを登録します。

2点目はカスタムリソースに関連づけるLambda関数の内容です。
冒頭のページでは、Python用パッケージ管理ツールpipでパッケージをダウンロードしました。
今回の構成では、urllib.request.urlopenを使用して、ファイルをダウンロードします。
なお関数のランタイム環境はPython3.8とします。

CloudFormationテンプレートファイル

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

awstut-fa/130 at main · awstut-an-r/awstut-fa
Contribute to awstut-an-r/awstut-fa development by creating an account on GitHub.

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

SSMパラメータストア

Resources:
  UrlsParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Ref Prefix
      Type: String
      Value: |
        https://data.nasa.gov/resource/gvk9-iz74.json
Code language: YAML (yaml)

ダウンロードするファイルのURLのリストを登録します。
今回はサンプルのファイルとして、NASAが公開しているJSONファイルを対象とします。

CloudFormationカスタムリソース

Lambda関数

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

Resources:
  Function1:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          LAYER_PACKAGE: !Ref LayerPackage
          REGION: !Ref AWS::Region
          URLS_PARAMETER: !Ref UrlsParameter
          S3_BUCKET: !Ref CodeS3Bucket
          S3_BUCKET_FOLDER: !Ref Prefix
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import os
          import shutil
          import subprocess
          import urllib
          
          layer_package = os.environ['LAYER_PACKAGE']
          region = os.environ['REGION']
          urls_parameter = os.environ['URLS_PARAMETER']
          s3_bucket = os.environ['S3_BUCKET']
          s3_bucket_folder = os.environ['S3_BUCKET_FOLDER']
          
          CREATE = 'Create'
          response_data = {}
          
          work_dir = '/tmp'
          package_dir = 'python'
          
          package_dir_path = os.path.join(work_dir, package_dir)
          layer_package_path = os.path.join(
            work_dir,
            layer_package
            )
            
          ssm_client = boto3.client('ssm', region_name=region)
          s3_client = boto3.client('s3', region_name=region)
          
          def lambda_handler(event, context):
            try:
              if event['RequestType'] == CREATE:
                ssm_response = ssm_client.get_parameter(Name=urls_parameter)
                urls = ssm_response['Parameter']['Value']
                
                result = subprocess.run(
                  ['mkdir', package_dir_path],
                  stdout=subprocess.PIPE,
                  stderr=subprocess.PIPE
                )
                
                for url in urls.splitlines():
                  print(url)
                  file_name = os.path.basename(url)
                  download_path = os.path.join(package_dir_path, file_name)
                  
                  data = urllib.request.urlopen(url).read()
                  
                  with open(download_path, mode='wb') as f:
                    f.write(data)

                shutil.make_archive(
                  os.path.splitext(layer_package_path)[0],
                  format='zip',
                  root_dir=work_dir,
                  base_dir=package_dir
                )

                s3_client.upload_file(
                  layer_package_path,
                  s3_bucket,
                  os.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)
      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関数で実行するコードをインライン表記で定義します。
詳細につきましては、以下のページをご確認ください。

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

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

  • os.environにアクセスして、CloudFormationテンプレートで定義した環境変数を取得する。
  • Boto3でSSMパラメータストアにアクセスして、URLのリストを取得する。
  • urllib.request.urlopenでファイルをダウンロード後、ファイルに書き込む。
  • インストールしたライブラリをshutil.make_archiveでZIPファイル化する。
  • 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/${UrlsParameter}"
              - Effect: Allow
                Action:
                  - s3:PutObject
                Resource:
                  - !Sub "arn:aws:s3:::${CodeS3Bucket}/*"
Code language: YAML (yaml)

AWS管理ポリシーであるAWSLambdaBasicExecutionRoleに加えて、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
      Environment:
        Variables:
          DIR_PATH: /opt/python
          FILE_NAME: gvk9-iz74.json
      Code:
        ZipFile: |
          import json
          import os
          
          dir_path = os.environ['DIR_PATH']
          file_name = os.environ['FILE_NAME']
          
          def lambda_handler(event, context):
            file_path = os.path.join(dir_path, file_name)
            
            with open(file_path, 'r') as f:
              json_data = json.load(f)
              print(json_data)
              
              return {
                'statusCode': 200,
                'body': json.dumps(json_data, indent=2)
              }
      FunctionName: !Sub "${Prefix}-function2"
      Handler: !Ref Handler
      Layers:
        - !Ref LambdaLayer
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole2.Arn
      Timeout: !Ref Timeout

  FunctionUrl:
    Type: AWS::Lambda::Url
    Properties:
      AuthType: NONE
      TargetFunctionArn: !GetAtt Function2.Arn

  FunctionUrlPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunctionUrl
      FunctionName: !GetAtt Function2.Arn
      FunctionUrlAuthType: NONE
      Principal: "*"

  FunctionRole2:
    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
Code language: YAML (yaml)

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

Layersプロパティで、先述のLambdaレイヤーを指定します。
レイヤーに含まれているJSONファイルを開いて、内容を返します。

Lambdaレイヤーのコンテンツの配置場所がポイントです。

Lambda 関数にレイヤーを含めると、Lambda は関数実行環境で/optディレクトリにレイヤーコンテンツを抽出します。

関数からレイヤーコンテンツにアクセスする

この関数の呼び出し方法ですが、Function URLを作成します。
Function URLの詳細につきましては、以下のページをご確認ください。

環境構築

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

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

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

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

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

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

Detail of CloudFormation 1.

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

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

Detail of SSM 1.

ダウンロードし、Lambdaレイヤーに含めるファイルのURLが登録されています。

動作確認

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

Lambda関数の実行結果

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

Detail of Lambda 1.

CloudFormationカスタムリソースによって、本関数が実行されたことがわかります。
そしてファイルをダウンロードしたことも確認できます。
つまりLambdaレイヤーパッケージが正常に作成されたということです。

S3バケット

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

Detail of S3 1.

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

Lambdaレイヤー

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

Detail of Lambda 2.

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

確認用Lambda関数

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

Detail of Lambda 3.

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

準備が整いましたので、この関数のFunction URLにアクセスします。

$ curl https://24ugu7yzkz6jl3lqrskebwwpoa0gnymj.lambda-url.ap-northeast-1.on.aws/
[
  {
    "center": "Kennedy Space Center",
    "center_search_status": "Public",
    "facility": "Control Room 2/1726/HGR-S ",
    "occupied": "1957-01-01T00:00:00.000",
    "record_date": "1996-03-01T00:00:00.000",
    "last_update": "2015-06-22T00:00:00.000",
    "country": "US",
    "contact": "Sheryl Chaffee",
    "phone": "321-867-8047",
    "location": {
      "latitude": "28.538331",
      "longitude": "-81.378879",
      "human_address": "{\"address\": \"\", \"city\": \"\", \"state\": \"\", \"zip\": \"32899\"}"
    },
    "city": "Kennedy Space Center",
    "state": "FL",
    "zipcode": "32899",
    ":@computed_region_bigw_e76g": "173",
    ":@computed_region_cbhk_fwbd": "30",
    ":@computed_region_nnqa_25f4": "1078"
  },
  ...
]Code language: Bash (bash)

JSONファイルの中身が返ってきました。
Lambdaレイヤーにこのファイルが含まれており、関数からこのファイルにアクセスできることがわかります。

まとめ

CloudFormationカスタムリソースを使用することによって、一般ファイルを含むLambdaレイヤー作成を自動化することができました。

タイトルとURLをコピーしました