AWS日記15 (Serverless Application Model)

# はじめに
今回は AWS Serverless Application Model (AWS SAM)を試します。
アクセスすると自己削除するWebページを作成します。
[Lambda関数・SAMテンプレート]
(https://github.com/tanaka-takurou/serverless-application-ephemerality-page-go/tree/minimum)


# 準備
[AWS SAM CLI をインストールします](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
[S3の準備をします](https://qiita.com/tanaka_takurou/items/de897b0906087cec1a86)

[AWS SAMの資料]
[AWS サーバーレスアプリケーションモデル](https://aws.amazon.com/jp/serverless/sam/)
[AWS Serverless Application Model](https://docs.aws.amazon.com/ja_jp/serverless-application-model/index.html)


# AWS SAM テンプレート作成

AWS SAM テンプレートで API-Gateway , Lambdaの設定をします。

[参考資料]
[AWS SAM テンプレートを作成する](https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/tutorial-lambda-sam-template.html)

<details><summary>template.yml</summary><div>

```yaml:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless Application Ephemerality

Parameters:
  ApplicationName:
    Type: String
    Default: 'ServerlessApplicationEphemeralityMinimum'
  EphemeralityFunctionName:
    Type: String
    Default: 'EphemeralityFunctionMinimum'

Metadata:
  AWS::ServerlessRepo::Application:
    Name: Serverless-Application-Minimum
    Description: 'This application is deleted when accessed.'
    Author: tanaka-takurou
    SpdxLicenseId: MIT
    LicenseUrl: LICENSE.txt
    ReadmeUrl: README.md
    Labels: ['ServerlessRepo']
    HomePageUrl: https://github.com/tanaka-takurou/serverless-application-ephemerality-page-go/tree/minimum
    SemanticVersion: 0.0.2
    SourceCodeUrl: https://github.com/tanaka-takurou/serverless-application-ephemerality-page-go/tree/minimum

Resources:
  EphemeralityApi:
    Type: AWS::Serverless::HttpApi
  EphemeralityFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref EphemeralityFunctionName
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Front Function'
      Role: !GetAtt EphemeralityFunctionRole.Arn
      Events:
        EphemeralityApi:
          Type: HttpApi
          Properties:
            Path: '/'
            Method: get
            ApiId: !Ref EphemeralityApi
      Environment:
        Variables:
          COUNT: "0"
          LIMIT: "0"
          REGION: !Ref AWS::Region
          STACK_NAME: !Ref AWS::StackName
          FUNCTION_NAME: !Ref EphemeralityFunctionName
  EphemeralityApiPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref EphemeralityFunction
      Principal: apigateway.amazonaws.com
  EphemeralityFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'lambda.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: KillFunctionPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'cloudformation:DescribeStackResources'
                  - 'cloudformation:DeleteStack'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'lambda:*'
                  - 'events:RemoveTargets'
                  - 'events:DeleteRule'
                  - 'iam:DeleteRolePolicy'
                  - 'iam:DeleteRole'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'apigateway:*'
                Resource: '*'

Outputs:
  APIURI:
    Value: !Join [ '', [ 'https://', !Ref EphemeralityApi, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/'] ]
```
</div></details>

### API-Gatewayの設定は以下の部分
```yaml
  EphemeralityApi:
    Type: AWS::Serverless::HttpApi
```


### Lambdaの設定は以下の部分
```yaml
  EphemeralityFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref EphemeralityFunctionName
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Front Function'
      Role: !GetAtt EphemeralityFunctionRole.Arn
      Events:
        EphemeralityApi:
          Type: HttpApi
          Properties:
            Path: '/'
            Method: get
            ApiId: !Ref EphemeralityApi
      Environment:
        Variables:
          COUNT: "0"
          LIMIT: "0"
          REGION: !Ref AWS::Region
          STACK_NAME: !Ref AWS::StackName
          FUNCTION_NAME: !Ref EphemeralityFunctionName
```


# Lambda関数作成

※ Lambda関数は aws-lambda-go を利用し、cloudformationの周りの処理は aws-sdk-go-v2 を利用しました。

<details><summary>main.go</summary><div>

```golang:main.go
package main

import (
	"os"
	"log"
	"context"
	"strconv"
	"net/http"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/aws/external"
	slambda "github.com/aws/aws-sdk-go-v2/service/lambda"
	"github.com/aws/aws-sdk-go-v2/service/cloudformation"
)

var cfg aws.Config

func HandleRequest(ctx context.Context, request events.APIGatewayV2HTTPRequest) (events.APIGatewayProxyResponse, error) {
	count, _ := strconv.Atoi(os.Getenv("COUNT"))
	limit, _ := strconv.Atoi(os.Getenv("LIMIT"))
	if count < limit {
		client := slambda.New(cfg)
		req := client.GetFunctionConfigurationRequest(&slambda.GetFunctionConfigurationInput{
			FunctionName: aws.String(os.Getenv("FUNCTION_NAME")),
		})
		res, err := req.Send(ctx)
		if err != nil {
			log.Println(err)
		} else {
			env := res.GetFunctionConfigurationOutput.Environment.Variables
			env["COUNT"] = strconv.Itoa(count + 1)
			req_ := client.UpdateFunctionConfigurationRequest(&slambda.UpdateFunctionConfigurationInput{
				FunctionName: aws.String(os.Getenv("FUNCTION_NAME")),
				Environment: &slambda.Environment{
					Variables: env,
				},
			})
			_, err := req_.Send(ctx)
			if err != nil {
				log.Println(err)
			}
		}
	} else {
		client := cloudformation.New(cfg)
		req := client.DeleteStackRequest(&cloudformation.DeleteStackInput{
			StackName: aws.String(os.Getenv("STACK_NAME")),
		})
		_, err := req.Send(ctx)
		if err != nil {
			log.Println(err)
		}
	}
	return events.APIGatewayProxyResponse{
		StatusCode:      http.StatusOK,
		IsBase64Encoded: false,
		Body:            "<html><head><title>Serverless Application Ephemerality</title></head><body><span>Serverless Application Ephemerality</span></body></html>",
		Headers: map[string]string{
			"Content-Type": "text/html",
		},
	}, nil
}

func init() {
	var err error
	cfg, err = external.LoadDefaultAWSConfig()
	cfg.Region = os.Getenv("REGION")
	if err != nil {
		log.Print(err)
	}
}

func main() {
	lambda.Start(HandleRequest)
}
```
</div></details>

### スタックを削除するには DeleteStackRequest を使う
```golang

client := cloudformation.New(cfg)
req := client.DeleteStackRequest(&cloudformation.DeleteStackInput{
	StackName: aws.String(os.Getenv("STACK_NAME")),
})
_, err := req.Send(ctx)
```
※ この処理により 作成したWebページが削除されます。


# デプロイ
```shell
sam package --output-template-file packaged.yml --s3-bucket "${bucket}"
sam deploy --stack-name "${stack}" --capabilities CAPABILITY_IAM --template-file packaged.yml
```
※ ${bucket} には 「準備」 で作成した S3バケット名を入力
※ ${stack} には スタック名を入力 (既にあるスタックを指定すると、スタックを更新します)

・デプロイが完了すると、作成したWebページにアクセスできるようになります。


・デプロイ完了後 [CloudFormationのスタック一覧](https://ap-northeast-1.console.aws.amazon.com/cloudformation/) に表示されます。

# パブリッシュ
```shell
sam package --output-template-file packaged.yml --s3-bucket "${bucket}"
sam publish --template packaged.yml" --region "${AWS_DEFAULT_REGION}"
```
※ ${bucket} には 「準備」 で作成した S3バケット名を入力
※ ${AWS_DEFAULT_REGION} には リージョン (ap-northeast-1 など) を入力

・パブリッシュ完了後 [Serverless Application Repositoryのマイアプリケーション一覧](https://ap-northeast-1.console.aws.amazon.com/serverlessrepo/) に表示されます。


# 削除
```shell
aws cloudformation delete-stack --stack-name "${stack}"
```
※ ${stack} には スタック名を入力


# 終わりに
Policy周りが原因でデプロイする際にエラーが発生することが多くありました。
使い慣れることで、AWS管理画面よりも簡単に、サーバレスアプリケーションの作成・削除・複製できそうです。

<details><summary>参考資料</summary><div>
[serverless-application-model](https://github.com/aws/serverless-application-model)
[究極のCloudFormationをたずねて三千里](https://qiita.com/Kta-M/items/53bfd638eae21b76250f)
[AWS SAMを使ってみる](https://qiita.com/spring_i/items/e087905a82c40cf900a0)
[AWS SAMのコマンドをまとめてみた](https://qiita.com/gnk263/items/7f8796c26b9b61d33d96)
[AWS SAM アプリケーションをデプロイする](https://qiita.com/yh1224/items/f3e22b886639e2605f2f)
[今日から始めるサーバーレス SAM【API Gateway + Lambda + DynamoDB】](https://qiita.com/y4u0t2a1r0/items/2d27eed7afd4e4ffbab5)
[SAMで作成されるApiGatewayをエッジ最適化以外で作成したいとき](https://qiita.com/tinsep19/items/cb84d1bba795ffeb3ceb)
[aws-sam-cliでLambda,DynamoDBのサーバーレスアプリケーション開発に入門してみる](https://qiita.com/umeneri/items/6fb3f7560f4a878f6dfd)
[AWS SAM で Hello World する](https://qiita.com/mokuo/items/3348f19d12cb9b17295d)
[ゼロから始める AWS SAM 入門](https://qiita.com/OMOIKANESAN/items/b62fb62b8cd04544e1fc)
[SAM で API Gateway の CloudWatch Logs を有効にしたい](https://qiita.com/couzie/items/17b6fb94e92dd43d2743)
[CloudFormationでクソが！って叫んだこと](https://qiita.com/ryo0301/items/c43451dc1ac1eb938f77)
[SAMでtemplate.yamlの記述方法（Events、Policyはここを参照して書く）](https://qiita.com/icck/items/4d1debd8b1710b25a2b2)
[AWS SAM CLI 再入門 2020.07](https://qiita.com/hayao_k/items/7827c3778a23c514e196)
[AWS SAMでローカル環境でS3とDynamoDBを扱うLambdaを実行する](https://qiita.com/Yuki10/items/7a445a108b63b9298071)
[cloudformation/samでsns通知をslackに流すgoのlambdaを作る](https://qiita.com/ymgyt/items/648554a6b5c2cf07a981)
[CloudFormation で Cognito](https://qiita.com/y13i/items/1923b47079bdf7c44eec)
[AWS SAMのテンプレートではリソースごとに!Refと!GetAttの戻り値が違う](https://qiita.com/takamizawach/items/6a84359bd1432242ca41)
[CloudFormationスタック作成エラー: Requires capabilities : [CAPABILITY_NAMED_IAM]](https://qiita.com/akatsukaha/items/796d73e2c736667a6909)
[AWS SAMを利用してGolangなLambdaをデプロイする](https://qiita.com/ikeisuke/items/3c0c422888ae8ae09831)
[多分わかりやすいCloudFormation入門とチュートリアル](https://qiita.com/ryurock/items/766154e0afb8fdb629e2)
[CloudFormationでAWS Lambdaを作成・更新する際のベストプラクティス](https://qiita.com/kihoair/items/fe204fc8582946646d07)
[CloudFormationでAPI Gateway＋LambdaなAPIを作成する](https://qiita.com/hir00/items/c5c0ca61658d4c8949c2)
[AWS SAMを使う前にCloudFormationテンプレートを書こう](https://qiita.com/izanari/items/78258251cced2f713b33)
</div></details>