AWS日記16 (Serverless Application Repository)

# はじめに
今回は AWS Serverless Application Repository を試します。
サーバーレスアプリケーションを管理するページを作成します。
[Lambda関数・SAMテンプレート]
(https://github.com/tanaka-takurou/serverless-application-management-page-go)


# 準備
[サーバーレスアプリケーションの準備をします](https://qiita.com/tanaka_takurou/items/cae7c19fc3aaf0031abb)

[Serverless Application Repository](https://ap-northeast-1.console.aws.amazon.com/serverlessrepo/)の「マイアプリケーション」にアプリケーションが表示されている状態にします。
![serverlessrepo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/586707/d5e65977-bbb8-3e2a-947b-69c166b3f568.jpeg)


[AWS Serverless Application Repositoryの資料]
[AWS Serverless Application Repository](https://aws.amazon.com/jp/serverless/serverlessrepo/)
[よくある質問と規約](https://aws.amazon.com/jp/serverless/serverlessrepo/faqs/)
[リポジトリへのアプリケーションの公開](https://aws.amazon.com/jp/serverless/serverlessrepo/publishing/)


# 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 Management Page

Parameters:
  ApplicationName:
    Type: String
    Default: 'ServerlessApplicationManagementPage'
  FrontPageApiStageName:
    Type: String
    Default: 'ProdStage'

Resources:
  FrontPageApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: ServerlessApplicationManagementPageApi
      EndpointConfiguration: REGIONAL
      StageName: !Ref FrontPageApiStageName
  FrontPageFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ServerlessApplicationManagementPageFrontFunction
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'ApplicationManagement Function'
      Policies:
      Environment:
        Variables:
          REGION: !Ref 'AWS::Region'
          API_PATH: !Join [ '', [ '/', !Ref FrontPageApiStageName, '/api'] ]
      Events:
        FrontPageApi:
          Type: Api
          Properties:
            Path: '/'
            Method: get
            RestApiId: !Ref FrontPageApi
  MainFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ServerlessApplicationManagementPageApiFunction
      CodeUri: api/bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'ApplicationManagement Function'
      Role: !GetAtt MainFunctionRole.Arn
      Environment:
        Variables:
          REGION: !Ref 'AWS::Region'
      Events:
        FrontPageApi:
          Type: Api
          Properties:
            Path: '/api'
            Method: post
            RestApiId: !Ref FrontPageApi
  MainFunctionRole:
    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: ManagementApplicationPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'serverlessrepo:ListApplications'
                  - 'serverlessrepo:CreateCloudFormationTemplate'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'cloudformation:DescribeStackResources'
                  - 'cloudformation:DeleteStack'
                  - 'cloudformation:CreateStack'
                  - 'cloudformation:ListStacks'
                  - 'cloudformation:ListStackResources'
                  - 'cloudformation:CreateChangeSet'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'lambda:*'
                  - 'events:RemoveTargets'
                  - 'events:PutTargets'
                  - 'events:DescribeRule'
                  - 'events:DeleteRule'
                  - 'events:PutRule'
                  - 'iam:DeleteRolePolicy'
                  - 'iam:DeleteRole'
                  - 'iam:CreateRole'
                  - 'iam:AttachRolePolicy'
                  - 'iam:PutRolePolicy'
                  - 'iam:GetRole'
                  - 'iam:PassRole'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 's3:PutObject'
                  - 's3:GetObject'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'apigateway:*'
                Resource: '*'

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

### API-Gatewayの設定
```yaml
  FrontPageApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: ServerlessApplicationManagementPageApi
      EndpointConfiguration: REGIONAL
      StageName: !Ref FrontPageApiStageName
```


### フロントエンド用Lambdaの設定
```yaml
  FrontPageFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ServerlessApplicationManagementPageFrontFunction
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'ApplicationManagement Function'
      Policies:
      Environment:
        Variables:
          REGION: !Ref 'AWS::Region'
          API_PATH: !Join [ '', [ '/', !Ref FrontPageApiStageName, '/api'] ]
      Events:
        FrontPageApi:
          Type: Api
          Properties:
            Path: '/'
            Method: get
            RestApiId: !Ref FrontPageApi
```

### バックエンド用Lambdaの設定
```yaml
  MainFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ServerlessApplicationManagementPageApiFunction
      CodeUri: api/bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'ApplicationManagement Function'
      Role: !GetAtt MainFunctionRole.Arn
      Environment:
        Variables:
          REGION: !Ref 'AWS::Region'
      Events:
        FrontPageApi:
          Type: Api
          Properties:
            Path: '/api'
            Method: post
            RestApiId: !Ref FrontPageApi
```


# Lambda関数作成

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

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

```golang:main.go
package main

import (
	"os"
	"fmt"
	"log"
	"time"
	"context"
	"strings"
	"net/http"
	"encoding/json"
	"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"
	"github.com/aws/aws-sdk-go-v2/service/cloudformation"
	"github.com/aws/aws-sdk-go-v2/service/serverlessapplicationrepository"
)

type Application struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	Stack       Stack  `json:"stack"`
}

type Stack struct {
	Name   string `json:"name"`
	Status string `json:"status"`
	Url    string `json:"url"`
}

type APIResponse struct {
	Message         string        `json:"message"`
	ApplicationList []Application `json:"applicationList"`
}

type Response events.APIGatewayProxyResponse

var cfg aws.Config
var cloudformationClient *cloudformation.Client
var serverlessApplicationRepositoryClient *serverlessapplicationrepository.Client

const layout string = "20060102150405.000"

func HandleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (Response, error) {
	var jsonBytes []byte
	var err error
	d := make(map[string]string)
	json.Unmarshal([]byte(request.Body), &d)
	if v, ok := d["action"]; ok {
		switch v {
		case "status" :
			l, e := getApplications(ctx)
			if e != nil {
				err = e
			} else {
				jsonBytes, _ = json.Marshal(APIResponse{Message: "Success.", ApplicationList: l})
			}
		case "create" :
			if n, ok := d["name"]; ok {
				e := createStack(ctx, n)
				if e != nil {
					err = e
				} else {
					jsonBytes, _ = json.Marshal(APIResponse{Message: "Success.", ApplicationList: nil})
				}
			}
		case "delete" :
			if n, ok := d["name"]; ok {
				e := deleteStack(ctx, n)
				if e != nil {
					err = e
				} else {
					jsonBytes, _ = json.Marshal(APIResponse{Message: "Success.", ApplicationList: nil})
				}
			}
		}
	}
	log.Print(request.RequestContext.Identity.SourceIP)
	if err != nil {
		log.Print(err)
		jsonBytes, _ = json.Marshal(APIResponse{Message: fmt.Sprint(err)})
		return Response{
			StatusCode: http.StatusInternalServerError,
			Body: string(jsonBytes),
		}, nil
	}
	return Response {
		StatusCode: http.StatusOK,
		Body: string(jsonBytes),
	}, nil
}

func getApplications(ctx context.Context)([]Application, error) {
	if serverlessApplicationRepositoryClient == nil {
		serverlessApplicationRepositoryClient = serverlessapplicationrepository.New(cfg)
	}
	req := serverlessApplicationRepositoryClient.ListApplicationsRequest(&serverlessapplicationrepository.ListApplicationsInput{})
	res, err := req.Send(ctx)
	if err != nil {
		return nil, err
	}
	var applicationList []Application
	for _, i := range res.ListApplicationsOutput.Applications {
		applicationList = append(applicationList, Application{
			Name:        aws.StringValue(i.Name),
			Description: aws.StringValue(i.Description),
			Stack:       Stack{},
		})
	}
	applicationList, err = addStackData(ctx, applicationList)
	if err != nil {
		return nil, err
	}
	return applicationList, nil
}

func getApplicationId(ctx context.Context, name string)(string, error) {
	if serverlessApplicationRepositoryClient == nil {
		serverlessApplicationRepositoryClient = serverlessapplicationrepository.New(cfg)
	}
	req := serverlessApplicationRepositoryClient.ListApplicationsRequest(&serverlessapplicationrepository.ListApplicationsInput{})
	res, err := req.Send(ctx)
	if err != nil {
		return "", err
	}
	var applicationId string
	for _, i := range res.ListApplicationsOutput.Applications {
		if name == aws.StringValue(i.Name) {
			applicationId = aws.StringValue(i.ApplicationId)
			break
		}
	}
	return applicationId, nil
}

func getTemplateUrl(ctx context.Context, applicationId string)(string, error) {
	if serverlessApplicationRepositoryClient == nil {
		serverlessApplicationRepositoryClient = serverlessapplicationrepository.New(cfg)
	}
	req := serverlessApplicationRepositoryClient.CreateCloudFormationTemplateRequest(&serverlessapplicationrepository.CreateCloudFormationTemplateInput{
		ApplicationId: aws.String(applicationId),
	})
	res, err := req.Send(ctx)
	if err != nil {
		return "", err
	}
	return aws.StringValue(res.CreateCloudFormationTemplateOutput.TemplateUrl), nil
}

func addStackData(ctx context.Context, applicationList []Application)([]Application, error) {
	if cloudformationClient == nil {
		cloudformationClient = cloudformation.New(cfg)
	}
	req := cloudformationClient.ListStacksRequest(&cloudformation.ListStacksInput{
		StackStatusFilter: []cloudformation.StackStatus{
			cloudformation.StackStatusCreateComplete,
			cloudformation.StackStatusCreateInProgress,
			cloudformation.StackStatusDeleteInProgress,
		},
	})
	res, err := req.Send(ctx)
	if err != nil {
		return nil, err
	}
	for _, i := range res.ListStacksOutput.StackSummaries {
		stackName := aws.StringValue(i.StackName)
		for n, j := range applicationList {
			if strings.HasPrefix(stackName, j.Name) {
				var url string
				if i.StackStatus == cloudformation.StackStatusCreateComplete {
					req_ := cloudformationClient.ListStackResourcesRequest(&cloudformation.ListStackResourcesInput{StackName: i.StackName})
					res_, err := req_.Send(ctx)
					if err != nil {
						log.Println(err)
						break
					}
					for _, j := range res_.ListStackResourcesOutput.StackResourceSummaries {
						if aws.StringValue(j.ResourceType) == "AWS::ApiGatewayV2::Api" {
							url = "https://" + aws.StringValue(j.PhysicalResourceId) + ".execute-api." + os.Getenv("REGION") + ".amazonaws.com/"
						}
					}
				}
				applicationList[n].Stack = Stack{Name: stackName, Status: string(i.StackStatus), Url: url}
				break
			}
		}
	}
	return applicationList, nil
}

func createStack(ctx context.Context, name string) error {
	applicationId, err := getApplicationId(ctx, name)
	if err != nil {
		log.Println(err)
		return err
	}
	templateUrl, err := getTemplateUrl(ctx, applicationId)
	if err != nil {
		log.Println(err)
		return err
	}
	t := time.Now()
	stackName := name + strings.Replace(t.Format(layout), ".", "", 1)
	if cloudformationClient == nil {
		cloudformationClient = cloudformation.New(cfg)
	}
	req := cloudformationClient.CreateStackRequest(&cloudformation.CreateStackInput{
		Capabilities: []cloudformation.Capability{
			cloudformation.CapabilityCapabilityIam,
			cloudformation.CapabilityCapabilityAutoExpand,
		},
		StackName: aws.String(stackName),
		TemplateURL: aws.String(templateUrl),
	})
	_, err = req.Send(ctx)
	if err != nil {
		log.Println(err)
		return err
	}
	return nil
}

func deleteStack(ctx context.Context, name string) error {
	if cloudformationClient == nil {
		cloudformationClient = cloudformation.New(cfg)
	}
	req := cloudformationClient.DeleteStackRequest(&cloudformation.DeleteStackInput{
		StackName: aws.String(name),
	})
	_, err := req.Send(ctx)
	if err != nil {
		log.Println(err)
	}
	return nil
}

func getTargetStack(name string, list []Stack) Stack {
	var stack Stack
	for _, i := range list {
		if i.Name == name {
			stack = i
			break
		}
	}
	return stack
}

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>

### アプリケーション一覧を取得するには ListApplicationsRequest を使う
```golang
req := serverlessApplicationRepositoryClient.ListApplicationsRequest(&serverlessapplicationrepository.ListApplicationsInput{})
res, err := req.Send(ctx)
```
※ Serverless Application Repositoryのマイアプリケーションに表示されているアプリケーション一覧が取得できます。


### CloudFormationテンプレートを作成するには CreateCloudFormationTemplateRequest を使う
```golang
req := serverlessApplicationRepositoryClient.CreateCloudFormationTemplateRequest(&serverlessapplicationrepository.CreateCloudFormationTemplateInput{
	ApplicationId: aws.String(applicationId),
})
res, err := req.Send(ctx)
```

### スタックを作成するには CreateStackRequest を使う
```golang
req := cloudformationClient.CreateStackRequest(&cloudformation.CreateStackInput{
	Capabilities: []cloudformation.Capability{
		cloudformation.CapabilityCapabilityIam,
		cloudformation.CapabilityCapabilityAutoExpand,
	},
	StackName: aws.String(stackName),
	TemplateURL: aws.String(templateUrl),
})
_, err = req.Send(ctx)
```


### スタック一覧を取得するには ListStacksRequest を使う
```golang
req := cloudformationClient.ListStacksRequest(&cloudformation.ListStacksInput{
	StackStatusFilter: []cloudformation.StackStatus{
		cloudformation.StackStatusCreateComplete,
		cloudformation.StackStatusCreateInProgress,
		cloudformation.StackStatusDeleteInProgress,
	},
})
res, err := req.Send(ctx)
```
上記の例では、フィルターを設定し [作成完了、作成中、削除中]のスタックのみを取得しています。


### スタックを削除するには DeleteStackRequest を使う
```golang
req := cloudformationClient.DeleteStackRequest(&cloudformation.DeleteStackInput{
	StackName: aws.String(name),
})
_, err := req.Send(ctx)
```

# 終わりに
Serverless Application Repositoryにマイアプリケーションを発行することで、Web環境のみの状況でもサーバレスアプリケーションを作成することができます。
AWS-SAMとあわせて使い慣れていこうと思います。