2
1

More than 1 year has passed since last update.

俺みたいになるな!Lambda+Node.jsでDynamoDBにputItemするだけで半日溶かしたAWS SAP

Last updated at Posted at 2022-09-19

やりたいこと

ただただシンプルにLambdaでDynamoDBに書き込みたい。それだけなんです。初体験でも無い。
なのにハマって、シルバーウィークの初日の半日それで溶かしてしまいました。
AWS SAPの称号を剥奪されるかと思って誰にも相談できませんでした。

ちなみに、なんでそんなことしたいかというとコレです↓ :frog:

実装!

DyanmoDBの設計

完了したタスク(task)を、ユーザー(user)、日付(date)ごとに登録したいです。
で、取り出す時はあるuserとdateのtaskを全て取得したい。

試行錯誤の末、保存したいアイテムはuser, unixtime, date, taskにしました。

ちなみに、user, dateはDynamoDBの予約語。予約語だけど使えちゃう。queryする時にちょい厄介だけど使えちゃう。

DynamoDBをこんな感じで用意します。

table_name: done_tasks

user:     String, パーティションキー 
unixtime: Number, ソートキー
date:     String, ローカルセカンダリインデックス
task:     String

本当はunixtimeは要らないのですが、今回使うputItemメソッドは、プライマリキー(パーティションキー+ソートキー)が一緒だと上書きしてしまうので、userとdateが同じだと複数のtaskがDBに登録できなくなってしまいます。それでunixtimeをソートキーに採用しました。

ちなみに、RDBに慣れてると違和感あるかもですが、DynamoDBを新規作成する時に、taskの分の属性(カラム)は事前に作ってなくても大丈夫です。putItemする時にない属性は自動で作られます。(てか厳密に言うと、属性(attribute)って呼んでるぽいのでカラムではないのでしょう。)

Lambdaの実行ロールにDynamoDBを触る権限を付与する

以前入社研修で作った社内IoTシステムには、AmazonDynamoDBFullAccessなんていう暴力的なポリシーを取り敢えず付けてしまっていたが、今回はプロらしく真面目にやる。

Lambda→設定→実行ロールからこのLambdaの実行ロールに飛んで以下のポリシーを作成してアタッチ。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "dynamodb:PutItem",
                "dynamodb:Scan",
                "dynamodb:Query"
            ],
            "Resource": "arn:aws:dynamodb:<region>:<account_id>:table/<table_name>"
        }
    ]
}

DynamoDBにputItemする

このドキュメントを元に書く。

index.js
const AWS = require('aws-sdk');

// これLambdaでは別にいらないぽい。書いても怒られないけど、やらなくてもsetされてる。
AWS.config.update({region: 'ap-northeast-1'});

// クライアントを生成する
// {apiVersion: '2012-08-10'}もLambdaでは別にいらないぽい。書いても怒られないけど、やらなくてもsetされてる。
const ddb = new AWS.DynamoDB({apiVersion: '2012-08-10'});

const user = "kakudaisuke";
const unixtime = 1663160618.493949;
const date = "2022/1/1";
const task = "仕様を固める";

// Lambdaの本ちゃんの処理始まる
exports.handler = async (event) => {
/*
  ごにょごにょ処理して...
  さらにごにょごにょ処理して...*/

  let params = {
    TableName: 'done_tasks',
    Item: {
      'user': { S: user },
      'unixtime': { N: unixtime },
      'date': { s: date },
      'task': { S: task }
    }
  };

  // awaitと.promise()がめっちゃ大事。
  const res = await ddb.putItem(params, function(err, data) {
    if (err) {
      console.log("Error", err);
    } else {
      console.log("Success", data);
    }
  }).promise();

  console.log("putItem終わったよ");

  return;

CloudWatch Logsを見ても、console.log("Error", err);が何も出力されていませんでした。でもputItemの後のputItem終わったよのログは出力されている。
今までのAWS実装では、何度かロール、ポリシーで適切な権限を与えていないという原因で、実装をミスっていたことがありますが、今回はちゃんとしてる。ってめっちゃくちゃハマってました。DynamoDB ドキュメントクライアントにして、メソッドをputにみても結果同じ。

なぜだ、なぜなんだ!
半日くらい溶けました。
というところで、過去につよつよ先輩エンジニアと一日会初合宿で実装したコードを眺めてたらヒントを得ました。

上のコードにあるように、putItemメソッドにawaitpromise()を入れてみたら成功しました!
というわけで上のコードは合ってるコードです。(実際のコードから簡略化して書いてるので、そのままコピペしても動かないかもです :pray:

つまり、非同期処理でputItemが時間かかってる間にreturn;してしまっているもんだから、DynamoDBに書き込もうとしている間にLambdaの処理が終わってしまったのだと思います。たぶん。だからputItemの中のログすら出力されていなかったんだと思います。たぶん。

最後に

ドキュメントクライアントってのもあって、Lambdaの最初用意されてたコードにもそちらで書かれてましたが、それだと型の指定とかしないで同じ処理を簡潔に書けるそうです。

ツッコミとかもっといい方法とかアドバイスとかあれば、ぜひコメントください:bow:

2
1
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
2
1