バイセル Tech Blog

バイセル Tech Blogは株式会社BuySellTechnologiesのエンジニア達が知見・発見を共有する技術ブログです。

AWS SAM + OpenAPI(Swagger) + Golang でAPIを構築する

自己紹介

初めまして、今年度21卒として入社したテクノロジー戦略本部の早瀬です。 バイセルではインターン生として2020年の11月から活動しており、今年の4月に入社しました。 入社してからアサインされたプロジェクトでサーバーレスアーキテクチャの実装にチャレンジさせていただく機会を頂いたので、今回はその一部を紹介したいと思います。

背景

今回は以前の記事に関連する新システムを開発することになり、前回同様サーバーレスで構築することになりました。 tech.buysell-technologies.com ただ今回はRubyでなくGolangでバックエンドを実装することになったので、フレームワークはRuby on JetsではなくAWS SAMを採用しました。

SAMとは?

SAM(Serverless Application Model)はサーバーレスアプリケーションを構築するためのAWS公式のフレームワークです。yaml形式で簡単にリソースを定義でき、SAM CLIを使用することでローカルにもlambdaの実行環境を作ることができます。他にフレームワークの候補としてServerlessFrameworkがありましたが、ローカルでの開発環境の構築のしやすさからSAMを使用することにしました。

OpenAPIとは?

OpenAPIはREST APIのためのAPI記述フォーマットで、APIの仕様をyaml形式で定義することができます。またopenapi-generatorなどのツールを使用することで定義したOpenAPIからクライアントコードなどを自動生成することができます。今回のプロジェクトでもOpenAPIからiosとGolangのコードを自動生成して使用したので、その辺はまた別の記事で紹介したいと思います。 本記事では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を作成でき、スキーマファーストな開発ができると思います! 次回はaws-sdk-go-v2を使ってS3に画像をアップロードするところまでを記事にしようかなと思ってます。

ほとんどが初めて触る技術ばかりだったので大変でしたが、その分かなり成長できたのではないかなと感じています! そして、バイセルでは新卒でもこのような機会を与えてもらえるチャンスがあります! ご興味のある方は是非ご連絡ください。バイセルテクノロジーズではエンジニアを募集しています。

hrmos.co