Lambdaを使用したRDSの操作

## はじめに

AWS上でRDSでのDB操作をする場合、踏み台サーバ（EC2）等からRDSに接続し操作をするのが一般的かと思います。
ただし、この場合サーバの管理などが必要になるため不要なコストが発生する可能性があります。
Lambdaを使用してDB操作をする場合はサーバの管理が不要なため、簡単なデータ参照や登録、更新、削除などを行いたいときはLambdaを使用するのも一つの選択肢になるかと思います。
本記事ではLambdaからRDSの操作を行う際の各種設定手順とエラーポイントについてまとめていきます。

## 前提条件

・コンソール操作で各種リソースを作成
・データベースの作成における、エンジンのタイプなど一部の設定値に関しては要件に応じて読み替えてください。

## RDSの作成

①VPCの作成
・作成するリソースで「**VPCなど**」を選択
・VPCエンドポイントで「**なし**」を選択
・その他の設定はデフォルト

②DBサブネットグループの作成
・①で作成したVPCを選択
・①で作成したVPCのサブネットが存在するAZを2つとも選択
・①で作成したVPCのプライベートサブネットを2つとも選択

③データベースの作成

・作成方法で「**標準作成**」を選択
・エンジンのタイプは「**MySQL**」を選択
・テンプレートは「**無料利用枠**」を選択
・認証情報管理は「**セルフマネージド**」を選択
・①で作成したVPCを選択
・②で作成したDBサブネットグループを選択
・その他の設定はデフォルト

**※セキュリティグループは一旦、「default」のまま**

## Lambdaの作成

①Lambdaの作成
・「**一から作成**」を選択
・ランタイムは「**python3.11**」を選択
・その他の設定はデフォルト

②Layerの作成
・Lambdaで選択したランタイムと同様の環境を準備（EC2等）
・対象ライブラリをディレクトリ指定でインストール
・インストールしたディレクトリをZip圧縮
・ZipファイルをLayerとして追加
・Lambda関数にLayerを追加

③IAMロールの修正
・「**AWSLambdaVPCAccessExecutionRole**」の権限を追加
・RDSに対する権限を追加（Lambdaの用途次第で変動）

## RDS接続設定

①Lambda関数用のセキュリティグループの作成
・RDSが配置されているVPCを選択
・その他の設定はデフォルト

②RDS用のセキュリティグループの作成
・RDSが配置されているVPCを選択
・インバウンドルールを①のセキュリティグループからポート番号3306で設定
・アウトバウンドルールはデフォルトのものを削除
・RDSで使用しているセキュリティグループを変更

③Lambda関数用のセキュリティグループの修正
・アウトバウンドルールを②のセキュリティグループへポート番号3306で設定

④Lambda関数をRDSが配置されているVPCに接続
・VPCはRDSが配置されているVPCを選択
・サブネットはDBサブネットグループで選択したサブネットを選択
・セキュリティグループは①で作成したセキュリティグループを選択

<font color="RED">※適切な権限がLambda関数に付与されていない場合、以下のメッセージが出力されVPCに接続できない</font>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637963/4ccbadf7-1d3e-8ac5-67d6-fcb95ceb6093.png)

## 動作確認
```text:実行スクリプト
import json
import os
import pymysql
import uuid
    
def generate_data(num_records=100):
    data = []
    for i in range(num_records):
        datum =[
            str(uuid.uuid4()), 
            i
            ]
        data.append(datum)

    return data

def connect_to_rds():
    conn = pymysql.connect(
        host=os.environ['DB_HOST'],   # RDSのエンドポイント
        user=os.environ['DB_USER'],   # ユーザ名
        passwd=os.environ['DB_PASSWORD'],   # パスワード
    )
    
    return conn    

def create_db(conn):
    with conn.cursor() as cur:
        sql = "CREATE DATABASE IF NOT EXISTS TEST_DB;"
        cur.execute(sql)
        print(cur.fetchall())

def show_db(conn):
    with conn.cursor() as cur:
        sql = "SHOW DATABASES;"
        cur.execute(sql)
        print(cur.fetchall())

def use_db(conn):
    with conn.cursor() as cur:
        sql = "USE TEST_DB;"
        cur.execute(sql)

def create_table(conn):
    with conn.cursor() as cur:
        sql = """
CREATE TABLE IF NOT EXISTS TEST_DATA (
    id VARCHAR(255) NOT NULL,
    no INT NOT NULL,
    PRIMARY KEY (id)
);"""
        cur.execute(sql)
    conn.commit()

def show_tables(conn):
    with conn.cursor() as cur:
        sql = "SHOW TABLES;"
        cur.execute(sql)
        print(cur.fetchall())

def insert_data(conn, data):
    with conn.cursor() as cur:
        for d in data:
            sql = "INSERT INTO TEST_DATA (id, no) VALUES (%s, %s);"
            cur.execute(sql, d)
    conn.commit()

def select_data(conn):
    with conn.cursor() as cur:
        sql = "SELECT * FROM TEST_DATA LIMIT 10;"
        cur.execute(sql)
        print(cur.fetchall())
    
    conn.commit()

def lambda_handler(event, context):
    try:
        data = generate_data(100)
        
        conn = connect_to_rds()
        
        create_db(conn)
        
        show_db(conn)

        use_db(conn)
        
        create_table(conn)
        
        show_tables(conn)
 
        insert_data(conn, data)

        select_data(conn)

        conn.close()
        
        return {
            'statusCode': 200,
            'body': json.dumps(f'Successfully inserted {len(data)} records into RDS')
        }
    except Exception as e:
        print(f"Error: {str(e)}") 
        return {
            'statusCode': 500,
            'body': json.dumps(f'Error: {str(e)}')
        }
```

<font color="RED">※RDSへの接続情報を環境変数で設定していますが、AWS Secrets Manager を使用する方が良いかと思います。</font>

```text:実行結果
START RequestId: 7eb816c7-6c70-4074-b823-5e9a31c4f050 Version: $LATEST
[]
(('TEST_DB',), ('information_schema',), ('mysql',), ('performance_schema',), ('sys',))
(('TEST_DATA',),)
(('046d008e-1f5e-42ad-a3bb-8dd4fe566f6c', 50), ('04a56755-0c59-4378-96d2-28852fa09d79', 2), ('064d3a56-1a64-46aa-b4a3-4236213fb610', 71), ('065e4fd4-fab1-4726-8f5d-9ed2e30be4b6', 51), ('0742f655-9c21-46f7-b401-08ef252af0ac', 63), ('090a7895-0238-4bfe-a520-9746f40e69a7', 7), ('09121553-112a-4c7e-afc0-a6a65c9d51ef', 12), ('0bedb22f-7bc0-4389-b26f-c7767a10953a', 61), ('0e5876ac-219d-4086-9d05-4643e7ed7cdd', 17), ('1225eb24-fa07-45fd-b30d-31a48ad5f8d8', 27))
END RequestId: 7eb816c7-6c70-4074-b823-5e9a31c4f050
REPORT RequestId: 7eb816c7-6c70-4074-b823-5e9a31c4f050	Duration: 68.70 ms	Billed Duration: 69 ms	Memory Size: 128 MB	Max Memory Used: 44 MB	Init Duration: 147.33 ms	
```

## エラーポイント
①RDSのセキュリティグループ不備
・設定済みのインバウンドルールを削除

```text:実行結果
{
  "errorMessage": "2024-11-12T06:36:16.308Z 6fa5f3bb-1f05-4612-87ba-96978ddc78d4 Task timed out after 3.01 seconds"
}
```

②Lambdaのセキュリティグループ不備
・設定済みのアウトバウンドルールを削除

```text:実行結果
{
  "errorMessage": "2024-11-12T06:42:12.755Z fe8062d7-b6ff-4700-963a-7516f829b445 Task timed out after 3.01 seconds"
}
```

③接続情報の不備
・DBホスト名の誤り

```text:実行結果
{
  "statusCode": 500,
  "body": "\"Error: (2003, \\\"Can't connect to MySQL server on '*****' ([Errno -2] Name or service not known)\\\")\""
}
```

・ユーザ名の誤り

```text:実行結果
{
  "statusCode": 500,
  "body": "\"Error: 'cryptography' package is required for sha256_password or caching_sha2_password auth methods\""
}
```

・パスワードの誤り

```text:実行結果
{
  "statusCode": 500,
  "body": "\"Error: (1045, \\\"Access denied for user '*****'@'*****' (using password: YES)\\\")\""
}
```

## まとめ
Lambdaを使用したRDSの操作方法を実施してみました。
DBの異なる場合やLambdaでのDB操作が異なる場合でも、本記事で紹介した設定内容と変わるものではないかと思いますので参考になればと思います。