見出し画像

Goで書いた関数をLambdaとAPI GatewayでサーバーレスAPIしてみる

こんにちは、株式会社SHIFT、自動化エンジニアの水谷です。
少し前の記事では、Goで書いたマンデルブロー集合を計算するアプリをEchoを使ってAPI化する話を書いたのですが、今回はこれをAWSのLambdaとAPI GatewayでサーバーレスAPIにしてみたいと思います。

と、今回のネタを決めたはいいものの、これまでLambdaがサーバーレスで自分で作ったコードを実行させてくれるAWSのサービス、ということだけは知っているけど、実は何気に一度も使ったことがないので、手探りで試行錯誤しながら進めることになります。はたして無事動くでしょうか……?


Goコードの作成

まずは、AWSの公式ドキュメントなどを参考にGoのコードを作ります。作ると言っても、前回のコードをほぼ流用して、パラメーターの受け取りや、画像データを返すところをLambdaの仕様に合わせる感じになります。

まずは、適当なディレクトリを作成して、"go mod init"を実行します。

>go mod init mandel-lambda
go: creating new go.mod: module mandel-lambda

そして、github.com/aws/aws-lambda-go/lambdaをパッケージを"go get"でインストールします。

>go get github.com/aws/aws-lambda-go/lambda
go: downloading github.com/aws/aws-lambda-go v1.26.0
go get: added github.com/aws/aws-lambda-go v1.26.0

さて、コードの方ですが、まず下のようにLambda関連パッケージをimportしておきます。

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

main()関数については、下のようにシンプルにハンドラー関数を登録するだけで良いようです。

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

そして、このハンドラー関数ですが、入力(APIに与えるパラメーター)がある場合とない場合、出力(APIが返す値)がある場合とない場合、それからコンテキスト情報にアクセスするかどうかによって、10種類程度の定義方法があります。今回は、入力(マンデルブロー集合の計算範囲)と出力(画像データ)が共にあり、コンテキスト情報にはアクセスしないため、"func (TIn) (TOut, error)"の形の関数にします。

ここで、TInとTOutは共にencoding/json 標準ライブラリと互換性のあるタイプである必要があるとのこと。要するに、JSON形式の値でなければなんでもよいのですが、TInは"events.APIGatewayProxyRequest"型にしておくことで、API Gateway Proxy経由のパラメーターを受け取れるようになる、とのことなので、これを使うことにします。また、TOutについては、ステータスコード、レスポンスボディ、ヘッダー、それからBASE64でエンコードされているかどうかの4項目のJSON形式となっているようですが、今回はその中のステータスコードとレスポンスボディのみを返すこととし、下のように定義しておきます。

type Response struct {
	StatusCode int    `json:"statusCode"`
	Body       string `json:"body"`
}

つまり、正しいパラメーターが与えられた場合は、マンデルブロー集合の計算を行って、StatusCodeには200を入れ、さらにBodyにはBASE64エンコードした画像データを入れてリターンするようにします。

ということで、コードは以下のようになりました(ファイル名はmain.go)。

package main
import (
	"bytes"
	"encoding/base64"
	"fmt"
	"image"
	"image/color"
	"image/png"
	"math/cmplx"
	"strconv"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)
type Response struct {
	StatusCode int    `json:"statusCode"`
	Body       string `json:"body"`
}
func CalcGR(img *image.RGBA, rmin float64, rmax float64, imin float64, imax float64, offset int, nGR int, fchannel chan int) {
	for y := 0; y < 800; y++ {
		for x := offset; x < 800; x += nGR {
			var C = complex(rmin+(rmax-rmin)*float64(x)/800, imin+(imax-imin)*float64(y)/800)
			var V = C
			var it uint8 = 0
			for ; it <= 100; it++ {
				V = V*V + C
				if cmplx.Abs(V) > 2 {
					break
				}
			}
			img.Set(x, y, color.RGBA{uint8(cmplx.Abs(V) * 40), uint8(cmplx.Abs(V) * 120), it * 2, 255})
		}
	}
	fchannel <- 1
}
func getMapHandler(request events.APIGatewayProxyRequest) (Response, error) {
	realMin, err1 := strconv.ParseFloat(request.QueryStringParameters["realmin"], 64)
	realMax, err2 := strconv.ParseFloat(request.QueryStringParameters["realmax"], 64)
	imaginaryMin, err3 := strconv.ParseFloat(request.QueryStringParameters["imaginarymin"], 64)
	imaginaryMax, err4 := strconv.ParseFloat(request.QueryStringParameters["imaginarymax"], 64)
	if err1 != nil || err2 != nil || err3 != nil || err4 != nil ||
		realMin >= realMax || imaginaryMin >= imaginaryMax {
		return Response{
			StatusCode: 400,
			Body:       "incorrect parameter",
		}, nil
	}
	img := image.NewRGBA(image.Rect(0, 0, 800, 800))
	nGR := 4
	fchannel := make(chan int)
	defer close(fchannel)
	r := make([]int, nGR)
	for i := range r {
		go CalcGR(img, realMin, realMax, imaginaryMin, imaginaryMax, i, nGR, fchannel)
	}
	for range r {
		<-fchannel
	}
	var buff bytes.Buffer
	png.Encode(&buff, img)
	encodedString := base64.StdEncoding.EncodeToString(buff.Bytes())
	return Response{
		StatusCode: 200,
		Body:       encodedString,
	}, nil
}
func main() {
	lambda.Start(getMapHandler)
}

ビルドとZip処理

Lambdaでは、ビルドしたコードがLinux環境で実行されますので、このコードもLinux向けにビルドする必要があります。もちろん、Linuxマシン上でビルドするなら、デフォルト設定でビルドしてしまえばOKなのですが、Windows環境でビルドするのであれば、以下のコマンドで環境変数"GOOS"を"linux"にしておきます。

> set GOOS=linux

あとは、いつものようにビルドすれば、Linux向けバイナリが生成されます。

> go build -o main

これで、拡張子がない"main"というファイルが出来ます。

Lambdaで実行するには、バイナリーをZipしたものをアップロードする必要があるので、このファイル("main")をエクスプローラーのコンテキストメニュー「送る」→「圧縮(zip形式)フォルダー」などの方法でZipしておきます。

Lambda関数の作成とアップロード

では、Lambda関数を作成しましょう。AWSのコンソールにログインして、サービス一覧からLambdaを選ぶ、あるいはサービス検索ボックスに"Lambda"と入力して、"AWS Lambda"のページに移動します。右上に「関数の作成」ボタンがありますので、これを押して新しいLambda関数を作成しましょう。

画像1

デフォルトで「一から作成」選択されているので、そのまま下の欄の「関数名」に作成する関数名を入力しますが、今回は"lambdaMandel"としました。また、「ランタイム」のドロップダウンボックスで"Go 1.x"を選択します。残りの部分は今回はデフォルトのままで「作成」ボタンを押します。

続いてZipファイルのアップロードです。「コード」タブが選択されている状態で、「アップロード元」のドロップダウンボックスで「.zipファイル」を選び、先ほど作成したmain.zipをアップロードします。

画像2

そして、ここでもう一つ大事な作業として、このLambda関数のエントリーポイントとなるGoコード内の関数名を指定する必要があります。これは、「ランタイム設定」という項目にある「編集」ボタンを押して、関数名を"main"に変更します。

画像4

これで、Lambda関数の実行準備が整いました。

さっそくテスト実行して見ましょう。「テスト」タブに移動して、ページの右の方にある「テスト」ボタンを押すと、下のようにAPIが実行され、「実行結果:成功」と表示されれば、APIが実行されたと判断できます。

画像3

なお、ここではまだAPIにパラメーターを渡していないので、"incorrect parameter"と返してきていますが、それでも、Goで作ったコードが正しくアップロードされたこと、それからそのAPIがLambda実行環境で実行できたことは確認できたことになります。

API GatewayでAPIを公開する

作成したLambda関数は、このままでは外部から呼び出して使用することができません。そこで、同じくAWSのサービスであるAPI Gatewayを使って公開してみましょう。

まずは、AWSのコンソールでサービス一覧から(あるいはサービスを検索して)API Gatewayのページに移動します。そして、ページの右上にある「API作成」のボタンを押して、APIの作成を行います。

APIの種類はREST APIにしますので、「REST API」(プライベートではないほう)と書かれたボックス内の「構築」ボタンを押します。

続くページではAPI名を指定し、それ以外の項目はデフォルトのまま「APIの作成」ボタンを押します。

そして、「アクション」から「リソースの追加」を選択し、"Mandel"というリソースを作り、さらにその下に"getmap"というリソースを追加して、そこに「メソッドの追加」で”GET”メソッドを作りました。

画像5

ここで、先ほど作成したLambda関数を選択し、また「Lambdaプロキシ統合の使用」にチェックを入れて、「保存」ボタンを押します。

APIの構成が表示され、その左に「テスト」のリンクがありますので、これを押してAPIのテスト実行をしてみます。

画像7

下のように、APIのパラメーターを入力して「テスト」ボタンを押すと、下のようにBASE64でエンコードされた文字列が表示されましたので、どうやら上手く動いているようです。

画像6

それでは、APIの公開作業を行います。アクションから「APIのデプロイ」を選択し、「デプロイされるステージ」で「新しいステージ」を選んで、ステージ名を「beta」としました。「デプロイ」ボタンを押すと、下のようにAPIのURLが表示されます(青色バックの「URLの呼び出し」のところ)。

画像8

これが外部からAPIを呼び出すためのURLとなりますので、curlコマンドで実行してみましょう。

>curl -x GET "https://b9rpdojdpe.execute-api.ap-northeast-1.amazonaws.com/beta/mandel/getmap?realmin=-1.5&realmax=0.5&imaginarymin=-1.0&imaginarymax=1.0" 
iVBORw0KGgoAAAANSUhEUgAAAyAAAAMgCAIAAABUEpE/AACAAElEQVR4nOy9B5gb1dn+feuRVtJqq9e763UvuHfTMRhMN5hiTInpBAyEXgOhpb6pQBIIJbSQhBB6b6FjwAYb99772rvrrdpVH535rqmarpFWa/L+32+vuXSdOTozGo2kmd/ez32exzerA8IfLzw88hmBgReX3Bq80wDpWWM/73qVt9kVb+rkXT+lH2AcxttvUsBH82vZdNptaDvMPMDdU3l2avdpGGO/Sc6r5j0bXtduq/z67fZv2TAdWLca7vYm/Grtn73vLuI58GnwHJjSsFzl02KPYTVtNUbbqWk4PKV71vSUbY/dtnktwvl0GGD5LHPcg2b19osoHQUXRVpchEZMbMdMS1x4ZHGlHRfbaiOhPIqLrpEU20llSYCllLbY4FNC45RNlGxFsg2pNqQ6xCUsLFwnuC5xiSAdUY4wphxMHEe9Q9FdiNUjvhfxRiT2IdkCaVdHvExtS9CxGl2bENmGM9uo4T9o+w7h9YjuQLxBGDn1dWr8BC3fon0ZTl5LWx7H3ndx4KO09DoUVdBZnb7nGujgp8cvumR1OsYd+RbW/IIFeqPlW6o6jGv6hKs8EL2PwPZnmb8XiipRVAFfGbzFID88JJzhdBxcGIkW4eXmjqdl12HxVZjwa9/S66pvmME9Pr/17Bhe9TMwzI7RvONx6UB6vo3VHCOcmZZvsPd91E73B/uhbbFvzD3BhRexQedHS0dwe95iQ68QPspgHwpUsxcT/xlw9nHTv8Dq+9i+eay4H4r7IzQIocEoGYbS4SgbhfIxqBiPJ75j3hL4SuArVR6lpUxYisqEN+jxwryQT9/jMzbkAfr+TKe6qvRYtO0bVKSsFimdRfrHIvlZtSF1ahvCh2Jo++W2/Kgsv3uCwSN8gtpHwyoIHo/4aB5g6NSMtF41DO5+w2vq9ObQXnQJEy7QHvi0dCVfSgrEVXYDDLzltJoVs5w7rVgqCz9ZMpbdhvaDXWGTJTxZsVdWkMoPpyypyH2nW8YqLFRZwocVJNmxkRMzOcKWqwEF2aR7jZ/eTdbMZKYlNz36TqE/5Rqn3LFUQSgqT8YykxPLdILJi8WzYiNDVxpkUbFJhqeEZkkqwJTMEJKAR5zyyCkEzNlzp
...

BASE64のデータが送られてきたことが確認できます。これで、無事マンデルブロー集合の計算を行うAPIがLambdaとAPI Gatewayで公開できたことになります。

APIキーを使ってアクセスを制限することを始め、API Gatewayの設定はとても多くあるので、まだまだ勉強しながら設定していく必要があるのですが、今回の記事はここまでとさせていただきます。

――――――――――――――――――――――――――――――――――

執筆者プロフィール:水谷 裕一
大手外資系IT企業で15年間テストエンジニアとして、多数のプロジェクトでテストの自動化作業を経験。その後画像処理系ベンチャーを経てSHIFTに自動化エンジニアとして入社。
SHIFTでは、テストの自動化案件を2件こなした後、株式会社リアルグローブ・オートメーティッド(RGA)に出向中。RGAでは副社長という立場でありながら、エンジニアとしてAnsibleやOpenshiftに関する案件も担当。また、Ansibleの社内教育や、外部セミナー講師も行っている。
最近の趣味は電動キックボードでの散歩。

★ 過去記事をまとめたマガジン「SHIFTエンジニアが学びながら解説するGo言語」もご参照ください。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/