はじめに
今回は AWS Serverless Application Repository を試します。
サーバーレスアプリケーションを管理するページを作成します。
[Lambda関数・SAMテンプレート]
(https://github.com/tanaka-takurou/serverless-application-management-page-go)
準備
Serverless Application Repositoryの「マイアプリケーション」にアプリケーションが表示されている状態にします。
[AWS Serverless Application Repositoryの資料]
AWS Serverless Application Repository
よくある質問と規約
リポジトリへのアプリケーションの公開
AWS SAM テンプレート作成
AWS SAM テンプレートで API-Gateway , Lambdaの設定をします。
[参考資料]
AWS SAM テンプレートを作成する
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
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とあわせて使い慣れていこうと思います。