AWS SAM + OpenAPI(Swagger) + Golang でサーバーレスなAPIを構築する
はじめに
AWS SAM + OpenAPI(Swagger) + Golangを使ってサーバーレスなAPIを実装する方法を紹介しようと思います。
SAMとは?
SAM(Serverless Application Model)はサーバーレスアプリケーションを構築するためのAWS公式のフレームワークです。yaml形式で簡単にリソースを定義でき、SAM CLIを使用することでローカルにもlambdaの実行環境を作ることができます。他にフレームワークの候補としてServerlessFrameworkがありますが、ローカルでの開発環境の構築はSAMの方がしやすいのかなと思います。
OpenAPIとは?
OpenAPIはREST APIのためのAPI記述フォーマットで、APIの仕様をyaml形式で定義することができます。またopenapi-generatorなどのツールを使用することで定義したOpenAPIからクライアントコードなどを自動生成することができます。本記事ではAPIの定義とAPIGatewayの生成に使用します。
環境
- SAM CLI 1.23.0
- Golang 1.16
SAMプロジェクトの準備
下記コマンドでプロジェクトを作成します。コマンド入力後、対話形式で必要な情報を入力していきます。
$ sam init
-----------------------
Generating application:
-----------------------
Name: sam-app
Runtime: go1.x
Dependency Manager: mod
Application Template: hello-world
Output Directory: .
ディレクトリ構成
生成されたプロジェクトを元に大枠を作成していきます。今回はGETとPOSTで呼び出す用のlambdaを2つ作成します。
変更後のディレクトリは下記のようになります。
├── get
│ └── main.go
├── post
│ └── main.go
├── go.mod
├── go.sum
├── openapi.yaml
└── template.yaml
各lambda定義
次にlambdaの定義をしていきます。今回はシンプルにHelloWorldと返す関数と、送られてきたパラメーターを少し加工して返す関数を定義します。
package main
import (
"encoding/json"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
type Response struct {
Message string `json:"message"`
}
func handler() (events.APIGatewayProxyResponse, error) {
body := Response{Message: "Hello World"}
jsonBody, err := json.Marshal(body)
if err != nil {
panic(err)
}
return events.APIGatewayProxyResponse{
Body: string(jsonBody),
StatusCode: 200,
}, nil
}
func main() {
lambda.Start(handler)
}
package main
import (
"encoding/json"
"fmt"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
type Response struct {
Message string `json:"message"`
}
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
var p Person
b := []byte(request.Body)
err := json.Unmarshal(b, &p)
if err != nil {
panic(err)
}
body := Response{Message: fmt.Sprintf("Hello %v : %v", p.Name, p.Age)}
jsonBody, err := json.Marshal(body)
if err != nil {
panic(err)
}
return events.APIGatewayProxyResponse{
Body: string(jsonBody),
StatusCode: 200,
}, nil
}
func main() {
lambda.Start(handler)
}
APIの設計
lambdaの定義が終わったので本題のAPIの定義をしていきます。
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
sam-app
Sample SAM Template for sam-app
Globals:
Function:
Timeout: 5
Runtime: go1.x
Resources:
ApiRole:
Type: AWS::IAM::Role
Properties:
RoleName: api-execution-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- apigateway.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: api-execution-role-policy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource:
- Fn::Sub: ${GetFunction.Arn}
- Fn::Sub: ${PostFunction.Arn}
Api:
Type: AWS::Serverless::Api
Properties:
Name: "rest-api"
StageName: dev
DefinitionBody:
Fn::Transform:
Name: AWS::Include
Parameters:
Location: ./openapi.yaml # 参照するyamlファイルを指定
GetFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: get/
Handler: get
PostFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: post/
Handler: post
APIGatewayに付与するロールとAPIGateway、各lambdaを定義しています。DefinitionBodyでopenapi.yaml
のディレクトリを指定することでopenapi.yaml
の定義を元にAPIGatewayを生成することができます。またFn::Transformを使用することで、参照したopenapi.yaml
内でCloudFormation固有の変数が使用可能になります。
openapi.yaml
openapi: 3.0.0
info:
title: OpenAPI sam-app
description: sam-appのAPI
version: 1.0.0
paths:
/hello:
get:
summary: GETサンプル
description: GETのサンプルです
responses:
'200':
description: 成功レスポンス
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
x-amazon-apigateway-integration:
credentials:
Fn::Sub: ${ApiRole.Arn}
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetFunction.Arn}/invocations
passthroughBehavior: when_no_templates
httpMethod: POST
type: aws_proxy
post:
summary: POSTサンプル
description: POSTのサンプルです
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
description: 名前
type: string
age:
description: 年齢
type: integer
format: int32
required:
- name
- age
responses:
'200':
description: 成功レスポンス
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
x-amazon-apigateway-integration:
credentials:
Fn::Sub: ${ApiRole.Arn}
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${PostFunction.Arn}/invocations
passthroughBehavior: when_no_templates
httpMethod: POST
type: aws_proxy
components:
schemas:
Success:
description: 成功の場合のレスポンス
type: object
properties:
message:
type: string
required:
- message
x-amazon-apigateway-integrationはOpenAPI の仕様に対するAPI Gatewayの拡張です。credentials
プロパティでtemplate.yaml
で定義したロール、uri
プロパティでそのエンドポイントに対応するlambdaを指定してます。
動作確認
上記の実装が完了したらビルドを実行します。
$ sam build
ビルドが完了したらAPIをローカルでホストします。下記コマンドでtemplate.yaml
とopenapi.yaml
を元にDockerコンテナを立てて、すべての関数をホストするローカル HTTP サーバーが作成されます!
$ sam local start-api
Mounting GetFunction at http://127.0.0.1:3000/hello [GET]
Mounting PostFunction at http://127.0.0.1:3000/hello [POST]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2021-06-08 02:16:59 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
上記のようにエンドポイントが表示されたらcurlを叩いて繋ぎこみが完了しているか確認します。
$ curl --request GET --url http://127.0.0.1:3000/hello
{"message": "Hello World"}
$ curl --request POST --url http://127.0.0.1:3000/hello --data '{"name": "Taro", "age": 20}'
{"message":"Hello Taro : 20"}
レスポンスが返って来てたらOKです!
デプロイ
ローカルでの動作確認ができたら最後にデプロイします。デプロイといっても下記コマンドだけで完了します。
コマンド入力後いくつかパラメーターの入力を求められますが全てデフォルト値でOKです!
$ sam deploy --capabilities CAPABILITY_NAMED_IAM --guided
作成したAPIのエンドポイントを叩いてレスポンスが返ってきたら全て完了です!
まとめ
今回はSAMとOpenAPIを組み合わせてサーバーレスなAPIを作成しました。
SAMとOpenAPIを活用することで簡単にAPIを作成でき、スキーマファーストな開発ができると思います!
Discussion