Lambda ↔ DynamoDB

# はじめに

`Lambda` から `Ruby` で `aws-sdk` つかって `DynamoDB` な話です。何番煎じやねんと思いつつ、本日付のものは一つしかないだろうということで書きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32540/21ebdab8-8851-8c3c-2599-648311da9967.png)


# ゴール的なもの

- LambdaつかってサーバレスでクラウドネイティブなWebのバックエンドをブラウザ開発してみる
- 今回はとりあえず、`Lambda` から `Ruby` で `aws-sdk` つかって `DynamoDB` へというとこまでやる
- 最終的には、AWSのあれやこれやのサービスをつなぎ合わせてアプリ作る


# 開発の流れ

1. DynamoDBにテーブルつくる
2. IAMでDynamoにアクセスできるようなロール作る
3. ロールをLambdaに適用する
3. Cloud9上で、aws-sdkつかってDynamoDBを叩くコード書く
4. サーバへのデプロイとかテストとかもWebUIで簡単にできる（ショートカット覚えると快適）

以下、もうちょっと具体的にかいてみます[^1]

[^1]: [ここのページ](https://business.ntt-east.co.jp/content/cloudsolution/column-try-20.html)もステップ・バイ・ステップで手順がまとまっている

# DynamoDB

1. コンソールから `DynamoDB` > `テーブル` > `テーブルの作成` でテーブル名とプライマリーキーを決めてテーブル作る
2. `DynamoDB` > `テーブル` > `項目` でWebUI上でレコード足せる（今回は、`project-id` `name` `is-default` というカラムを作成）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32540/cb434aff-ea14-427a-f559-9b732d38bd2a.png)

考えるところはあんまりない

# IAM

1. `AmazonDynamoDBFullAccess` と `AWSLambdaDynamoDBExecutionRole` の二つのポリシーをロール（ ここでは `LambdaDynamoDB`という名前にした ）にアタッチする

# Lambda

1. `Lambda` > `関数` > `関数の作成` > `一から作成` で `ランタイム` は `Ruby2.7`にして関数を作る（ ここでは `project` という名前にした ）
2. `Lambda` > `関数` > `project` > `アクセス権限` で先に作っておいたロール `LambdaDynamoDB` を付与する
3. よし、関数にコード書き始めるよ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32540/5b934415-562c-7863-2d4c-81d821418a3d.png)

Webバックエンドになるようなコードだと、以下の `lambda_handler` にロジックを書くことになる。

```ruby:lambda_handler
def lambda_handler(event:, context:)
  # Your cool code is here
  { statusCode: 200, body: event }
end
```

ちなみに、`event` はクライアントから送ってくるデータが Hash で入っていて、`context` は関数のコンテキストが入っている。なにそれ？なときは `puts context.inspect` とかやって中身を見ましょう。初めのうちは気にしなくていいと思います。

```ruby:context
#<LambdaContext:0x00000000017d3830 @clock_diff=1607145893186, @deadline_ms=1607146470672, @aws_request_id="ea9e9b24-aec1-49ab-8d91-3d8eacc8c3e7", @invoked_function_arn="arn:aws:lambda:us-east-2:578170637269:function:qiita", @log_group_name="/aws/lambda/qiita", @log_stream_name="2020/12/05/[$LATEST]04dae62a2b914b20a6097187ac047ec6", @function_name="qiita", @memory_limit_in_mb="128", @function_version="$LATEST">
```

適当にコード書いたら `Save` > `Deploy` > `Test` > `Edit data` な 開発サイクルに入ります。

| Command   | Shortcut            |
| --------- | ------------------- |
| Save      | `⌘` + `s`           |
| Deploy    | `⌘` + `shift` + `u` |
| Test      | `⌘` + `i`           |
| Edit data | `⌘` + `j`           |

# コード

とりあえず、DynamoDB に対して CRUD するものを書いてみます

```json
{
  "project-id": 2,
  "name": "Running",
  "is-default": false
}
```

みたいなデータが送られてきた時に以下のように CRUD します

```ruby:CRUD
require 'json'
require 'aws-sdk-dynamodb'


def add_project(table, event)
  table.put_item({ item: event })  
end

def delete_project(table, event)
  params = { table_name: table, key: { 'project-id': event['project-id'] } }
  begin
    table.delete_item(params)
  rescue Aws::DynamoDB::Errors::ServiceError => error
    puts error.message
  end
end

def update_project(table, event)
  params = {
    table_name: table,
    key: { 'project-id': event['project-id'] },
    attribute_updates: {
      name: {
        value: body['name'],
        action: "PUT"
      }
    }
  }
  table.update_item(params)  
end

def list_project(table)
  scan_output = table.scan({ limit: 50, select: "ALL_ATTRIBUTES" })

  scan_output.items.each do |item|
    keys = item.keys

    keys.each do |k|
      puts "#{k}: #{item[k]}"
    end
  end
end

def lambda_handler(event:, context:)
  http_method = 'POST'
  
  dynamodb = Aws::DynamoDB::Resource.new(region: 'us-east-2')
  table = dynamodb.table('project')
  
  case http_method
    when 'GET'    then list_project(table)
    when 'PUT'    then update_project(table, event)
    when 'POST'   then add_project(table, event)
    when 'DELETE' then delete_project(table, event)
    else 0
  end

  { statusCode: 200, body: list_project(table) }
end
```

- `aws-sdk` で簡単に書くことができる
- `Aws::DynamoDB::Resource` vs. `Aws::DynamoDB::Client` についてはもう少し調べてみたい
- HTTPメソッドごとに処理を切り替える部分は（今後、接続するであろう）API Gateway から Lamda に `context.http_method` という形で HTTPメソッド（GET/PUT/POST/DELETE）が送られてくることを想定（ベストプラクティスはなんだろう？ CRUD 個々に関数ファイル分けるのが一般的？それだとAPI Gatewayの設定が増えてちょっとめんどくさそう）
- `attribute_updates` より `UpdateExpression` を使いなさいという記載も見かけるので、後で変更も考えたい（個人的には `attribute_updates` の方が読みやすく感じるが）

# STDOUT が見たい

コード書いていると、`p` や `puts` した STDOUT をみたくなる。手っ取り早い方法は、AWS CLI を使うことだと思う。Lambda のログは、デフォルトで `Cloud Watch` に飛ばされているようで、以前はみるのがややめんどくさかった[^2] が、今は以下で簡単に最新のログをみることができる

```
aws logs tail /aws/lambda/project
```

[^2]: `aws logs get-log-events --log-group-name /aws/lambda/project --log-stream-name '2020/12/01/[$LATEST]3cc66941ad844ba1afb5659ae677410b' --limit 100` みたいに ログ・グループとログ・ストリームの指定が必要だった。で、ログ・ストリームの値は関数実行ごとに毎回変わるのでめんどくさかった

# やってみた感じ

- アカウントつくってからコンソールログインして、Lambda で Hello World まで 5 分くらい
- Lambda は最初のリリースから 10 年くらいたってるみたいなんですが、かなりこなれてきている印象
- WebUI での開発もいい感じ。Cloud9 の買収・サーバレス開発環境への統合がワークしていると思う。エディタ部分をフルスクリーンにして、`vim` キーバインド、ショートカット覚えると集中できる
- `aws-sdk` つかって Dynamo へ簡単な CRUD 書くまでで1, 2時間って感じ。生産性はめっちゃいい

# 参考にしたサイト
- [AWS CLI V2でCloudWatch Logsのロググループをtailできるようになりました！]((https://dev.classmethod.jp/articles/tail-cloudwatch-logs-group/))
- [AWS API GatewayとLambdaでDynamoDB操作](https://business.ntt-east.co.jp/content/cloudsolution/column-try-20.html)

Cheers,

# シリーズ

- [Lambda ↔ DynamoDB](https://qiita.com/narutaro/items/b553a26cc84f195f165f)
- [API Gateway ↔ Lambda ↔ DynamoDB](https://qiita.com/narutaro/items/57cdd9f5ec8634112b09)
- [Cognito ↔ API Gateway ↔ Lambda ↔ Dynamo](https://qiita.com/narutaro/items/adaf160d944b91791b59)
- [Amplify [Hosting] ↔ API Gateway [REST] ↔ Lambda](https://qiita.com/narutaro/items/7aba191c12d917d36758)
- [API GatewayでCORSを有効にするためにやったこと](https://zenn.dev/masaino/articles/dd9ee709799af0)
- [AmplifyのJavascript SDKでCognito認証からデータ取得まで](https://zenn.dev/masaino/articles/9c4586fe1fc179)