Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
18
Help us understand the problem. What is going on with this article?
@saki-engineering

AWS Lambda+API Gateway+DynamoDBでCRUD APIを作るのをGolangでやってみた

この記事について

Developers.IO 2020のサーバーレスセッションに触発されました。
[動画公開] 初めてのサーバーレスアプリケーション開発 #devio2020

というわけで、Golangを用いてAWSで基本的なサーバーレスをやってみたその手順をまとめました。
具体的には以下の手順を紹介します。

  1. GolangでLambdaを動かしAPI Gatewayと連携させる
  2. LambdaとDynamoDBと連携してAPIを作る

使用する環境・バージョン

  • OS : macOS Mojave 10.14.5
  • Golang : version go1.14 darwin/amd64

読者に求める前提知識

Golangの基本的な文法がわかること。

Lambda関数の作成

コンソールで関数を作成

AWS Lambdaのコンソールを開くと、以下のような画面になります。
スクリーンショット 2020-07-28 19.21.38.png
右上にある「関数の作成」ボタンをクリックします。
すると、以下のような関数作成画面に遷移します。

スクリーンショット 2020-07-28 19.23.07.png
「一から作成」を選択し、関数名・ランタイムを記入します。今回は以下のように設定しました。

  • 関数名: 好きな名前を入力(今回はmyTestFunction)
  • ランタイム: Go1.x

次にLambda関数のアクセス権限の設定をします。
「実行ロールの選択または作成」のプルダウンを開くと、以下のようなフォームが表れます。
スクリーンショット 2020-07-28 19.24.25.png
今回Lambdaを動かすのは初めてなので、「基本的なLambdaアクセス権限で新しいロールを作成」を選択します。
これで、Lambda関数作成時に、関数のログをCloudWatchに出力するためのロールが作られます。

ここまでの入力が終わったら、関数を作成します。
正常に作成されたら、以下のような画面になります。
スクリーンショット 2020-07-28 19.25.31.png
補足: このとき関数と同時に作られたロールと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

インストールしたら、コードを書いていきます。

hello.go
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.Marshalstringと変換

コードをLambdaにアップロード

Lambdaにアップロードするのはコンパイル済みの実行ファイルである必要があるので、上で書いたhello.goをビルドしてバイナリファイルhelloを作ります。
また、アップロードの形式がzipファイルなので、ビルド後にhelloをzip圧縮します。

$ GOOS=linux GOARCH=amd64 go build -o hello hello.go
$ zip hello.zip hello

参考:AWS Lambda× Goを試す

先ほど作ったmyTestFunction関数をLambdaコンソールで開き、以下の設定画面からhello.zipをアップロードします。
スクリーンショット 2020-07-28 21.35.42.png

注意:アップロードする実行ファイルの名前

「一から作成」のオプションから作成したLambda関数に渡す実行ファイルの名前は、必ずhelloである必要があります。
これは一から作成のLambda関数がデフォルトで「helloという名前のバイナリファイルを実行する」という設定になっているため、他の名前だと以下のようなPathErrorが起きます。

{
   “errorMessage”: “fork/exec /var/task/binaryname: permission denied”, 
   “errorType”: “PathError”
}

Lambdaのテスト

Lambda関数は、Webコンソール上でテストを実行することができます。
スクリーンショット 2020-08-01 14.32.53.png
コンソール上で、右上の「テスト」のボタンをクリックします。
すると、テストリクエストを編集する画面が表れます。
スクリーンショット 2020-07-29 14.28.35.png
デフォルトだとこのような状態です。このまま名前をつけて保存します。
スクリーンショット 2020-07-29 15.14.14.png
この状態で「テスト」ボタンをクリックすると、テストが実行・結果が表示されます。
スクリーンショット 2020-08-01 14.33.50.png
きちんとステータスコード200が返ってくることが確認できました。

補足: このテストはAPI Gateway経由のリクエストを送っているわけではないので、httpメソッドやボディなどのリクエスト情報が空のときの結果が表示されています。

API Gatewayと連携

APIの作成

スクリーンショット 2020-07-28 21.43.51.png
開始画面から、「今すぐはじめる」ボタンをクリックします。
すると、以下のようなAPI作成画面になります。
スクリーンショット 2020-07-28 21.45.29.png
以下のような設定を入力して作成します。

  • プロトコル: REST
  • 新しいAPIの作成: 新しいAPI
  • API名: 好きな名前をつける
  • 説明: 好きな説明文を書く
  • エンドポイントタイプ: リージョン

スクリーンショット 2020-07-28 21.46.25.png
APIを作成したら、「どのパス・どのメソッドにどの処理を結びつけるか」の設定を行う画面が表示されます。

URLリソースの作成

まずは、URLリソースの作成を行います。「アクション」→「リソースの作成」を選択します。
スクリーンショット 2020-07-28 21.57.13.png
以下のような、パスパラメータの名前等を設定する画面になります。
スクリーンショット 2020-07-28 22.04.28.png

  • プロキシリソース: なし
  • リソース名: 好きな名前をつける
  • リソースパス: {pathparam}
  • API Gateway CORS : なし

以上の設定でリソースを作成します。

メソッドの作成

次に、httpリクエストメソッドーLambda関数の紐付けを行います。
スクリーンショット 2020-07-28 22.05.23.png
/{pathparam}を選択した状態で、「アクション」→「メソッドの作成」を選択します。
スクリーンショット 2020-08-01 15.21.17.png
プルダウンから、設定を行いたいリクエストメソッドを選択します。GETやPOSTなどの特定メソッドの選択はもちろん、全てのメソッドに対しての設定を行いたい場合はANYという選択肢もあります。

今回はANYを選択します。
メソッドを選択したら、その選択メソッドにどんな処理(Lambda関数)を紐づけるのかの設定画面が表示されます。
スクリーンショット 2020-08-01 15.21.44.png

  • 統合タイプ: Lambda関数
  • Lambdaプロキシ統合の使用: あり
  • Lambdaリージョン: ap-northeast-1
  • Lambda関数: myTestFunction
  • デフォルトタイムアウトの使用: あり

以上の設定で保存をクリックすると、以下のような確認画面が出ます。
スクリーンショット 2020-07-28 21.58.34.png
問題ないので、OKを選択します。結果は以下の通り。
スクリーンショット 2020-08-01 15.22.24.png

テストの実行

上の画面で「テスト⚡️」をクリックすることで、API Gatewayからリクエストを送ったときにきちんと動くかどうかのテストを実行することができます。

スクリーンショット 2020-07-31 14.54.26.png
このように、パスパラメータ・クエリパラメータ・リクエストメソッド・ボディなどを自由にコンソール上で設定して、それに対してどのような応答が返ってくるのかを確認することができます。

APIのデプロイ

実際にAPIをデプロイするには、「アクション」→「APIのデプロイ」を選択します。
スクリーンショット 2020-07-28 22.22.33.png
すると、デプロイステージを指定する画面になります。
スクリーンショット 2020-07-28 22.24.51.png
ステージを指定して「デプロイ」ボタンを押すと、以下のような画面に遷移します。
ここで、APIが公開されているURLを確認することができます。
スクリーンショット 2020-07-28 22.25.01.png

参考:【AWS】API Gateway + LambdaでAPIをつくる

DynamoDBと連携

ここからは、[動画公開] 初めてのサーバーレスアプリケーション開発 #devio2020で紹介された、以下のようなCRUDを行うDB連携APIを新しく作っていきます。
スクリーンショット 2020-07-31 15.13.36.png

テーブル作成

スクリーンショット 2020-07-31 15.42.54.png
テーブル作成画面で、以下のような設定を入力して作成します。

  • テーブル名: 好きな名前(ここではuser)
  • プライマリーキー: userid(数値)
  • ソートキー: 追加しない
  • テーブル設定: デフォルト設定の使用

補足: プライマリーキーに設定できるデータ型は文字列orバイナリor数値です。

スクリーンショット 2020-07-31 15.58.43.png
作成に成功すると、以下のような画面に遷移します。

データ項目の追加

作成直後は、プライマリーキー以外の属性が存在しないので、他のキーが欲しいのならば手動で追加する必要があります。
テーブルの項目タブを開きます。
スクリーンショット 2020-07-31 16.04.50.png
「項目の作成」ボタンをクリックすると、以下のような項目編集画面が表示されます。
スクリーンショット 2020-07-31 16.05.09.png
+をクリックします。
スクリーンショット 2020-07-31 16.05.20.png
Appendを選択します。
スクリーンショット 2020-07-31 16.05.27.png
すると、どういう型のフィールドを追加するかを選択できます。
今回はStringを選択しました。
スクリーンショット 2020-07-31 16.06.07.png
すると、String型のフィールドが追加されました。好きにフィールド名をつけたり、値も設定を行ったりします。
スクリーンショット 2020-07-31 16.08.41.png
項目の追加を複数回行い、最終的にこうなりました。これで保存します。
スクリーンショット 2020-07-31 16.08.47.png
無事にプライマリーキー以外の項目が作成されたことが確認できます。

参考:初めてのサーバーレスアプリケーション開発 ~DynamoDBにテーブルを作成する~

Lambda用のロールを作成

Lambda関数がDynamoDBにアクセスできるように、Lambda関数に付与するロールを作成します。

IAMコンソール→ロールから作成画面を開きます。
スクリーンショット 2020-07-31 16.26.23.png
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)
}

ここでやっていることは以下の操作です。

  1. DBに接続
  2. リクエストボディからItem構造体を作る
  3. Item構造体から、DBにinsertするためのデータを作る
  4. insertを実行する
  5. レスポンスを作成

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形式で格納されています。

example-requestbody.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のときと同じなので割愛

ここでやっていることは以下の操作です。

  1. DBに接続(説明割愛)
  2. DBに問い合わせる検索条件を作る
  3. DBに問い合わせてデータを取得する
  4. レスポンスを作成(説明割愛)

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

参考:AWS公式ドキュメント AttributeValue

また、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のときと同じなので割愛

ここでやっていることは以下の操作です。

  1. DBに接続(説明割愛)
  2. リクエストボディからItem構造体を作る
  3. DBのデータをどう更新するかを指定する
  4. update実行
  5. レスポンスを作成(説明割愛)

2. リクエストボディからItem構造体を作る

Createのときとやり方は全く同じです。

今回は、パスパラメータで指定されたuseridレコードの、emailとnameを更新したくて以下のようなリクエストボディがきたと仮定します。

example-requestbody.json
{
    "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

KeyTableNameについては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のときと同じなので割愛

ここでやっていることは以下の操作です。

  1. DBに接続(説明割愛)
  2. deleteするデータを指定する
  3. delete実行
  4. レスポンスを作成(説明割愛)

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の構築が完了です。お疲れ様でした。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
18
Help us understand the problem. What is going on with this article?