背景
現在Golang + Nginxで動いているAPIをLambda関数へ移行したい。
API仕様
- おみくじAPI
-
/fortune
へのアクセスで大吉・中吉・小吉のどれかをjsonとして返してくれる -
/list
でおみくじで得ることが出来る結果一覧をJSONで取得できる -
/version
でプレーンテキストとしてAPIのバージョンを取得できる - 無効なパスへのアクセスは404を返す
環境
OS: Ubuntu 18.04
バージョン
$ go version
go version go1.10.3 gccgo (Ubuntu 8.3.0-6ubuntu1~18.04.1) 8.3.0 linux/amd64
実際に移植してみる!
ざっくりやること
- Lambda関数作成
- ALBとターゲットグループを作成
- ビルドしてLambdaへデプロイ
移植前のソース
下記のコードをLambdaで動くように修正します。
移植前ソース
package main
import (
"encoding/json"
"fmt"
"log"
"math/rand"
"net/http"
)
var fortune = []string{"大吉", "中吉", "小吉"}
func fortuneHandler(w http.ResponseWriter, r *http.Request) {
res, err := json.Marshal(fortune[rand.Intn(3)])
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, string(res))
}
func versionHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plane; charset=utf-8")
fmt.Fprint(w, string("version 1.0.0"))
}
func listHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, fortune)
}
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(404)
fmt.Fprint(w, "404 not found")
}
func main() {
http.HandleFunc("/fortune", fortuneHandler)
http.HandleFunc("/version", versionHandler)
http.HandleFunc("/list", listHandler)
http.HandleFunc("/", notFoundHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Lambda環境作成
Lambda関数の作成
ランタイムが Go 1.x
になっていることを確認し、関数を作成します。
ALBとターゲットグループを作成
Application Load Balancer
の 作成
を押下
ロードバランサーの設定は各々の環境に合わせて設定してください
ターゲットグループを新規作成し、 ターゲットの種類
が Lambda
になっていることを確認し、ターゲットの登録ボタンを押下
リストからターゲットにしたいLambdaを選択し、確認を押下
確認して問題なければ作成ボタンを押下
動作確認
作成したALBのDNS名をコピーして、実際にアクセスしてみて確認
Hello from Lambda!
と表示されればOK
$ curl <ALBのDNS名> -D -
HTTP/1.1 200 OK
Server: awselb/2.0
Date: Mon, 24 Feb 2020 12:47:12 GMT
Content-Type: application/octet-stream
Content-Length: 18
Connection: keep-alive
Hello from Lambda!
Lambda関数として動くようにする
- やること
- サーバとしてではなく関数として動作するようにする
-
*http.Request
とhttp.ResponseWriter
をALBTargetGroupRequest
とevents.ALBTargetGroupResponse
へ置き換える
置き換え後
置き換え後ソース
package main
import (
"encoding/json"
"fmt"
"math/rand"
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
var fortune = []string{"大吉", "中吉", "小吉"}
func fortuneHandler() (events.ALBTargetGroupResponse, error) {
res, err := json.Marshal(fortune[rand.Intn(3)])
if err != nil {
return events.ALBTargetGroupResponse {
StatusCode: 500,
Body: fmt.Sprintf("%s", err),
}, err
}
return events.ALBTargetGroupResponse {
Headers: map[string]string{
"content-type": "application/json",
},
Body: fmt.Sprintf("%s", string(res)),
}, nil
}
func versionHandler() (events.ALBTargetGroupResponse, error) {
return events.ALBTargetGroupResponse {
Headers: map[string]string{
"content-type": "text/plane; charset=utf-8",
},
Body: fmt.Sprintf("%s", string("version 1.0.0")),
}, nil
}
func listHandler() (events.ALBTargetGroupResponse, error) {
return events.ALBTargetGroupResponse {
Headers: map[string]string{
"content-type": "application/json",
},
Body: fmt.Sprintf("%s", fortune),
}, nil
}
func notFoundHandler() (events.ALBTargetGroupResponse, error) {
return events.ALBTargetGroupResponse {
StatusCode: 404,
Headers: map[string]string{
"content-type": "text/plain; charset=utf-8",
},
Body: fmt.Sprintf("404 not found\n"),
}, nil
}
func main() {
lambda.Start(handleRequest)
}
func handleRequest(ctx context.Context, request events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) {
switch request.Path {
case "/fortune": return fortuneHandler()
case "/version": return versionHandler()
case "/list": return listHandler()
default: return notFoundHandler()
}
}
修正差分
6d5
< "log"
8c7,9
< "net/http"
---
> "context"
> "github.com/aws/aws-lambda-go/events"
> "github.com/aws/aws-lambda-go/lambda"
13c14
< func fortuneHandler(w http.ResponseWriter, r *http.Request) {
---
> func fortuneHandler() (events.ALBTargetGroupResponse, error) {
17,18c18,21
< http.Error(w, err.Error(), http.StatusInternalServerError)
< return
---
> return events.ALBTargetGroupResponse {
> StatusCode: 500,
> Body: fmt.Sprintf("%s", err),
> }, err
21,22c24,29
< w.Header().Set("Content-Type", "application/json")
< fmt.Fprint(w, string(res))
---
> return events.ALBTargetGroupResponse {
> Headers: map[string]string{
> "content-type": "application/json",
> },
> Body: fmt.Sprintf("%s", string(res)),
> }, nil
25,27c32,38
< func versionHandler(w http.ResponseWriter, r *http.Request) {
< w.Header().Set("Content-Type", "text/plane; charset=utf-8")
< fmt.Fprint(w, string("version 1.0.0"))
---
> func versionHandler() (events.ALBTargetGroupResponse, error) {
> return events.ALBTargetGroupResponse {
> Headers: map[string]string{
> "content-type": "text/plane; charset=utf-8",
> },
> Body: fmt.Sprintf("%s", string("version 1.0.0")),
> }, nil
30,32c41,47
< func listHandler(w http.ResponseWriter, r *http.Request) {
< w.Header().Set("Content-Type", "application/json")
< fmt.Fprint(w, fortune)
---
> func listHandler() (events.ALBTargetGroupResponse, error) {
> return events.ALBTargetGroupResponse {
> Headers: map[string]string{
> "content-type": "application/json",
> },
> Body: fmt.Sprintf("%s", fortune),
> }, nil
35,38c50,57
< func notFoundHandler(w http.ResponseWriter, r *http.Request) {
< w.Header().Set("Content-Type", "text/plain; charset=utf-8")
< w.WriteHeader(404)
< fmt.Fprint(w, "404 not found")
---
> func notFoundHandler() (events.ALBTargetGroupResponse, error) {
> return events.ALBTargetGroupResponse {
> StatusCode: 404,
> Headers: map[string]string{
> "content-type": "text/plain; charset=utf-8",
> },
> Body: fmt.Sprintf("404 not found\n"),
> }, nil
42,46c61,64
< http.HandleFunc("/fortune", fortuneHandler)
< http.HandleFunc("/version", versionHandler)
< http.HandleFunc("/list", listHandler)
< http.HandleFunc("/", notFoundHandler)
< log.Fatal(http.ListenAndServe(":8080", nil))
---
> lambda.Start(handleRequest)
> }
>
> func handleRequest(ctx context.Context, request events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) {
47a66,71
> switch request.Path {
> case "/fortune": return fortuneHandler()
> case "/version": return versionHandler()
> case "/list": return listHandler()
> default: return notFoundHandler()
> }
差分解説
func main() {
lambda.Start(handleRequest)
}
func handleRequest(ctx context.Context, request events.ALBTargetGroupRequest) (events.ALBTargetGroupResponse, error) {
switch request.Path {
case "/fortune": return fortuneHandler()
case "/version": return versionHandler()
case "/list": return listHandler()
default: return notFoundHandler()
}
}
- lambda.Start
lambda.Start(HandleRequest) を追加すると、Lambda 関数が実行されます。
-
ALBTargetGroupRequest
- ここにリクエストのパスなどが入ってきます
- golangの
*http.Request
相当
func listHandler() (events.ALBTargetGroupResponse, error) {
return events.ALBTargetGroupResponse {
Headers: map[string]string{
"content-type": "application/json",
},
Body: fmt.Sprintf("%s", fortune),
}, nil
}
- ALBTargetGroupResponse
- この構造体をLambdaからreturnする事で任意のレスポンスを返すことが出来ます
- 基本的に、
http.ResponseWriter
をALBTargetGroupResponse
へ置き換えるだけで任意のレスポンスを返すように出来ます
ビルド&動作確認
ビルド
ここでのポイントはクロスコンパイルするところです
$ GOOS=linux GOARCH=amd64 go build -o fortune
ここで作ったバイナリをLambdaへアップロードして、動作確認をします!
動作確認
ここで大吉・中吉・小吉のどれかが出力されればOKです
$ curl <ALBのDNS名>/fortune -D -
HTTP/1.1 000
Server: awselb/2.0
Date: Mon, 24 Feb 2020 16:18:30 GMT
Content-Type: application/json
Content-Length: 8
Connection: keep-alive
"小吉"
記事外で関数に移植する際に困ったところ
条件によって特定のヘッダーを付与してALBTargetGroupResponseを返したい
ALBTargetGroupResponseを一度変数に入れて変数値を操作したものをreturnすればいい
下記コードだと if err {
の部分
response := events.ALBTargetGroupResponse {
StatusCode: http.StatusInternalServerError,
Body: fmt.Sprintf("%d", http.StatusInternalServerError),
Headers: map[string]string{},
}
if err {
response.Headers["X-Error-Message"] = fmt.Sprintf("error: %v", err)
}
response.StatusCode = statusCode
response.Headers["Content-Type"] = "text/plane; charset=utf-8"
response.Body = http.StatusInternalServerError
return response
おわりに
置き換えはLambdaのお作法に従うだけで意外と簡単に移植できるみたいです。
参考資料
AWS Lambda で Go が使えるようになったので試してみた
Go の AWS Lambda 関数ハンドラー
README_ALBTargetGroupEvents.md
type ALBTargetGroupResponse