サーバーレス構成をCloudFormationで構築

## はじめに
簡単なサーバーレス環境の構築をCloudFormationを使って構築してみました。

背景として、よくある構成だと思うので、プロジェクトなどでちょっとした検証をするときに参考になるかと思い整理しました。


## 構成
- EventBridge
  - EventBridgeを起点にStep Functionsを実行する
- Step Functions
  - Lambdaのワークフロー
- Lambda
  - アップロード or 削除
- S3
  -  

![template.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415982/4b411e31-c73e-a05b-d218-b10a52e1827a.png)


## テンプレート
```yaml:template.yaml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  BucketName:
    Type: String

  PutLambdaImageUri:
    Type: String

  DeleteLambdaImageUri:
    Type: String

Resources:
  # ====================
  # S3 Buckets
  # ====================

  Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  # ====================
  # Step Functions
  # ====================

  StateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      DefinitionString: !Sub
        - |-
          {
            "Comment": "Put and Delete S3 Object",
            "StartAt": "Put",
            "States": {
                "Put": {
                    "Type": "Task",
                    "Resource": "arn:aws:states:::lambda:invoke",
                    "Parameters": {
                        "Payload.$": "$",
                        "FunctionName": "${PutLambdaArn}"
                    },
                    "Next": "Wait",
                    "OutputPath": "$.Payload"
                },
                "Wait": {
                    "Type": "Wait",
                    "Seconds": 60,
                    "Next": "Delete"
                },
                "Delete": {
                    "Type": "Task",
                    "Resource": "arn:aws:states:::lambda:invoke",
                    "Parameters": {
                        "Payload.$": "$",
                        "FunctionName": "${DeleteLambdaArn}"
                    },
                    "End": true
                }
            }
          }
        - PutLambdaArn: !GetAtt PutLambda.Arn
          DeleteLambdaArn: !GetAtt DeleteLambda.Arn
      RoleArn: !GetAtt StateMachineRole.Arn

  # ====================
  # Lambda Functions
  # ====================

  PutLambda:
    Type: AWS::Lambda::Function
    Properties:
      Role: !GetAtt LambdaRole.Arn
      PackageType: Image
      Code:
        ImageUri: !Ref PutLambdaImageUri
      Environment:
        Variables:
          BUCKET_NAME: !Ref Bucket

  DeleteLambda:
    Type: AWS::Lambda::Function
    Properties:
      Role: !GetAtt LambdaRole.Arn
      PackageType: Image
      Code:
        ImageUri: !Ref DeleteLambdaImageUri
      Environment:
        Variables:
          BUCKET_NAME: !Ref Bucket

  # ====================
  # EventBridge Rules
  # ====================

  StateMachineEventsRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: rate(5 minutes)
      State: ENABLED
      Targets:
        - Arn: !GetAtt StateMachine.Arn
          Id: !GetAtt StateMachine.Name
          RoleArn: !GetAtt EventsRuleRole.Arn

  # ====================
  # Roles
  # ====================

  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AmazonS3FullAccess

  StateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - !Sub states.${AWS::Region}.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: LambdaExecutionPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - lambda:InvokeFunction
                Resource: "*"

  EventsRuleRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: StateMachineExecutionPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - states:StartExecution
                Resource: "*"
```

## Lambda
### 更新Lambda
```python:update.py
import json
import os
from datetime import datetime

import boto3


def handler(_event, _context):
    client = boto3.client("s3")

    key = f"sample-{datetime.timestamp(datetime.now())}.json"
    client.put_object(
        Body=json.dumps({"message": "sample"}).encode("utf-8"),
        Bucket=os.getenv("BUCKET_NAME"),
        Key=key,
    )

    return {
        "statusCode": 200,
        "body": json.dumps("Put S3 Object"),
        "key": key,
    }
```

### 削除Lambda
```python:delete.py
import json
import os

import boto3


def handler(event, _context):
    key = event["key"]

    client = boto3.client("s3")

    client.delete_object(
        Bucket=os.getenv("BUCKET_NAME"),
        Key=key,
    )

    return {
        "statusCode": 200,
        "body": json.dumps("Delete S3 Object"),
    }
```

## まとめ
今回であれば、結果だけ見ると何もしていないように見えますが、簡単なサーバーレスの構成を検討構築してみました。

あまり難しいことはやっていないので、AWS環境がある方はぜひ試してみてください。