LambdaとGolangで始めるサーバレス開発
開発内容
タイトルにあるように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という名称を変更します。
-
フォルダ名
hello-world
をapi-virtual-currency
に変更します。
(変更名はお好きなもので構いません) -
api-virtual-currency/go.modでmoduleの箇所を
api-virtual-currency
に変更
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
- 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
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
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
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