この記事について
Developers.IO 2020のサーバーレスセッションに触発されました。
[動画公開] 初めてのサーバーレスアプリケーション開発 #devio2020
というわけで、Golangを用いてAWSで基本的なサーバーレスをやってみたその手順をまとめました。
具体的には以下の手順を紹介します。
- GolangでLambdaを動かしAPI Gatewayと連携させる
- LambdaとDynamoDBと連携してAPIを作る
使用する環境・バージョン
- OS : macOS Mojave 10.14.5
- Golang : version go1.14 darwin/amd64
読者に求める前提知識
Golangの基本的な文法がわかること。
Lambda関数の作成
コンソールで関数を作成
AWS Lambdaのコンソールを開くと、以下のような画面になります。
右上にある「関数の作成」ボタンをクリックします。
すると、以下のような関数作成画面に遷移します。
- 関数名: 好きな名前を入力(今回はmyTestFunction)
- ランタイム: Go1.x
次にLambda関数のアクセス権限の設定をします。
「実行ロールの選択または作成」のプルダウンを開くと、以下のようなフォームが表れます。
今回Lambdaを動かすのは初めてなので、「基本的なLambdaアクセス権限で新しいロールを作成」を選択します。
これで、Lambda関数作成時に、関数のログをCloudWatchに出力するためのロールが作られます。
ここまでの入力が終わったら、関数を作成します。
正常に作成されたら、以下のような画面になります。
補足: このとき関数と同時に作られたロールとCloudWatchのロググループは、このLambda関数を削除しても消されず残ったままになります。つまり、ロール・ロググループの削除は、Lambda関数の削除とは別に手動で行う必要があるということです。
関数のコードを作成
作ったばかりの関数の中身は「hello,world」を返すだけのデフォルト状態なので、これからLambda上で動かしたいプログラムを別に書いてやる必要があります。
今は手始めに「httpリクエストを受けたら、httpメソッド・リクエストボディ・パスパラメータ・クエリパラメータをjsonにして返す」という関数を作成してみます。
まずは、ローカルに必要なライブラリをインストールします。
go get -u github.com/aws/aws-lambda-go/lambda
go get -u github.com/aws/aws-lambda-go/events
インストールしたら、コードを書いていきます。
package main
import (
"encoding/json"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
type Response struct {
RequestMethod string `json:"RequestMethod"`
RequestBody string `json:"RequestBody"`
PathParameter string `json:"PathParameter"`
QueryParameter string `json:"QueryParameter"`
}
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// httpリクエストの情報を取得
method := request.HTTPMethod
body := request.Body
pathParam := request.PathParameters["pathparam"]
queryParam := request.QueryStringParameters["queryparam"]
// レスポンスとして返すjson文字列を作る
res := Response{
RequestMethod: method,
RequestBody: body,
PathParameter: pathParam,
QueryParameter: queryParam,
}
jsonBytes, _ := json.Marshal(res)
// 返り値としてレスポンスを返す
return events.APIGatewayProxyResponse{
Body: string(jsonBytes),
StatusCode: 200,
}, nil
}
func main() {
lambda.Start(handler)
}
参考:AWS公式ドキュメント Go の AWS Lambda 関数ハンドラー
参考:Go+Lambdaで最速サーバレスチュートリアル
ここで、コードについていくつか解説します。
main関数について
Lambdaで実行されるのはmain関数です。そのため、Lambdaにアップロードするコードには必ずmain関数を用意してやる必要があります。
今回は、handler
というAPIハンドラ(関数)を起動する操作をmain関数に書きました。
API Gatewayからhttpリクエストの情報を取得する方法
ハンドラはevents.APIGatewayProxyRequest
型の変数request
を引数にとっています。この変数request
の中に、どのようなhttpリクエストを受け取ったかの情報が格納されています。
例えば、今回の場合は以下のように情報を取得しています。
- リクエストメソッド:
request.HTTPMethod
で取得 - リクエストボディ:
request.Body
で取得 - パスパラメータ(
/{pathparam}
とAPI Gatewayで設定した部分):equest.PathParameters["pathparam"]
で取得 - クエリパラメータ(
/?queryparam=
の部分):request.QueryStringParameters["queryparam"]
で取得
events.APIGatewayProxyRequest
型の定義をGo Docで確認すると、他にもどのようなフィールドがあるのかがわかります。やりたい処理に合わせて活用すればよいでしょう。
type APIGatewayProxyRequest struct {
Resource string `json:"resource"` // The resource path defined in API Gateway
Path string `json:"path"` // The url path for the caller
HTTPMethod string `json:"httpMethod"`
Headers map[string]string `json:"headers"`
MultiValueHeaders map[string][]string `json:"multiValueHeaders"`
QueryStringParameters map[string]string `json:"queryStringParameters"`
MultiValueQueryStringParameters map[string][]string `json:"multiValueQueryStringParameters"`
PathParameters map[string]string `json:"pathParameters"`
StageVariables map[string]string `json:"stageVariables"`
RequestContext APIGatewayProxyRequestContext `json:"requestContext"`
Body string `json:"body"`
IsBase64Encoded bool `json:"isBase64Encoded,omitempty"`
}
参考:GoDoc package aws/aws-lambda-go/events
API Gatewayにレスポンスを返す方法
ハンドラは返り値にevents.APIGatewayProxyResponse
型をとります。なので、所望のレスポンス内容に沿ったこの型の変数を作成するのが、ハンドラ内で行う処理内容です。
events.APIGatewayProxyResponse
型の定義は以下のようになっています。
type APIGatewayProxyResponse struct {
StatusCode int `json:"statusCode"`
Headers map[string]string `json:"headers"`
MultiValueHeaders map[string][]string `json:"multiValueHeaders"`
Body string `json:"body"`
IsBase64Encoded bool `json:"isBase64Encoded,omitempty"`
}
参考:GoDoc package aws/aws-lambda-go/events
今回の場合は、以下のようにレスポンスを作っています。
- StatusCode: httpレスポンスコード200を指定
- Body: 自作の構造体(
Response
)からjson.Marshal
→string
と変換
コードをLambdaにアップロード
Lambdaにアップロードするのはコンパイル済みの実行ファイルである必要があるので、上で書いたhello.go
をビルドしてバイナリファイルhello
を作ります。
また、アップロードの形式がzipファイルなので、ビルド後にhello
をzip圧縮します。
$ GOOS=linux GOARCH=amd64 go build -o hello hello.go
$ zip hello.zip hello
先ほど作ったmyTestFunction
関数をLambdaコンソールで開き、以下の設定画面からhello.zip
をアップロードします。
注意:アップロードする実行ファイルの名前
「一から作成」のオプションから作成したLambda関数に渡す実行ファイルの名前は、必ずhello
である必要があります。
これは一から作成のLambda関数がデフォルトで「hello
という名前のバイナリファイルを実行する」という設定になっているため、他の名前だと以下のようなPathErrorが起きます。
{
“errorMessage”: “fork/exec /var/task/binaryname: permission denied”,
“errorType”: “PathError”
}
Lambdaのテスト
Lambda関数は、Webコンソール上でテストを実行することができます。
コンソール上で、右上の「テスト」のボタンをクリックします。
すると、テストリクエストを編集する画面が表れます。
デフォルトだとこのような状態です。このまま名前をつけて保存します。
この状態で「テスト」ボタンをクリックすると、テストが実行・結果が表示されます。
きちんとステータスコード200が返ってくることが確認できました。
補足: このテストはAPI Gateway経由のリクエストを送っているわけではないので、httpメソッドやボディなどのリクエスト情報が空のときの結果が表示されています。
API Gatewayと連携
APIの作成
開始画面から、「今すぐはじめる」ボタンをクリックします。 すると、以下のようなAPI作成画面になります。 以下のような設定を入力して作成します。- プロトコル: REST
- 新しいAPIの作成: 新しいAPI
- API名: 好きな名前をつける
- 説明: 好きな説明文を書く
- エンドポイントタイプ: リージョン
URLリソースの作成
まずは、URLリソースの作成を行います。「アクション」→「リソースの作成」を選択します。
以下のような、パスパラメータの名前等を設定する画面になります。
- プロキシリソース: なし
- リソース名: 好きな名前をつける
- リソースパス: {pathparam}
- API Gateway CORS : なし
以上の設定でリソースを作成します。
メソッドの作成
次に、httpリクエストメソッドーLambda関数の紐付けを行います。
/{pathparam}
を選択した状態で、「アクション」→「メソッドの作成」を選択します。
プルダウンから、設定を行いたいリクエストメソッドを選択します。GETやPOSTなどの特定メソッドの選択はもちろん、全てのメソッドに対しての設定を行いたい場合はANYという選択肢もあります。
今回はANYを選択します。
メソッドを選択したら、その選択メソッドにどんな処理(Lambda関数)を紐づけるのかの設定画面が表示されます。
- 統合タイプ: Lambda関数
- Lambdaプロキシ統合の使用: あり
- Lambdaリージョン: ap-northeast-1
- Lambda関数: myTestFunction
- デフォルトタイムアウトの使用: あり
以上の設定で保存をクリックすると、以下のような確認画面が出ます。
問題ないので、OKを選択します。結果は以下の通り。
テストの実行
上の画面で「テスト⚡️」をクリックすることで、API Gatewayからリクエストを送ったときにきちんと動くかどうかのテストを実行することができます。
このように、パスパラメータ・クエリパラメータ・リクエストメソッド・ボディなどを自由にコンソール上で設定して、それに対してどのような応答が返ってくるのかを確認することができます。APIのデプロイ
実際にAPIをデプロイするには、「アクション」→「APIのデプロイ」を選択します。
すると、デプロイステージを指定する画面になります。
ステージを指定して「デプロイ」ボタンを押すと、以下のような画面に遷移します。
ここで、APIが公開されているURLを確認することができます。
参考:【AWS】API Gateway + LambdaでAPIをつくる
DynamoDBと連携
ここからは、[動画公開] 初めてのサーバーレスアプリケーション開発 #devio2020で紹介された、以下のようなCRUDを行うDB連携APIを新しく作っていきます。
テーブル作成
テーブル作成画面で、以下のような設定を入力して作成します。- テーブル名: 好きな名前(ここではuser)
- プライマリーキー: userid(数値)
- ソートキー: 追加しない
- テーブル設定: デフォルト設定の使用
補足: プライマリーキーに設定できるデータ型は文字列orバイナリor数値です。
作成に成功すると、以下のような画面に遷移します。データ項目の追加
作成直後は、プライマリーキー以外の属性が存在しないので、他のキーが欲しいのならば手動で追加する必要があります。
テーブルの項目タブを開きます。
「項目の作成」ボタンをクリックすると、以下のような項目編集画面が表示されます。
+をクリックします。
Appendを選択します。
すると、どういう型のフィールドを追加するかを選択できます。
今回はStringを選択しました。
すると、String型のフィールドが追加されました。好きにフィールド名をつけたり、値も設定を行ったりします。
項目の追加を複数回行い、最終的にこうなりました。これで保存します。
無事にプライマリーキー以外の項目が作成されたことが確認できます。
参考:初めてのサーバーレスアプリケーション開発 ~DynamoDBにテーブルを作成する~
Lambda用のロールを作成
Lambda関数がDynamoDBにアクセスできるように、Lambda関数に付与するロールを作成します。
IAMコンソール→ロールから作成画面を開きます。
AWSサービス、Lambdaを選択します。
そして、以下のアクセス権限を追加して、ロールを作成します。
- AmazonDynamoDBFullAccess
- AWSLambdaDynamoDBExecutionRole
DynamoDBとの接続を必要とするLambda関数には、今作ったロールを付加します。
参考:初めての、LambdaとDynamoDBを使ったAPI開発
Lambda関数のコードを作成
いよいよDBにアクセスしてCRUD操作を行う関数コードを書いていきます。
まず、必要なパッケージをダウンロードします。
$ go get -u github.com/aws/aws-sdk-go/aws
$ go get -u github.com/aws/aws-sdk-go/aws/session
$ go get -u github.com/aws/aws-sdk-go/service/dynamodb
Create(POST)の作成
package main
import (
"encoding/json"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
// Item DBに入れるデータ
type Item struct {
UserID int `dynamodbav:"userid" json:userid`
Address string `dynamodbav:"address" json:address`
Email string `dynamodbav:"email" json:email`
Gender string `dynamodbav:"gender" json:gender`
Name string `dynamodbav:"name" json:name`
}
// Response Lambdaが返答するデータ
type Response struct {
RequestMethod string `json:RequestMethod`
Result Item `json:Result`
}
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
method := request.HTTPMethod
// DBと接続するセッションを作る→DB接続
sess, err := session.NewSession()
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
db := dynamodb.New(sess)
// リクエストボディのjsonから、Item構造体(DB用データの構造体)を作成
reqBody := request.Body
resBodyJSONBytes := ([]byte)(reqBody)
item := Item{}
if err := json.Unmarshal(resBodyJSONBytes, &item); err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
// Item構造体から、inputするデータを用意
inputAV, err := dynamodbattribute.MarshalMap(item)
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
input := &dynamodb.PutItemInput{
TableName: aws.String("user"),
Item: inputAV,
}
// insert実行
_, err = db.PutItem(input)
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
// httpレスポンス作成
res := Response{
RequestMethod: method,
}
jsonBytes, _ := json.Marshal(res)
return events.APIGatewayProxyResponse{
Body: string(jsonBytes),
StatusCode: 200,
}, nil
}
func main() {
lambda.Start(handler)
}
ここでやっていることは以下の操作です。
- DBに接続
- リクエストボディからItem構造体を作る
- Item構造体から、DBにinsertするためのデータを作る
- insertを実行する
- レスポンスを作成
1. DBに接続
以下のコードが該当します。
sess, err := session.NewSession()
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
db := dynamodb.New(sess)
これは、DynamoDBに接続する処理を行うときには必ず必要な定型句といってもいいでしょう。
2. リクエストボディからItem構造体を作る
httpリクエストボディに、DBに挿入したいデータがjson形式で格納されています。
{
"userid": 2,
"address": "Osaka",
"email": "bbb.jp",
"gender": "F",
"name": "Nancy"
}
request.Body
で得られるリクエストボディはstring型なので、これをjson.Unmarshal
でパースして構造体形式(上だとItem構造体)に変換することで、ボディに格納されているデータを扱えるようにします。
3. Item構造体から、DBにinsertするためのデータを作る
DynamoDBにデータを挿入する関数は、db.PutItem()
です。
func (c *DynamoDB) PutItem(input *PutItemInput) (*PutItemOutput, error)
参考:GoDoc github.com/aws/aws-sdk-go/service/dynamodb#DynamoDB.PutItem
しかし、見ての通りこの関数の引数はdynamodb.PutItemInput
型なので、Item型をそのままDBに渡すことはできません。そのため、Item型をdynamodb.PutItemInput
型に変換する必要があります。
dynamodb.PutItemInput
型の定義を見てみましょう。
type PutItemInput struct {
// 今回関係ないフィールドを省略
Item map[string]*AttributeValue `type:"map" required:"true"`
TableName *string `min:"3" type:"string" required:"true"`
}
参考:GoDoc github.com/aws/aws-sdk-go/service/dynamodb#PutItemInput
TableName
は、データを追加したDynamoDBのテーブル名を指定するフィールドです。
追加するデータの内容を入れるフィールドは、map[string]*AttributeValue
型のItem
です。つまり、2でリクエストボディから作ったItem
型構造体を、このmap[string]*AttributeValue
型に変換してやる必要があるわけです。
まさに、この変換を行う関数が公式から提供されています。dynamodbattribute.MarshalMap
という関数です。
// Item型のitem変数を、map[string]*AttributeValue型のimputAVに変換
inputAV, err := dynamodbattribute.MarshalMap(item)
この変換を正しく行うためには、Item型構造体に指定のメタタグをつける必要があります。
(json.Unmarshal
でjsonを構造体にパースするために、構造体にjsonタグをつけたのと同じ論理です)
ここで、Item型を以下のように定義していました。
type Item struct {
UserID int `dynamodbav:"userid" json:userid`
Address string `dynamodbav:"address" json:address`
Email string `dynamodbav:"email" json:email`
Gender string `dynamodbav:"gender" json:gender`
Name string `dynamodbav:"name" json:name`
}
この構造体の各フィールドにつけているdynamodbav
タグは、「各フィールドがDynamoDBのどのキーに対応しているか」ということを示しています。例えば、UserIDフィールドは、DynamoDBではuseridキーに紐づいています。
dynamodbattribute.MarshalMap
を実行するためには、各フィールドにこのdynamodbav
タグを確実につけましょう。
4. insertを実行する
db.PutItem()
を実行します。
5. レスポンスを作成
APIを作成したときと要領は同じなので割愛します。
Read(GET)の作成
package main
import (
"encoding/json"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
// Item構造体とResponse構造体は、Createのときと同じなので割愛
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
method := request.HTTPMethod
pathparam := request.PathParameters["userid"]
// DB接続
sess, err := session.NewSession()
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
db := dynamodb.New(sess)
// 検索条件を用意
getParam := &dynamodb.GetItemInput{
TableName: aws.String("user"),
Key: map[string]*dynamodb.AttributeValue{
"userid": {
N: aws.String(pathparam),
},
},
}
// 検索
result, err := db.GetItem(getParam)
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 404,
}, err
}
// 結果を構造体にパース
item := Item{}
err = dynamodbattribute.UnmarshalMap(result.Item, &item)
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
// httpレスポンス作成
res := Response{
RequestMethod: method,
Result: item,
}
jsonBytes, _ := json.Marshal(res)
return events.APIGatewayProxyResponse{
Body: string(jsonBytes),
StatusCode: 200,
}, nil
}
// main関数はCreateのときと同じなので割愛
ここでやっていることは以下の操作です。
- DBに接続(説明割愛)
- DBに問い合わせる検索条件を作る
- DBに問い合わせてデータを取得する
- レスポンスを作成(説明割愛)
2. DBに問い合わせる検索条件を作る
DBに問い合わせてデータを取得する関数はdb.GetItem()
です。
func (c *DynamoDB) GetItem(input *GetItemInput) (*GetItemOutput, error)
参考:GoDoc github.com/aws/aws-sdk-go/service/dynamodb#DynamoDB.GetItem
この引数としてとるのはdynamodb.GetItemInput
型なので、この型の変数を作成します。
dynamodb.GetItemInput
型の定義を確認します。
type GetItemInput struct {
// 今回関係ないフィールドを省略
Key map[string]*AttributeValue `type:"map" required:"true"`
TableName *string `min:"3" type:"string" required:"true"`
}
TableName
はCreateのときと同様に、対象テーブルの名前を指定するフィールド、Key
は、取得したいレコードのプライマリーキーを指定するフィールドです。
そのため、db.GetItem()
に渡すdynamodb.GetItemInput
型引数を以下のように作成します。
getParam := &dynamodb.GetItemInput{
TableName: aws.String("user"),
Key: map[string]*dynamodb.AttributeValue{
"userid": {
N: aws.String(pathparam),
},
},
}
このコードの意味は以下の通りです。
- TableName: userテーブルを検索
- Key: ここでは、Number型(N)のキーであるuseridの値が、
aws.String(pathparam)
であるデータを検索するという意味
Key
フィールドで、useridキーが数値型であることを、N
という風に指定しています。各データ型がどの表現に対応するのかは以下の表をご覧ください。
データ型 | アルファベット |
---|---|
バイナリ | B |
ブール型 | BOOL |
バイナリセット | BS |
リスト | L |
マップ | M |
数値 | N |
数値セット | NS |
null | NULL |
文字列 | S |
文字列セット | SS |
また、Key
フィールドは、dynamodb.PutItemInput
型のItem
フィールドと同じくmap[string]*AttributeValue
型なのです。しかし、Createのときと同様にKey
フィールドをdynamodbattribute.MarshalMap
関数から作ろうとしてもうまくいきません。
// ダメな例
searchItem := Item{UserID: userid}
searchAV, _ := dynamodbattribute.MarshalMap(searchItem)
getParam := &dynamodb.GetItemInput{
TableName: aws.String("user"),
Key: searchAV,
}
これはおそらくdynamodb.PutItemInput.Item
フィールドとは異なり、Key
フィールドには「プライマリーキーの情報だけを含めなければいけない」という仕様が関係していると推測されます。
dynamodbattribute.MarshalMap(Item{UserID: userid})
には、主キーであるUserID以外にも、ゼロ値に設定された他フィールドが含まれてしまっているのでうまくいかないんだと思います。
3. DBに問い合わせてデータを取得する
db.GetItem()
を実行すればOKです。
Update(PUT)の作成
package main
import (
"encoding/json"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/expression"
)
// Item構造体とResponse構造体は、Createのときと同じなので割愛
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
method := request.HTTPMethod
pathparam := request.PathParameters["userid"]
// まずはDBと接続するセッションを作る
sess, err := session.NewSession()
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
db := dynamodb.New(sess)
// リクエストボディのjsonから、Item構造体を作成
reqBody := request.Body
resBodyJSONBytes := ([]byte)(reqBody)
item := Item{}
if err := json.Unmarshal(resBodyJSONBytes, &item); err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
// updateするデータを作る
update := expression.UpdateBuilder{}
if address := item.Address; address != "" {
update = update.Set(expression.Name("address"), expression.Value(address))
}
if email := item.Email; email != "" {
update = update.Set(expression.Name("email"), expression.Value(email))
}
if gender := item.Gender; gender != "" {
update = update.Set(expression.Name("gender"), expression.Value(gender))
}
if name := item.Name; name != "" {
update = update.Set(expression.Name("name"), expression.Value(name))
}
expr, err := expression.NewBuilder().WithUpdate(update).Build()
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
input := &dynamodb.UpdateItemInput{
TableName: aws.String("user"),
Key: map[string]*dynamodb.AttributeValue{
"userid": {
N: aws.String(pathparam),
},
},
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
UpdateExpression: expr.Update(),
}
// update実行
_, err = db.UpdateItem(input)
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
// httpレスポンス作成
res := Response{
RequestMethod: method,
}
jsonBytes, _ := json.Marshal(res)
return events.APIGatewayProxyResponse{
Body: string(jsonBytes),
StatusCode: 200,
}, nil
}
// main関数はCreateのときと同じなので割愛
ここでやっていることは以下の操作です。
- DBに接続(説明割愛)
- リクエストボディからItem構造体を作る
- DBのデータをどう更新するかを指定する
- update実行
- レスポンスを作成(説明割愛)
2. リクエストボディからItem構造体を作る
Createのときとやり方は全く同じです。
今回は、パスパラメータで指定されたuseridレコードの、emailとnameを更新したくて以下のようなリクエストボディがきたと仮定します。
{
"email": "ccc.com",
"name": "Emily"
}
3. DBのデータをどう更新するかを指定する
更新を行うdb.UpdateItem
の引数となるdynamodb.UpdateItemInput
型の変数を作成します。
dynamodb.UpdateItemInput
型の定義は以下の通りです。
type UpdateItemInput struct {
// 今回関係ないフィールドを省略
ExpressionAttributeNames map[string]*string `type:"map"`
ExpressionAttributeValues map[string]*AttributeValue `type:"map"`
Key map[string]*AttributeValue `type:"map" required:"true"`
TableName *string `min:"3" type:"string" required:"true"`
UpdateExpression *string `type:"string"`
}
参考:GoDoc github.com/aws/aws-sdk-go/service/dynamodb#UpdateItemInput
Key
とTableName
についてはReadのときと意味は同様です。
残り3つのフィールドについては、データの更新の種類・やり方について記述する場所です。「データの更新」といっても、ただ今ある値を捨てて新しい値に書き換えるだけではなく、データ型によって様々な操作が考えられます。主たる例を以下に挙げます。
- 数値型を収める属性Aを、Bという値に上書き保存したい
- 属性Aが保持しているリストに、Bという値を追加したい
- 指定したレコードから属性Aを消したい
- 属性Aが保持しているセット型から、Bというセットを消したい
参考:DynamoDBでデータを更新する際に使うUpdateExpressionについて一通りまとめてみた
そのため、「その属性を操作したいか」をExpressionAttributeNames
に、「上書きしたり追加したりしたい値」をExpressionAttributeValues
に、「上書きなのか追加なのかという更新の種類」をUpdateExpression
に記述するのです。
例えば、今回の「"name"という属性を、変数name
の値に上書きしたい」という操作をドキュメントどおりに記述するのならば以下のようになります。
input := &dynamodb.UpdateItemInput{
TableName: aws.String("user"),
Key: map[string]*dynamodb.AttributeValue{
"userid": {
N: aws.String(pathparam),
},
},
ExpressionAttributeNames: map[string]*string{
// "name"という属性名を以下#nameと扱う
"#name": aws.String("name"),
},
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
// 上書きしたい値nameを以下:name_valueとして扱う
":name_value": {
S: aws.String(name),
},
},
// #name属性を、:name_valueという値に上書き(set)する
UpdateExpression: aws.String("set #name = :name_value"),
}
参考:あえて aws-sdk-go で dynamoDB を使うときの基本操作
しかし、属性名を#
で指定したり、更新したい値を:
で指定したりするドキュメントどおりの書き方は少々面倒です。
そのため、これらの構造表現をコードベースで生成するパッケージが公式から提供されています(github.com/aws/aws-sdk-go/service/dynamodb/expression)。せっかくなのでその方法に書き換えていきましょう。
まずは、expression.UpdateBuilder{}
という型の構造体を用意して、その型のメソッドを用いて「どう更新したいのか」を記述します。
update := expression.UpdateBuilder{}
if name := item.Name; name != "" {
update = update.Set(expression.Name("name"), expression.Value(name))
}
上の部分は、「DBの"name"という属性を、name
という変数の中身に上書きする」という操作を、expression.UpdateBuilder{}
型のupdate
に記録しています。
このupdateの内容を指定し終わったら、updateの内容をExpressionAttributeNames
等のフィールドに入れられる形に変換します。
expr, err := expression.NewBuilder().WithUpdate(update).Build()
このexprを使って、db.UpdateItem
を作ると以下のようになります。
input := &dynamodb.UpdateItemInput{
TableName: aws.String("user"),
Key: map[string]*dynamodb.AttributeValue{
"userid": {
// Nはnumber型の意味
N: aws.String(pathparam),
},
},
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
UpdateExpression: expr.Update(),
}
4. update実行
db.UpdateItem
を実行すればOKです。
Delete(DELETE)の作成
package main
import (
"encoding/json"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
// Item構造体とResponse構造体は、Createのときと同じなので割愛
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
method := request.HTTPMethod
pathparam := request.PathParameters["userid"]
// まずはDBと接続するセッションを作る
sess, err := session.NewSession()
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
db := dynamodb.New(sess)
// deleteするデータを指定
deleteParam := &dynamodb.DeleteItemInput{
TableName: aws.String("user"),
Key: map[string]*dynamodb.AttributeValue{
"userid": {
// Nはnumber型の意味
N: aws.String(pathparam),
},
},
}
// delete実行
_, err = db.DeleteItem(deleteParam)
if err != nil {
return events.APIGatewayProxyResponse{
Body: err.Error(),
StatusCode: 500,
}, err
}
// httpレスポンス作成
res := Response{
RequestMethod: method,
}
jsonBytes, _ := json.Marshal(res)
return events.APIGatewayProxyResponse{
Body: string(jsonBytes),
StatusCode: 200,
}, nil
}
// main関数はCreateのときと同じなので割愛
ここでやっていることは以下の操作です。
- DBに接続(説明割愛)
- deleteするデータを指定する
- delete実行
- レスポンスを作成(説明割愛)
2. deleteするデータを指定する
Read(GET)のときと同じ方法でdynamodb.DeleteItemInput
型の変数を作り、消去したいデータを指定します。
3. delete実行
db.DeleteItem()
を実行すればOKです。
Lambda関数コードの参考文献
参考:AWS Lambda, API Gateway, DynamoDB, Golang でREST APIを作る
参考:DynamoDB×Go連載#2 AWS SDKによるDynamoDBの基本操作
Lambda関数のコードをアップロード→API Gatewayと連携
ここは既にやった手順と同じなので割愛します。
上述した通り、Lambda関数にDynamoDB用のロールを付与するのを忘れないでください。
まとめ
これでAPI Gateway-Lambda-Dynamo DBの3つを連携させたサーバーレスAPIの構築が完了です。お疲れ様でした。