CFNカスタムリソースでS3オブジェクトを作成・削除する

目次

CloudFormationカスタムリソースを使って、スタック生成/削除時にS3オブジェクトを作成/削除する方法

CloudFormationカスタムリソースはスタック操作(作成、更新、削除)時に、任意のアクションを実行できるというものです。

今回はカスタムリソースを使って、以下の動作を実現します。

  • CloudFormationでS3バケット生成時に、自動的にS3オブジェクトを生成する。
  • S3バケットを含むCloudFormationスタックを削除時に、自動的にバケット内の全オブジェクトを削除する。

なおカスタムリソースの基本的な事項については、以下のページをご確認ください。

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

構築する環境

Diagram of create and delete S3 object by CFN Custom Resource.

CloudFormationスタックを作成し、その内部に2つのリソースを定義します。

1つ目はS3バケットです。
このバケットに対してオブジェクト操作を行います。

2つ目はLambda関数です。
この関数をカスタムリソースとして設定します。
スタック生成・削除時に、自動的にバケットにオブジェクト生成・削除を実行するように設定します。
生成するオブジェクトは静的ウェブサイトホスティング用のindex.htmlとします。
関数のランタイムはPython3.8とします。

CloudFormationテンプレートファイル

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

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

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

本ページはカスタムリソースを使って、S3オブジェクトを操作する方法を中心に取り上げます。

S3静的ウェブサイトホスティング機能については、以下のページをご確認ください。

あわせて読みたい
S3静的ウェブサイトホスティング機能でサイトを公開する 【S3静的ウェブサイトホスティング機能でサイトを公開する構成】 S3の静的ウェブサイトホスティング機能を使って、ウェブサイトを公開する方法を確認します。 ウェブサ...

カスタムリソースとして動作するLambda関数

Resources:
  Function:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import os

          bucket_name = os.environ['BUCKET_NAME']

          object_name = 'index.html'
          object_body = """<html>
            <head></head>
            <body>
              <h1>index.html</h1>
              <p>{bucket_name}</p>
            </body>
          </html>""".format(bucket_name=bucket_name)
          content_type = 'text/html'
          char_code= 'utf-8'

          s3_client = boto3.client('s3')

          CREATE = 'Create'
          DELETE = 'Delete'
          response_data = {}

          def lambda_handler(event, context):
            try:
              if event['RequestType'] == CREATE:
                put_response = s3_client.put_object(
                  Bucket=bucket_name,
                  Key=object_name,
                  Body=object_body.encode(char_code),
                  ContentEncoding=char_code,
                  ContentType=content_type)
                print(put_response)

              elif event['RequestType'] == DELETE:
                list_response = s3_client.list_objects_v2(
                  Bucket=bucket_name)
                for obj in list_response['Contents']:
                  delete_response = s3_client.delete_object(
                    Bucket=bucket_name,
                    Key=obj['Key'])
                  print(delete_response)

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

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

関数そのものの設定に特別な項目はありません。
1つ挙げるとすると、環境変数がポイントです。
オブジェクトを作成・削除するバケット名を環境変数として関数に渡します。

event[‘RequestType’]の値を参照して、スタック操作に応じた処理を実装します。
スタック生成時はこの値が「Create」となりますし、スタック削除時は「Delete」となります。

スタック生成時、つまり「Create」の場合は、HTMLファイル(index.html)を作成します。
put_objectを実行してバケットに保存します。

スタック削除時、つまり「Delete」の場合は、バケット内の全オブジェクトを削除します。
list_objects_v2でバケット内の全オブジェクトを取得後、delete_objectで1つずつ削除します。

関数の実行完了メッセージをCloudFormationスタックに返す必要があります。
今回はcfnresponse.sendを使って実装しています。

なお今回実装したオブジェクト削除に関して、課題が2つあります。

1つ目は1000件以上のオブジェクトを削除することを想定していないことです。
boto3のlist_objects_v2は1000件を超えるオブジェクトデータを取得しようとすると、残りのデータを取得するためのトークンを返すしようとなっています。
今回のコードでは、その辺りの処理が未実装となっています。

2つ目はバージョニングを考慮していない点です。
S3バケットのバージョニングを有効化している場合、オブジェクトのバージョン情報も削除しなければ、バケットを削除することはできません。
今回のコードでは、その辺りの処理が未実装になっています。

以下のページでは、2点を考慮したコードが紹介されています。

https://dev.classmethod.jp/articles/custom-resource-empty-s3-objects/

(参考)S3オブジェクトを作成・削除するためのLambda関数用IAMロール

Resources:
  FunctionRole:
    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: !Sub "${Prefix}-S3Access"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:GetObject
                  - s3:PutObject
                  - s3:DeleteObject
                Resource:
                  - !Ref BucketArn
                  - !Sub "${BucketArn}/*"
Code language: YAML (yaml)

Lambda関数用のIAMロールです。
対象のバケットに対して、内部のオブジェクトを作成・削除できる権限を与える内容です。

カスタムリソース

Resources:
  CustomResource:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !Ref FunctionArn
Code language: YAML (yaml)

ServiceTokenプロパティに、先述のLambda関数のARNを指定します。
この設定によって、CloudFormationスタックの操作時に、都度関数が実行されることになります。

環境構築

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

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

AWS CLIを使ってCloudFormationスタックを作成します。
今回の構成は4つのテンプレートファイルに分割して構成されていますが、これらを任意のバケットに設置します。

以下は任意のS3バケットに配置したテンプレートファイルを参照して、スタックを作成する例です。
なおスタック名は「fa-047」、バケット名は「awstut-bucket」、ファイルを設置しているフォルダ名は「fa-047」とします。

$ aws cloudformation create-stack \
--stack-name fa-047 \
--template-url https://awstut-bucket.s3.ap-northeast-1.amazonaws.com/fa-047/fa-047.yaml \
--capabilities CAPABILITY_IAM
Code language: YAML (yaml)

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

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

AWS Management Consoleからスタックの作成状況を確認します。

CloudFormation Stack 1

コマンドで作成したスタックと、このスタックにネストされた3つのスタックが作成されていることがわかります。

ネストされたスタックの内、S3用スタックから生成されたリソースを確認します。

CloudFormation Stack 2

確かにS3バケットが作成されています。

続いてカスタムリソースとLambda関数を確認します。

CloudFormation Stack 3
CloudFormation Stack 4

どちらも作成されています。
これらが正常に動作する場合、スタック作成時にバケットにファイルを作成しているはずです。

動作確認

準備が整いましたので、バケットの中身を確認します。

HTML file was generated by CFN custom resource.

確かにindex.htmlが作成されています。

続いて静的ウェブサイトホスティング機能のエンドポイントにアクセスします。

HTML file was generated by CFN custom resource.

正常にアクセスできました。
CloudFormationスタック作成時に、カスタムリソースに紐づくLambda関数が実行されて、S3バケットにindex.htmlファイルが設置されました。

次にスタック全体を削除してみます。
本来であれば、S3バケットにオブジェクトが残っている場合、スタック削除に失敗します。

Delete the CloudFormation stack with the object still in the S3 bucket.

しばらく待つと、削除が正常に完了します。

The CloudFormation custom resource deleted the objects in the bucket and then deleted the stack.

このことからCloudFormationスタック削除時に、カスタムリソースに紐づくLambda関数が実行されて、S3バケットに保存されている全オブジェクトが削除されたことがわかります。

まとめ

CloudFormationカスタムリソースを使って、スタック生成/削除時にS3オブジェクトを作成/削除する方法を確認しました。

目次