🙄

AWS SAM + OpenAPI(Swagger) + Golang でサーバーレスなAPIを構築する

2021/07/13に公開

はじめに

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と返す関数と、送られてきたパラメーターを少し加工して返す関数を定義します。

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


post/main.go
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

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.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.yamlopenapi.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を作成でき、スキーマファーストな開発ができると思います!

株式会社BuySell Technologies

Discussion