ハッカソン参加時の備忘録 ～ 第４回：【Python】AWS LambdaとAWS DBを連携させてみよう ～

こんにちは。ITエンジニアのきゅうです。
今回作成するアプリのシステム構成図です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/c515cf6d-1566-722b-ddc5-3d98e44061f3.png)


# RDSを一旦断念します。代わりにDynamoDBを使っていきます。
まず最初に読者の皆様に謝らなければなりません。

**申し訳御座いません。**
**第３回目で、RDSへの接続をターゲットに記事を記載してまいりましたが、こちら一旦断念したいと思います。**

理由としては、AWSでRDS（MySQL等）を使うためにはどうしてもVPCを設定し、その中にRDSを置く必要がありました。
その為、ネットワークやセキュリティの設定が難しくなり、短時間で開発をしなければならないハッカソンには向かないと判断したためです。
(このVPCのせいで、この後RekognitionやBedrock等、AWSのサービスへの接続を予定しておりますが、接続が難しくなってしまい、一旦断念するという決断をしました。)


でも、ハッカソンでアプリを創る上では、情報を登録する機能というのは必要不可欠だと思います。
簡単に考えるだけでも、S3のようなストレージにExcelファイルを設けデータを保存、PythonであればPandasライブラリを使って、データを読み取ることも出来るでしょう。

ですが、せっかくAWSを使っているのですから、AWSのオススメを使いたい！！
以前参加したハッカソンで、テクニカルサポートとしてAWSジャパンの社員さんがいらっしゃっていた際に聞きました。
AWSでは『Amazon DynamoDB』の利用を薦めているようです。


## Amazon DynamoDBを選ぶメリット・デメリット
DynamoDBを選ぶメリットとデメリットを明記しておきますので、各々適切に選んでいきましょう。

### ＜前提＞
* あくまで本記事はハッカソンに参加した際参考となるよう、スピーディに実装できることを前提とします。
* また、ハッカソンでは開発期間が短いことが多い為、複雑なデータ管理はしないことが前提。

### ＜メリット＞
* DynamoDBはVPC内に設定しなくても良い。
そのため、VPCなどのネットワークやセキュリティで手間取ることがない為、時間の短縮に繋がる。
* VPCを設定しないことで、ほかのAWSのサービスもVPCによる複雑怪奇なことにならなくて済む。
* お手軽にAWSのコンソールからDBの作成や、テーブルの作成・編集、データの追加・削除・修正が出来る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/926fd07b-d6c7-28da-2d3c-d50f54932e37.png)
上記画像の赤枠のボタンよりデータの編集が出来ます。

### ＜デメリット＞
* RDBで可能だった複雑な検索やテーブル結合などは実行できない。
* 条件指定は基本的に、プライマリキー以外は使用できない
* 副問合せができない
* GROUP BYなどの集約関数が存在しない
* OR、NOTなどの論理演算子はなくANDのみ

⇒データを取得後、Python等でテーブル結合のような処理を実装する必要が御座います。
ですのでテーブル結合して複雑なデータを扱う業務アプリには向かないかもしれないです。
そもそも、DynamoDBはNoSQL型のDBですので、RDSの代替として考えるのではなく、あくまでデータを保存する別のDBと考えておくと良いかもしれません。


## Amazon DynamoDBの設定（所要時間：5分程度）
早速、DynamoDBを作っていきましょう。
```
使用するサービス：DynamoDB
使用する機能：テーブル
```
### 『テーブルの作成』を押下します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/d7f69f13-495f-3ce6-426b-0a8c65d24251.png)

### テーブルの作成をします
テーブルの設定画面にて、キャプチャーのように設定していきます。
ただ、DynamoDBはAmazonオリジナルのDBということもあり、分からない言葉が多発しておりましたので、以下のサイトが大変参考になりました。
設定前にお読みいただくと理解がはかどります。

https://qiita.com/shibataka000/items/e3f3792201d6fcc397fd

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/0b91d357-eaa6-2c9a-b070-04735893dc38.png)

## ロールの設定（所要時間：5分程度）
```
使用するサービス：IAM
使用する機能：ロール
```
### ロールの設定を行います。
IAMからLambdaがDynamoDBを操作できるように、ロールを付与します。
『ロールを作成』ボタンを押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/5d0b8cee-473f-22d1-4948-9ecbf9504d82.png)

### ユースケースを設定します。
今回はLambdaから呼び出しますので、ユースケースは『Lambda』を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/4f3e4d23-8f18-c782-e0b4-bef4556e0c4b.png)

### 許可ポリシーを設定します。
ここではまず「Dynamo」と検索し、表示された選択肢の内、
「AmazonDynamoDBFullAccess」と「AmazonDynamoDBExecutionRoll」を設定します。
許可ポリシーって苦手な方もいいると思いますので、基本、ハッカソンでは「～FullAccess」と「～ExectionRoll」の２つをつけておけば動くかと思います。
※ハッカソン以外で設定する場合はFullAccessなんてつけてはダメですよ！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/eeb7c9b5-d171-8082-c142-7887cb8d48e0.png)

### ロールに名前を付けます。
ロール名を付け、一番下の「ロールを作成」を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/9cf1106b-63fe-9a2f-28ed-38bc5e1d3f55.png)


## Lambda関数
```
使用するサービス：Lambda
使用する機能：関数
```
### 関数の作成を押下します。
上の検索ウィンドウから「Lambda」と入力し、Lambdaサービスを開きます。
その後『関数』を選択し、キャプチャーのような画面に遷移したら、「関数の作成」を押下します・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/5c2377e8-886b-068d-1236-076da242df9c.png)

### 関数の設定を行います。
関数を設定する上で一番大事なのは、先ほどのロールを設定することです。
これを行うことで、DynamoDBとの接続周りの事前設定は済みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/233a897f-20e5-ec6b-229a-17ceff7f5082.png)


### LINEアプリの設定をします。
続いて、以下の記事で投稿しているLINE側の設定と、Lambdaのプログラミングを行っていきます。

https://qiita.com/kyuu2197/items/68fcadc6ab6ac57cf8ea

:::note alert
上記記事の ***『AWSのLambda関数を作成する。』*** の内、
* 「１．AWSのLambdaにアクセスし、「関数作成」ボタンを押下」
* 「２．関数の初期設定をします。」
* 「３．関数の作成に成功。」
については、先ほど行いましたので、実施不要になります。
:::

この設定が終われば、まずはLINEへの接続が完了したかと思います。
文言は異なりますが、こんな感じに文字列が返ってくるかと。。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3618971/1fdc7edb-c50d-01a4-5277-9269378a149a.png)

## ソースコードをDB接続用に置換
今のソースはLINEへのオーム返しのソースとなりますので、DynamoDBへの接続用のソースに置き換えます。
```diff_python
- import logging
- import os
- import urllib.request
- import json
- 
- logger = logging.getLogger()
- logger.setLevel(logging.INFO)
- 
- def lambda_handler(event, context):
-     
-     logger.info(event)
-     
-     print("１．")
-     print(event)
-     for message_event in json.loads(event["body"])["events"]:
-         print("２．")
-         
-         logger.info(json.dumps(message_event))
-         
-         url = "https://api.line.me/v2/bot/message/reply"
-         print("３．")
-         headers = {
-             "Content-Type": "application/json",
-             'Authorization': 'Bearer ' + os.environ['ACCESSTOKEN']
-         }
-         print("３．")
- 
-         data = {
-             "replyToken": message_event["replyToken"],
-             "messages": [
-                 {
-                     "type": "text",
-                     "text": message_event["message"]["text"],
-                 }
-             ]
-         }
-         
-         print("４．")
-         req = urllib.request.Request(url=url, data=json.dumps(data).encode("utf-8"), method="POST", headers=headers)
-         
-         print("５．")
-         with urllib.request.urlopen(req) as res:
-             
-             logger.info(res.read().decode("utf-8"))
-             print("６．")
-             return {
-                 "statusCode": 200,
-                 "body": json.dumps("Hello from Lambda!")
-             }
-     
-     return {
-         "statusCode": 200,
-         "body": json.dumps("Hello from Lambda!")
-     }

+ import json
+ import boto3
+ import logging
+ import os
+ import urllib.request
+ 
+ logger = logging.getLogger()
+ logger.setLevel(logging.INFO)
+ 
+ from boto3.dynamodb.conditions import Key	#Keyオブジェクトを利用できるようにする
+ 
+ dynamodb = boto3.resource('dynamodb')	    #Dynamodbアクセスのためのオブジェクト取得
+ table = dynamodb.Table("test-dynamodb")	    #指定テーブルのアクセスオブジェクト取得
+ 
+ 
+ # テーブルスキャン
+ def operation_scan():
+     scanData = table.scan()	    #scan()メソッドでテーブル内をscan一覧を取得
+     items=scanData['Items']	    #応答からレコード一覧を抽出
+     print(items)	            #レコード一覧を表示
+     return scanData
+ 
+ # レコード検索
+ def operation_query(partitionKey, sortKey):
+     queryData = table.query(	    #query()メソッドでテーブル内を検索
+         KeyConditionExpression = Key("parId").eq(partitionKey) & Key("sortKey").eq(sortKey)	    #検索キー(parIdとsortKey)を設定
+     )
+     items=queryData['Items']	    #応答から取得レコードを抽出
+     print("３．５")
+     print(items)	            #取得レコードを表示
+     return items
+ 
+ # レコード追加・更新
+ def operation_put(partitionKey, sortKey, value, unit):
+     putResponse = table.put_item(	#put_item()メソッドで追加・更新レコードを設定
+         Item={	                    #追加・更新対象レコードのカラムリストを設定
+             'parId': partitionKey,
+             'sortKey': sortKey,
+             'value': value,
+             'unit': unit
+         }
+     )
+     if putResponse['ResponseMetadata']['HTTPStatusCode'] != 200:	#HTTPステータスコードが200 OKでないか判定
+         print(putResponse)	#エラーレスポンスを表示
+     else:
+         print('PUT Successed.')
+     return putResponse
+ 
+ # レコード削除
+ def operation_delete(partitionKey, sortKey):
+     delResponse = table.delete_item(	#delete()メソッドで指定テーブルを削除
+        key={	                        #Keyオブジェクトで削除対象レコードのキー設定
+            'parId': partitionKey,
+            'sortKey': sortKey
+        }
+     )
+     if delResponse['ResponseMetadata']['HTTPStatusCode'] != 200:	#HTTPステータスコードが200 OKでないか判定
+         print(delResponse)	                                        #エラーレスポンスを表示
+     else:
+         print('DEL Successed.')
+     return delResponse
+ 
+ def lambda_handler(event, context):	                        #Lambdaから最初に呼びされるハンドラ関数
+     logger.info("Received event: " + json.dumps(event))	    #引数：eventの内容を表示
+     print("Received event: " + json.dumps(event))	        #引数：eventの内容を表示
+     
+     
+     # LINEのリクエストからeventを取得する
+     for message_event in json.loads(event["body"])["events"]:
+         logger.info(json.dumps(message_event))
+         
+         url = "https://api.line.me/v2/bot/message/reply"
+         print("３．")
+         headers = {
+             "Content-Type": "application/json",
+             'Authorization': 'Bearer ' + os.environ['ACCESSTOKEN']
+         }
+         print("３．")
+         
+         ########################################
+         ### DynamoDBへの接続
+         ########################################
+         try:
+             item = operation_query("test001", 0)    ##今回は強制的にquery（検索）処理を実施
+             print("item")
+             print(item)             ## [{'sortKey': Decimal('0'), 'col1': 'aaaaa', 'parId': 'test001'}]
+             item1 = item[0]
+             print("item1")          ## {'sortKey': Decimal('0'), 'col1': 'aaaaa', 'parId': 'test001'}
+             print(item1)
+             item2 = item1['col1']   ## aaaaa
+             print("item2")
+             print(item2)
+             dataItem = item2   
+             
+         except Exception as e:
+             print("Error Exception.")
+             print(e)
+         ########################################
+         
+         data = {
+             "replyToken": message_event["replyToken"],
+             "messages": [
+                 {
+                     "type": "text",
+                     "text": dataItem,
+                 }
+             ]
+         }
+         
+         print("４．")
+         req = urllib.request.Request(url=url, data=json.dumps(data).encode("utf-8"), method="POST", headers=headers)
+         
+         print("５．")
+         with urllib.request.urlopen(req) as res:
+             
+             logger.info(res.read().decode("utf-8"))
+             print("６．")
+             return {
+                 "statusCode": 200,
+                 "body": json.dumps("Hello from Lambda!")
+             }
+     
+     return {
+         "statusCode": 200,
+         "body": json.dumps("Hello from Lambda!")
+     }
+     
+ 
```
↓↓↓　コピー用　↓↓↓
```python
import json
import boto3
import logging
import os
import urllib.request

logger = logging.getLogger()
logger.setLevel(logging.INFO)

from boto3.dynamodb.conditions import Key	#Keyオブジェクトを利用できるようにする

dynamodb = boto3.resource('dynamodb')	    #Dynamodbアクセスのためのオブジェクト取得
table = dynamodb.Table("test-dynamodb")	    #指定テーブルのアクセスオブジェクト取得


# テーブルスキャン
def operation_scan():
    scanData = table.scan()	    #scan()メソッドでテーブル内をscan一覧を取得
    items=scanData['Items']	    #応答からレコード一覧を抽出
    print(items)	            #レコード一覧を表示
    return scanData

# レコード検索
def operation_query(partitionKey, sortKey):
    queryData = table.query(	    #query()メソッドでテーブル内を検索
        KeyConditionExpression = Key("parId").eq(partitionKey) & Key("sortKey").eq(sortKey)	    #検索キー(parIdとsortKey)を設定
    )
    items=queryData['Items']	    #応答から取得レコードを抽出
    print("３．５")
    print(items)	            #取得レコードを表示
    return items

# レコード追加・更新
def operation_put(partitionKey, sortKey, value, unit):
    putResponse = table.put_item(	#put_item()メソッドで追加・更新レコードを設定
        Item={	                    #追加・更新対象レコードのカラムリストを設定
            'parId': partitionKey,
            'sortKey': sortKey,
            'value': value,
            'unit': unit
        }
    )
    if putResponse['ResponseMetadata']['HTTPStatusCode'] != 200:	#HTTPステータスコードが200 OKでないか判定
        print(putResponse)	#エラーレスポンスを表示
    else:
        print('PUT Successed.')
    return putResponse

# レコード削除
def operation_delete(partitionKey, sortKey):
    delResponse = table.delete_item(	#delete()メソッドで指定テーブルを削除
       key={	                        #Keyオブジェクトで削除対象レコードのキー設定
           'parId': partitionKey,
           'sortKey': sortKey
       }
    )
    if delResponse['ResponseMetadata']['HTTPStatusCode'] != 200:	#HTTPステータスコードが200 OKでないか判定
        print(delResponse)	                                        #エラーレスポンスを表示
    else:
        print('DEL Successed.')
    return delResponse

def lambda_handler(event, context):	                        #Lambdaから最初に呼びされるハンドラ関数
    logger.info("Received event: " + json.dumps(event))	    #引数：eventの内容を表示
    print("Received event: " + json.dumps(event))	        #引数：eventの内容を表示
    
    
    # LINEのリクエストからeventを取得する
    for message_event in json.loads(event["body"])["events"]:
        logger.info(json.dumps(message_event))
        
        url = "https://api.line.me/v2/bot/message/reply"
        print("３．")
        headers = {
            "Content-Type": "application/json",
            'Authorization': 'Bearer ' + os.environ['ACCESSTOKEN']
        }
        print("３．")
        
        ########################################
        ### DynamoDBへの接続
        ########################################
        try:
            item = operation_query("test001", 0)    ##今回は強制的にquery（検索）処理を実施
            print("item")
            print(item)             ## [{'sortKey': Decimal('0'), 'col1': 'aaaaa', 'parId': 'test001'}]
            item1 = item[0]
            print("item1")          ## {'sortKey': Decimal('0'), 'col1': 'aaaaa', 'parId': 'test001'}
            print(item1)
            item2 = item1['col1']   ## aaaaa
            print("item2")
            print(item2)
            dataItem = item2   
            
        except Exception as e:
            print("Error Exception.")
            print(e)
        ########################################
        
        data = {
            "replyToken": message_event["replyToken"],
            "messages": [
                {
                    "type": "text",
                    "text": dataItem,
                }
            ]
        }
        
        print("４．")
        req = urllib.request.Request(url=url, data=json.dumps(data).encode("utf-8"), method="POST", headers=headers)
        
        print("５．")
        with urllib.request.urlopen(req) as res:
            
            logger.info(res.read().decode("utf-8"))
            print("６．")
            return {
                "statusCode": 200,
                "body": json.dumps("Hello from Lambda!")
            }
    
    return {
        "statusCode": 200,
        "body": json.dumps("Hello from Lambda!")
    }
```

如何でしたでしょうか？
前回までのRDSを使ってのアプリ作成だと、2時間くらいかかりますが、
これであれば１時間以内で、LINEとLambdaの接続⇒LambdaからDB（DynamoDB）の接続が出来ると思います。
ハッカソンでお試しください。

次回は、AWSの基本機能であり、AWSのファイルサーバ機能のS3の連結方法をご紹介します。

ということで、本日はここまでです。
少しでも参考になったと思いましたら、「いいね」や「ストック」を押していただけますと、今後の励みになりますので、是非とも宜しくお願い致します。

最後までお読み頂きありがとうございました。