7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GolangのAPIサーバをAWS Lambdaへ移植してみた

Posted at

背景

現在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 になっていることを確認し、関数を作成します。
image.png

ALBとターゲットグループを作成

EC2の画面を開き、左のメニューからロードバランサーを選択
スクリーンショット 2020-02-24 21.35.05.png

画面上位の ロードバランサーの作成 を押下
image.png

Application Load Balancer作成 を押下
スクリーンショット 2020-02-24 21.39.16.png

ロードバランサーの設定は各々の環境に合わせて設定してください

ターゲットグループを新規作成し、 ターゲットの種類Lambda になっていることを確認し、ターゲットの登録ボタンを押下
image.png

リストからターゲットにしたいLambdaを選択し、確認を押下
image.png

確認して問題なければ作成ボタンを押下

動作確認

作成したALBのDNS名をコピーして、実際にアクセスしてみて確認
スクリーンショット 2020-02-24 21.50.00.png

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.Requesthttp.ResponseWriterALBTargetGroupRequestevents.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 関数が実行されます。


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.ResponseWriterALBTargetGroupResponse へ置き換えるだけで任意のレスポンスを返すように出来ます

ビルド&動作確認

ビルド

ここでのポイントはクロスコンパイルするところです

$ 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

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?