🐷

LambdaとGolangで始めるサーバレス開発

2023/06/20に公開

開発内容

タイトルにあるようにLambdaとGolangでサーバレス開発入門するための記事となります。
Lambdaから外部APIへリクエストしてデータ取得する方法を解説しています。
今回使用する外部APIはbitFlyer Lightningで仮想通貨の販売価格を取得します。会員登録してAPIキーを発行して自身の口座に入金していればAPI経由で取引売買できるそうですが、今回は無料で開発したかったのでこちらは解説で触れていません。

環境構築

試した環境はこちらになります。

* Golang
go version go1.20.2 darwin/amd64

* AWS CLI
aws-cli/2.11.20 Python/3.11.3 Darwin/22.4.0 exe/x86_64 prompt/off

* Doker
Docker version 20.10.21, build baeda1f

* AWS SAM CLI
SAM CLI, version 1.83.0

開発

AWS SAMで雛形を作成

簡単にサーバレスアプリを構築できるAWS SAMを利用します。

$ sam init --runtime go1.x --name api-virtual-currency
※api-virtual-currencyの部分はお好きな名前を指定してください
Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
	1 - Hello World Example
	2 - Infrastructure event management
	3 - Multi-step workflow
Template: 1

Based on your selections, the only Package type available is Zip.
We will proceed to selecting the Package type as Zip.

Based on your selections, the only dependency manager available is mod.
We will proceed copying the template using mod.

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: N

Would you like to enable monitoring using CloudWatch Application Insights?
For more info, please view https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-application-insights.html [y/N]: N

これででapi-virtual-currency配下に雛形が作成されています。
フォルダ構成は下記になります。

|--Makefile
|--README.md
|--events
|  |--event.json
|--hello-world
|  |--go.mod
|  |--go.sum
|  |--main.go
|  |--main_test.go
|--samconfig.toml
|--template.yaml

雛形を作成するときにオプションでHello Worldのテンプレートを指定していましたので、この段階でビルドができますので動かしてみます。

$ cd api-virtual-currency
$ make build

ビルド成功すると次の通りのメッセージがでます。

runtime: go1.x metadata: {} architecture: x86_64 functions: HelloWorldFunction  
Running GoModulesBuilder:Build                                                  

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch
[*] Deploy: sam deploy --guided

.aws-sam配下にビルドしたものが作成されています。
Commands you can use nextに次の推奨コマンドがありますが、作成した関数を指定して動かしてみます。

$ sam local invoke HelloWorldFunction

....
{"statusCode":200,"headers":null,"multiValueHeaders":null,"body":"Hello, 59.171.109.138\n"}

動作がうまくいき、スタータスコード200のレスポンスが返却されていることを確認できます。

雛形の修正

ここからは雛形で作成されたフォルダ名やファイルの一部を少し修正します。
具体的には雛形で指定したHello Worldという名称を変更します。

  1. フォルダ名hello-worldapi-virtual-currencyに変更します。
    (変更名はお好きなもので構いません)

  2. api-virtual-currency/go.modでmoduleの箇所をapi-virtual-currencyに変更

go.mod
require github.com/aws/aws-lambda-go v1.36.1

replace gopkg.in/yaml.v2 => gopkg.in/yaml.v2 v2.2.8

module api-virtual-currency

go 1.16
  1. template.yamlを修正
template.yaml
+    ApiVirtualCurrencyFunction
-    HelloWorldFunction

+    CodeUri: api-virtual-currency/
+    Handler: api-virtual-currency
-    CodeUri: hello-world/
-    Handler: hello-world

+    Path: /tickers
-    Path: /hello

+    VirtualCurrencyAPI
-    HelloWorldAPI

+    Value: !GetAtt ApiVirtualCurrencyFunction.Arn
-    Value: !GetAtt HelloWorldFunction.Arn

+    ApiVirtualCurrencyFunction
-    HelloWorldFunction

+    ApiVirtualCurrencyFunctionIamRole
-    HelloWorldFunctionIamRole

+    Value: !GetAtt ApiVirtualCurrencyFunctionRole.Arn
-    Value: !GetAtt HelloWorldFunctionRole.Arn

Lambdaを作成

api-virtual-currency/main.go
api-virtual-currency/main.go
package main

import (
	"api-virtual-currency/bitflyer"
	"fmt"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func handler() (events.APIGatewayProxyResponse, error) {
	markets, err := bitflyer.GetMarkets()
	if err != nil {
		return GetErrorResponse(err)
	}

	var tickers []bitflyer.Ticker
	for _, market := range markets {
		ticker, err := bitflyer.GetTicker(market.ProductCode)
		if err != nil {
			return GetErrorResponse(err)
		}
		tickers = append(tickers, *ticker)
	}

	return events.APIGatewayProxyResponse{
		Body:       fmt.Sprintf("%+v", tickers),
		StatusCode: 200,
	}, nil
}

func GetErrorResponse(err error) (events.APIGatewayProxyResponse, error) {
	return events.APIGatewayProxyResponse{
		Body:       err.Error(),
		StatusCode: 400,
	}, err
}

func main() {
	lambda.Start(handler)
}

bitflyerへのAPIリクエスト処理

api-virtual-currency/bitflyer/market_api.go
api-virtual-currency/bitflyer/market_api.go
package bitflyer

import (
	"api-virtual-currency/utils"
	"encoding/json"
)

type Market struct {
	ProductCode string `json:"product_code"`
	MarketType  string `json:"market_type"`
	Alias       string `json:"alias,omitempty"`
}

type Ticker struct {
	ProductCode     string  `json:"product_code"`
	State           string  `json:"state"`
	TimeStamp       string  `json:"timestamp"`
	TickID          int     `json:"tick_id"`
	BestBid         float64 `json:"best_bid"`
	BestAsk         float64 `json:"best_ask"`
	BestBidSize     float64 `json:"best_bid_size"`
	BestAskSize     float64 `json:"best_ask_size"`
	TotalBidDepth   float64 `json:"total_bid_depth"`
	TotalAskDepth   float64 `json:"total_ask_depth"`
	Ltp             float64 `json:"ltp"`
	Volume          float64 `json:"volume"`
	VolumeByProduct float64 `json:"volume_by_product"`
}

var (
	// マーケット一覧の取得URL
	MarketsURL = "https://api.bitflyer.com//v1/markets"

	// Tickerの取得URL
	TickerURL = "https://api.bitflyer.com/v1/ticker"
)

// マーケット一覧を取得します
func GetMarkets() ([]Market, error) {
	res, err := utils.HttpRequest("GET", MarketsURL, map[string]string{})
	if err != nil {
		return nil, err
	}

	var markets []Market
	err = json.Unmarshal(res, &markets)
	if err != nil {
		return nil, err
	}
	return markets, nil
}

// Ticker情報を取得します
func GetTicker(code string) (*Ticker, error) {
	res, err := utils.HttpRequest("GET", TickerURL, map[string]string{"product_code": code})
	if err != nil {
		return &Ticker{}, err
	}

	var ticker Ticker
	err = json.Unmarshal(res, &ticker)
	if err != nil {
		return &Ticker{}, err
	}
	return &ticker, nil
}

httpリクエスト処理

api-virtual-currency/utils/http.utils.go
api-virtual-currency/utils/http.utils.go
package utils

import (
	"io/ioutil"
	"net/http"
)

func HttpRequest(method, url string, query map[string]string) ([]byte, error) {
	req, err := http.NewRequest(method, url, nil)
	if err != nil {
		return nil, err
	}

	q := req.URL.Query()
	for key, val := range query {
		q.Add(key, val)
	}
	req.URL.RawQuery = q.Encode()

	client := new(http.Client)
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	return body, nil
}

AWS SAMによるLambdaデプロイ

$ sam deploy --guided

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Found
        Reading default arguments  :  Success

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [api-virtual-currency]: 
        AWS Region [ap-northeast-1]: 
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [Y/n]: Y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: Y
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [y/N]: y
        ApiVirtualCurrencyFunction may not have authorization defined, Is this okay? [y/N]: y
        Save arguments to configuration file [Y/n]: Y
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]: 

AWSコンソール画面で作成したLambda関数でテストすると正しく値が返却されていることを確認できます。

Discussion