0
0

More than 1 year has passed since last update.

AWS日記16 (Serverless Application Repository)

Last updated at Posted at 2020-08-29

はじめに

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

準備

サーバーレスアプリケーションの準備をします

Serverless Application Repositoryの「マイアプリケーション」にアプリケーションが表示されている状態にします。
serverlessrepo.jpg

[AWS Serverless Application Repositoryの資料]
AWS Serverless Application Repository
よくある質問と規約
リポジトリへのアプリケーションの公開

AWS SAM テンプレート作成

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

[参考資料]
AWS SAM テンプレートを作成する

template.yml
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,'/'] ]

API-Gatewayの設定

  FrontPageApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: ServerlessApplicationManagementPageApi
      EndpointConfiguration: REGIONAL
      StageName: !Ref FrontPageApiStageName

フロントエンド用Lambdaの設定

  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の設定

  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 を利用しました。

main.go
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)
}

アプリケーション一覧を取得するには ListApplicationsRequest を使う

req := serverlessApplicationRepositoryClient.ListApplicationsRequest(&serverlessapplicationrepository.ListApplicationsInput{})
res, err := req.Send(ctx)

※ Serverless Application Repositoryのマイアプリケーションに表示されているアプリケーション一覧が取得できます。

CloudFormationテンプレートを作成するには CreateCloudFormationTemplateRequest を使う

req := serverlessApplicationRepositoryClient.CreateCloudFormationTemplateRequest(&serverlessapplicationrepository.CreateCloudFormationTemplateInput{
	ApplicationId: aws.String(applicationId),
})
res, err := req.Send(ctx)

スタックを作成するには CreateStackRequest を使う

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 を使う

req := cloudformationClient.ListStacksRequest(&cloudformation.ListStacksInput{
	StackStatusFilter: []cloudformation.StackStatus{
		cloudformation.StackStatusCreateComplete,
		cloudformation.StackStatusCreateInProgress,
		cloudformation.StackStatusDeleteInProgress,
	},
})
res, err := req.Send(ctx)

上記の例では、フィルターを設定し [作成完了、作成中、削除中]のスタックのみを取得しています。

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

req := cloudformationClient.DeleteStackRequest(&cloudformation.DeleteStackInput{
	StackName: aws.String(name),
})
_, err := req.Send(ctx)

終わりに

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0