CFNのWaitConditionを使用して、Lambdaデプロイパッケージのビルドを待つ

CloudFormationのWaitConditionを使用して、Lambdaデプロイパッケージのビルドを待つ

CFNのWaitConditionを使用して、Lambdaデプロイパッケージのビルドを待つ

CloudFormationを使用して、Lambda関数を作成することを考えます。

以下のページでご紹介した通り、関数を作成する方法は3パターンあります。

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

本ページでは、特にZIPファイルのデプロイパッケージをS3バケットに配置する方法に注目します。

CloudFormationにおいて、この方法でLambda関数を作成する場合、関数リソースが生成されるよりも前に、デプロイパッケージをS3バケットに配置しておく必要があります。
しかしパッケージに含める内容によっては、ビルドに時間を要する場合もあり、関数リソース作成までに間に合わないこともあり得ます。

そこで今回はCloudFormationのWaitConditionを使用します。
具体的には、Lambda関数リソースの生成を、デプロイパッケージの作成まで待機します。

構築する環境

Diagram of using CloudFormation's WaitCondition to wait for the Lambda deploy package to build.

以下の流れでLambda関数3を作成します。

  1. CodeBuildで関数用のデプロイパッケージを作成する。
  2. パッケージをS3バケットに配置する。
  3. S3バケットのイベント通知先にEventBridgeを指定する。
  4. S3のオブジェクト作成に関するEventBridgeルールを作成し、Lambda関数2をトリガーする。
  5. 関数2では、CloudFormationのWaitConditionのシグナルを発信する。
  6. WaitConditionのシグナルを受けて、S3バケットのデプロイパッケージを用いて、Lambda関数3が作成する。

CodeBuildプロジェクトを開始するキッカケですが、CloudFormationカスタムリソースを使用します。
具体的には、カスタムリソースに紐づくLambda関数でCodeBuildプロジェクトをトリガーします。

なおLambda関数のランタイム環境はPython3.8とします。

CloudFormationテンプレートファイル

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

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

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

S3バケット

Resources:
  Bucket:
    Type: AWS::S3::Bucket
    Properties:
      AccessControl: Private
      BucketName: !Ref Prefix
      NotificationConfiguration:
        EventBridgeConfiguration:
          EventBridgeEnabled: true
Code language: YAML (yaml)

デプロイパッケージを配置するS3バケットです。

ポイントはイベント通知機能です。
今回の構成では、S3バケットにパッケージが配置されたことをトリガーとして、Lambda関数2を実行する必要があります。
NotificationConfigurationプロパティでEventBridge通知機能を有効化します。

なお今回はイベント通知先としてEventBridgeを選択しましたが、直接Lambda関数を呼び出すこともできます。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
S3イベント通知の4つの送信先 – SNS/SQS/Lambda/EventBridge 【S3イベント通知の4つの送信先 - SNS/SQS/Lambda/EventBridge】 以下のページで、S3のイベント通知機能を使用して、アップロードされた画像から、自動的にサムネイル画...

CodeBuild

Resources:
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: NO_ARTIFACTS
      Cache:
        Type: NO_CACHE
      Environment:
        ComputeType: !Ref ProjectEnvironmentComputeType
        EnvironmentVariables:
          - Name: BUCKET_NAME
            Type: PLAINTEXT
            Value: !Ref BucketName
          - Name: SOURCE_BUNDLE_NAME
            Type: PLAINTEXT
            Value: !Ref SourceBundleName
          - Name: SOURCE_FILE_NAME
            Type: PLAINTEXT
            Value: !Ref SourceFileName
        Image: !Ref ProjectEnvironmentImage
        ImagePullCredentialsType: CODEBUILD
        Type: !Ref ProjectEnvironmentType
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          Status: DISABLED
        S3Logs:
          Status: DISABLED
      Name: !Ref Prefix
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Source:
        Type: NO_SOURCE
        BuildSpec: !Sub |
          version: 0.2

          phases:
            pre_build:
              commands:
                - |
                  cat << EOF > $SOURCE_FILE_NAME
                  def lambda_handler(event, context):
                    return 'hogehoge'
                  EOF
            build:
              commands:
                - zip $SOURCE_BUNDLE_NAME -r * .[^.]*
                - sleep 300
            post_build:
              commands:
                - aws s3 cp $SOURCE_BUNDLE_NAME s3://$BUCKET_NAME/
      Visibility: PRIVATE
Code language: YAML (yaml)

CodeBuildを使用して、Lambda関数用のデプロイパッケージを作成します。

BuildSpecプロパティでbuildspec.yamlの中身を記載します。
pre_buildフェーズで関数用のソースコードを作成します。
buildフェーズでデプロイパッケージを作成します。
具体的には、前フェーズで作成したファイルをZIP化します。
なおこのビルドは時間を要するものとするため、sleepコマンドで5分待機させます。
post_buildフェーズで、デプロイパッケージをS3バケットにアップロードします。

以下がCodeBuild用のIAMロールです。

Resources:
  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: PutS3ObjectPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:PutObject
                Resource:
                  - !Sub "arn:aws:s3:::${BucketName}/*"
Code language: YAML (yaml)

S3バケットにオブジェクトを配置するための権限を付与します。

Lambda関数1

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt Function1.Arn

  Function1:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          CODEBUILD_PROJECT: !Ref CodeBuildProject
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import os

          codebuild_project = os.environ['CODEBUILD_PROJECT']

          CREATE = 'Create'
          response_data = {}

          client = boto3.client('codebuild')

          def lambda_handler(event, context):
            try:
              if event['RequestType'] == CREATE:
                response = client.start_build(
                  projectName=codebuild_project
                )
                print(response)

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

            except Exception as e:
              print(e)
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
      FunctionName: !Sub "${Prefix}-function-01"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole1.Arn
Code language: YAML (yaml)

Lambda関数1はCodeBuildプロジェクトを自動的に開始するために使用します。
具体的には、CloudFormationカスタムリソースに本関数を関連づけます。

CloudFormationカスタムリソースに関する詳細は、以下のページをご確認ください。

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

今回はカスタムリソース作成時に、boto3のCodeBuild用クライアントオブジェクトのstart_buildメソッドを実行します。
これで自動的にCodeBuildプロジェクトを開始させます。

以下が本関数用のIAMロールです。

Resources:
  FunctionRole1:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    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: StartCodeBuildPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - codebuild:StartBuild
                Resource:
                  - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:project/${CodeBuildProject}"
Code language: YAML (yaml)

CodeBuildプロジェクトを開始させるための権限を与えます。

EventBridge

Resources:
  EventsRule:
    Type: AWS::Events::Rule
    Properties:
      EventBusName: !Ref EventBusName
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref BucketName
      Name: !Sub "${Prefix}-EventsRule"
      State: ENABLED
      Targets:
        - Arn: !GetAtt Function2.Arn
          Id: !Ref Function2
Code language: YAML (yaml)

EventBridgeルールです。
先述のS3バケットにオブジェクトが作成された場合、後述のLambda関数2を実行します。

ターゲットにLambda関数を指定する場合、EventBridgeが関数を呼び出すという挙動になります。
そのためEventBridgeに関数を呼び出す権限を与える必要があります。

Resources:
  EventsRulePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref Function2
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventsRule.Arn
Code language: YAML (yaml)

WaitCondition

AWS公式サイトでは、WaitConditionについて以下のように説明されています。

待機条件および待機条件ハンドルを使用して、AWS CloudFormation にスタックの作成を一時停止させ、シグナルを待ってから作成を続行させることができます。

テンプレートでの待機条件の作成

具体的には、以下の通りに両リソースを定義します。

Resources:
  WaitConditionHandle:
    Type: AWS::CloudFormation::WaitConditionHandle

  WaitCondition:
    Type: AWS::CloudFormation::WaitCondition
    Properties:
      Handle: !Ref WaitConditionHandle
      Timeout: !Ref WaitConditionTimeout
Code language: YAML (yaml)

WaitConditionのTimeoutプロパティに600を指定しました。
これで最長10分間はシグナル受信を待機します。
なお10分待機してもシグナルを受信しない場合は失敗となり、CloudFormationスタックがロールバックすることになります。

Lambda関数2

Resources:
  Function2:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - !Ref Architecture
      Environment:
        Variables:
          SIGNAL_URL: !Ref WaitConditionHandle
      Code:
        ZipFile: |
          import json
          import os
          import urllib3
          import uuid

          def lambda_handler(event, context):
            body = json.dumps({
                "Status": "SUCCESS",
                "Reason": "Lambda Deploy Package Setup Successed",
                "UniqueId": str(uuid.uuid4()),
                "Data": "Lambda Deploy Package Setup Successed"
            })
            http = urllib3.PoolManager()
            http.request('PUT', os.environ['SIGNAL_URL'], body=body)
      FunctionName: !Sub "${Prefix}-function-02"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole2.Arn
Code language: YAML (yaml)

本関数の働きはWaitConditionに成功シグナルを通知することです。
実装につきましては、以下のページを参考にしました。

https://medium.com/superluminar/waiting-for-aws-eventbridge-events-with-aws-cloudformation-wait-conditions-456d0326f95b

この関数は先述のEventBridgeルールに関連づいています。
つまりCodeBuildによって、S3バケットにデプロイパッケージが配置されたことをトリガーとして、本関数が実行されます。
ですからS3バケットにデプロイパッケージが配置されるまで、CloudFomrationのリソース作成を待機させることができます。

Lambda関数3

Resources:
  Function3:
    Type: AWS::Lambda::Function
    DependsOn:
      - WaitCondition
    Properties:
      Architectures:
        - !Ref Architecture
      Code:
        S3Bucket: !Ref BucketName
        S3Key: !Ref SourceBundleName
      FunctionName: !Sub "${Prefix}-function-03"
      Handler: !Ref Handler
      PackageType: Zip
      Runtime: !Ref Runtime
      Role: !GetAtt FunctionRole2.Arn
Code language: YAML (yaml)

この関数はS3バケットに配置したデプロイパッケージを使用します。

ポイントはDependsOnプロパティです。
先ほどのWaitConditionが指定されています。
つまりWaitConditionに成功シグナルが通知されるまで、本関数の作成は待機されるということです。
WaitConditionを使用することで、S3バケットにデプロイパッケージが配置される前に、リソース作成が開始されることを防止します。

環境構築

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

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

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

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

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

  • S3バケット:fa-142
  • CodeBuildプロジェクト:fa-142
  • Lambda関数1:fa-142-function-01
  • Lambda関数2:fa-142-function-02
  • Lambda関数3:fa-142-function-03
  • EventBridgeルール:fa-142-EventsRule

作成されたリソースをAWS Management Consoleから確認します。

CodeBuildプロジェクトを確認します。

Detail of CodeBuild 1.

CloudFormationで定義した通りに、リソースが作成されています。

特にポイントなのが、buildspecです。
関数用のデプロイパッケージを実行過程で、300秒待機した上で、S3バケットに配置します。
これでビルドに時間を要する状況を再現しています。

S3バケットを確認します。

Detail of S3 1.

EventBridgeへのイベント通知機能が有効化されています。

EventBridgeルールを確認します。

Detail of EventBridge 1.
Detail of EventBridge 2.

イベントパターンを見ると、先述のS3バケットでオブジェクトが配置された時に、イベント通知する内容であることがわかります。

イベントのターゲットを見ると、後述するLambda関数2が指定されています。

Lambda関数1を確認します。

Detail of Lambda 1.

この関数はCloudFormationカスタムリソースに関連づけて使用します。
CloudFormationスタックの作成時に、本関数が動作します。
この関数の働きは、先述のCodeBuildプロジェクトを開始することです。

Lambda関数2を確認します。

Detail of Lambda 2.

この関数の働きは、WaitConditionに成功シグナルを通知することです。

動作確認

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

改めてCodeBuildプロジェクトを確認します。

Detail of CodeBuild 2.

プロジェクトが開始されています。
CloudFormationカスタムリソースの働きによって、これに関連づいているLambda関数1が実行されて、本プロジェクトが開始されたということです。

フェーズの詳細を見ると、ビルド中であることがわかります。
ビルドフェーズでsleepコマンドを実行することになっているため、これで待機中ということです。

CloudFormationスタックの作成状況を確認します。

Detail of CloudFormation 1.

ポイントは2つです。

1点目はWaitConditionが作成中である点です。
これによって、成功シグナルを受信するまで、スタックの作成が待機されている状態です。

2点目はLambda関数3の作成が開始されていない点です。
本関数はWaitConditionが成功シグナルを受信するまで作成されません。
これによってデプロイパッケージがS3バケットに配置されるまで、関数リソースの作成を待機できます。

しばらく待機すると、CodeBuildプロジェクトが正常終了します。

Detail of CodeBuild 3.

確かにビルドに300秒かかっていますね。
CodeBuildプロジェクトが正常に終了したことによって、S3バケットにデプロイパッケージが配置されているはずです。

S3バケットを確認します。

Detail of S3 2.

確かにデプロイパッケージが配置されています。

オブジェクト配置を受けて、EventBridgeがLambda関数2をトリガーします。

改めてCloudFormationスタックの作成状況を確認します。

Detail of CloudFormation 2.

WaitConditionのステータスが完了とあります。

これを受けて、Lambda関数3の作成が開始されています。

しばらく待機した後に、Lambda関数3を確認します。

Detail of Lambda 3.

確かにLambda関数3が作成されています。

このようにWaitConditionを使用することによって、リソースの作成のタイミングを制御することができます。

まとめ

CloudFormationのWaitConditionを使用sて、Lambda関数リソースの生成を、デプロイパッケージの作成まで待機する方法をご紹介しました。