S3オブジェクトをトリガーにした zip展開するLambda構築ハンズオン

## はじめに
##### 先日他のアカウントから、自分の保有しているS3に対してzipファイルが送られてくる事案がありました。
##### どうせなら受け取った際に『zipを展開出来ないか？』とのことだったので、展開するためのLambdaを構築したので記事として残します。
------------

## 構成図
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-18-9.50.27-640x205.png)

------------

## ハンズオン
### 構築の流れ
#### 1.S3作成
#### 2.Lambda作成
#### 3.EventBridge作成
##### 上記の順番で構築を行なっていきます。

##### 最終的には、今回作成したS3にzipファイルを保存すると、EventBridgeで検知して、展開Lambdaでzipファイルの展開を行います。
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-18-9.18.33-640x52.png)
##### ※`20221217.zip`をS3に保存すると、EventBridgeで検知して、展開Lambdaが起動されて`sample.txt`が展開されてアップロードされます（20221217.zipを削除することもLambdaで可能でしたが、S3にライフサイクルを導入しているので設定しませんでした。）

-----------

### 1.S3作成
##### EventBridgeに通知をするために`NotificationConfiguration`の`EventBridgeConfiguration`を`true`にしています。
##### S3のバケット名は全世界でユニークのため、`cfn-s3-20221217-inamura`は各自修正ください
##### ※現在既に`cfn-s3-20221217-inamura`は削除されております

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation to create S3 Bucket
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "S3 Configuration"
        Parameters:
        - S3BucketName
        - AccessControl
        - BlockPublicAcls
        - BlockPublicPolicy
        - IgnorePublicAcls
        - RestrictPublicBuckets
        - ExpirationInDays
        - EventBridgeConfiguration
# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  S3BucketName:
    Type: String
    Default: "cfn-s3-20221217-inamura"
    Description: Type of this BacketName.
  VersioningConfiguration:
    Type: String
    Default: "Enabled"
    Description: VersioningConfiguration.
  AccessControl:
    Type: String
    Description: AccessControl.
    Default: "Private"
    AllowedValues: [ "Private", "PublicRead", "PublicReadWrite", "AuthenticatedRead", "LogDeliveryWrite", "BucketOwnerRead", "BucketOwnerFullControl", "AwsExecRead" ]
  BlockPublicAcls: 
    Type: String
    Description: BlockPublicAcls.
    Default: "True"
    AllowedValues: [ "True", "False" ]
  BlockPublicPolicy:
    Type: String
    Description: BlockPublicPolicy.
    Default: "True"
    AllowedValues: [ "True", "False" ]
  IgnorePublicAcls:
    Type: String
    Description: IgnorePublicAcls.
    Default: "True"
    AllowedValues: [ "True", "False" ]
  RestrictPublicBuckets:
    Type: String
    Description: RestrictPublicBuckets.
    Default: "True"
    AllowedValues: [ "True", "False" ]
  ExpirationInDays:
    Type: String
    Description: Lifecycle Days.
    Default: "7"
  EventBridgeConfiguration:
    Type: String
    Description: EventBridgeConfiguration.
    Default: "true"
    AllowedValues: [ "true", "false" ]

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
#  S3
# ------------------------------------------------------------#
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref S3BucketName      
      VersioningConfiguration:
        Status: !Ref VersioningConfiguration
      AccessControl: !Ref AccessControl
      PublicAccessBlockConfiguration:
        BlockPublicAcls: !Ref BlockPublicAcls
        BlockPublicPolicy: !Ref BlockPublicPolicy
        IgnorePublicAcls: !Ref IgnorePublicAcls
        RestrictPublicBuckets: !Ref RestrictPublicBuckets
      LifecycleConfiguration:
        Rules:
          - Id: LifeCycleRule
            Status: Enabled
            ExpirationInDays: !Ref ExpirationInDays
      NotificationConfiguration:
        EventBridgeConfiguration: 
          EventBridgeEnabled: !Ref EventBridgeConfiguration

# ------------------------------------------------------------#
#  Outputs
# ------------------------------------------------------------#
Outputs:
  S3BucketName:
    Value: !Ref S3Bucket
    Export:
      Name: cfn-s3-BucketName
```

### 2.Lambda作成
##### EventBridgeのイベント通知を受けて、Lambdaの/tmp/配下にzipファイルを配置して、展開を行いS3にアップロードを行うLambdaを構築します。
##### Lambdaのエフェメラルストレージ(/tmp)はデフォルトの512MBのままで、毎回/tmpは以下を削除する記述をしています。削除しないと、こちらのブログで記載されている[「Lambda突然の死」「思わぬリソースが枯渇」AWSしくじり先生 part.2](https://logmi.jp/tech/articles/323795)のような問題が発生する可能性があったからです。
##### 展開するzipファイルの容量によっていは、/tmp配下を変更することも必要かと思います。Lambdaは現在10GBまでエフェメラルストレージをサポートしています。
##### 参考URL：[AWS Lambda が最大 10 GB のエフェメラル ストレージをサポートするようになりました](https://aws.amazon.com/jp/blogs/aws/aws-lambda-now-supports-up-to-10-gb-ephemeral-storage/)

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Description:
  Lambda Create
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Lambda Configuration"
        Parameters:
        - FunctionName
        - Description
        - Handler
        - MemorySize
        - Runtime
        - Timeout

# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  FunctionName:
    Type: String
    Default: "cfn-lmd-deployment-inamura"
  Description:
    Type: String
    Default: "cfn-lmd-deployment-inamura"
  Handler:
    Type: String
    Default: "index.lambda_handler"
  MemorySize:
    Type: String
    Default: "128"
  Runtime:
    Type: String
    Default: "python3.9"
  Timeout:
    Type: String
    Default: "180"

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
#  Lambda
# ------------------------------------------------------------#
  Lambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          import boto3
          import os
          import urllib.parse
          import zipfile
          import glob

          s3 = boto3.client('s3')
        
          def lambda_handler(event, context):
              FileListInZip = []
              Object = urllib.parse.unquote_plus(event['detail']['object']['key'], encoding='utf-8')
              Bucket = urllib.parse.unquote_plus(event['detail']['bucket']['name'], encoding='utf-8')
              FilePath = '/tmp/' + Object
              
              #/tmp/配下保存
              s3.download_file(Bucket, Object, FilePath)

              #zipファイル内一覧取得
              zfile = zipfile.ZipFile(FilePath)
              FileListInZip = zfile.namelist()
              
              #/tmp/配下展開
              zfile.extractall('/tmp/')
              zfile.close()

              #展開したファイルをS3アップロード
              for f in FileListInZip:
                  try:
                      if os.path.isfile('/tmp/' + f):
                          s3.upload_file('/tmp/'+f, Bucket, f)
                  except Exception as e:
                      print(e)
                      pass
              else:
                  pass
              
              #Lambda /tmp/配下削除
              for p in glob.glob('/tmp/' + '*'):
                  if os.path.isfile(p):
                      os.remove(p)

              return 0

      Description: !Ref Description
      FunctionName: !Ref FunctionName
      Handler: !Ref Handler 
      MemorySize: !Ref MemorySize
      Runtime: !Ref Runtime
      Timeout: !Ref Timeout
      Role: !GetAtt LambdaRole.Arn

  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${FunctionName}-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: lambda.amazonaws.com
              
      Policies:
        - PolicyName: !Sub "${FunctionName}-policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "logs:CreateLogGroup"
                Resource:  !Sub "arn:${AWS::Partition}:logs:*:*:*"

              - Effect: "Allow"
                Action:
                  - "s3:GetObject"
                  - "s3:PutObject"
                  - "s3:DeleteObject"
                Resource: !Sub 
                  - "arn:aws:s3:::${BucketName}/*"
                  - BucketName: {'Fn::ImportValue': cfn-s3-BucketName}

              - Effect: "Allow"
                Action:
                  - "s3:ListBucket"
                Resource: !Sub 
                  - "arn:aws:s3:::${BucketName}"
                  - BucketName: {'Fn::ImportValue': cfn-s3-BucketName}

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#                
Outputs:
  LambdaArn:
    Value: !GetAtt Lambda.Arn
    Export:
      Name: !Sub "${FunctionName}-arn"
```

### 3.EventBridge作成
##### S3にオブジェクト（条件：サフィックスが`.zip`）が保存された場合、EventBridgeが検知をして、展開Lambdaを呼び出す
##### ※zip展開をした中に、zipファイルがあった場合は再度EventBridgeで通知を受けるため、展開Lambdaが起動する

```yaml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  EventBridge gets s3 events and sends them to lambda
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Eventbridge Configuration"
        Parameters:
          - Name
          - EventBusName
          - State
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  EventBusName:
    Type: String
    Default: "default"
  Name:
    Type: String
    Default: "cfn-evb-deploymentlmd-inamura"
  State:
    Type: String
    Default: "ENABLED"

# ------------------------------------------------------------#
#  EventBridge
# ------------------------------------------------------------#
Resources:
  S3EventsRule:
    Type: AWS::Events::Rule
    Properties:
      Description: "Get S3 events and send to Lambda"
      EventBusName: !Ref EventBusName
      Name: !Ref Name
      State: !Ref State
      EventPattern:
        source: 
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name: 
              - !ImportValue cfn-s3-BucketName
          object:
            key:
              - "suffix" : ".zip"
      Targets:
        - Arn: !ImportValue cfn-lmd-deployment-inamura-arn
          Id: "cfn-lmd-deployment-inamura"
  
  PermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: cfn-lmd-deployment-inamura
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt 'S3EventsRule.Arn'
```

### 挙動の確認
#### ①構築したS3にzipファイルを保存する
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-17-21.17.07-640x189.png)
##### ↓
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-17-21.41.35-625x640.png)
##### ※S3は既に削除されています

#### ②S3の画面に戻り更新を押下すると、zipファイルの中身が展開される
##### `20221217.zip`をS3に保存した後、検知して3秒後にzip化されていた`sample.txt`が展開されてアップロードされました
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-18-9.18.33-1-640x52.png)


#### ③展開LambdaのCloudWatchLogsを確認すると、Lambdaが起動されているログを確認することができる
![](https://cloud5.jp/wp-content/uploads/2022/12/スクリーンショット-2022-12-17-21.43.30-640x35.png)

------------

## さいごに
##### LambdaにアタッチするIAMロールですが、最初バケットに対してしか付与しておらず`ClientError: An error occurred (403) when calling the HeadObject operation: Forbidden `が出たりして、改めてIAMロールの記述方法を学び直しました。
##### とかく使い所は局所的（たぶんzip化させる方が機会的には多そう）ですが、無事にzipを展開できて良かったです。
##### 残り少ない今年に捉われることもなく、時間を見つけては検証して、自分の作れるものを増やしていきたいと思います！