

本文為英文版的機器翻譯版本，如內容有任何歧義或不一致之處，概以英文版為準。

# AWS Lambda 搭配 Amazon RDS 使用
<a name="services-rds"></a>

您可以直接或透過 Amazon RDS Proxy 將 Lambda 函數連線到 Amazon Relational Database Service (Amazon RDS)。直接連線適用於簡單的案例，生產環境則建議使用代理。資料庫代理管理許多共用資料庫連線，讓函數在不耗盡資料庫連線的情況下達到高並行層級。

我們建議將 Amazon RDS Proxy 用於 Lambda 函數，這些函數會頻繁進行短資料庫連線，或是開啟和關閉大量資料庫連線。如需詳細資訊，請參閱《Amazon Relational Database Service 開發人員指南》中的 [Automatically connecting a Lambda function and a DB instance](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/lambda-rds-connect.html) 一節。

**提示**  
若要將 Lambda 函式快速連線至 Amazon RDS 資料庫，您可以使用主控台內的引導精靈。若要開啟精靈，請執行下列操作：  
開啟 Lambda 主控台中的[函數頁面](https://console.aws.amazon.com/lambda/home#/functions)。
選取您要連接資料庫的函數。
在**組態**索引標籤，選取 **RDS 資料庫**。
選擇**連接至 RDS 資料庫**。
將函數連接至資料庫之後，您可以選擇**新增代理**來建立代理。

## 設定函數以使用 RDS 資源
<a name="rds-configuration"></a>

在 Lambda 主控台中，您可以佈建和設定 Amazon RDS 資料庫執行個體和代理資源。您可以在**組態**索引標籤下導覽至 **RDS 資料庫**來執行此操作。或者，您也可以在 Amazon RDS 主控台中建立與設定 Lambda 函數的連線。設定 RDS 資料庫執行個體以與 Lambda 搭配使用時，請注意以下條件：
+ 若要連線到資料庫，您的函數必須位於資料庫執行所在的相同 Amazon VPC 內。
+ 您可以搭配 MySQL、MariaDB、PostgreSQL 或 Microsoft SQL Server 引擎，使用 Amazon RDS 資料庫。
+ 您也可以搭配 MySQL 或 PostgreSQL 引擎，使用 Aurora DB 叢集。
+ 您需要提供 Secrets Manager 秘密以用於資料庫身分驗證。
+ IAM 角色必須提供使用秘密的許可，而受信任的政策必須允許 Amazon RDS 擔任該角色。
+  使用主控台設定 Amazon RDS 資源，並將其連線至函數的 IAM 主體必須具有下列許可：

### 許可政策範例
<a name="rds-lambda-permissions"></a>

**注意**  
 只有在您設定 Amazon RDS Proxy 來管理資料庫連線集區時，才需要這些 Amazon RDS Proxy 許可。

------
#### [ JSON ]

****  

```
{
  "Version":"2012-10-17",		 	 	 
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:CreateSecurityGroup",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSubnets",
        "ec2:DescribeVpcs",
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:AuthorizeSecurityGroupEgress",
        "ec2:RevokeSecurityGroupEgress",
        "ec2:CreateNetworkInterface",
        "ec2:DeleteNetworkInterface",
        "ec2:DescribeNetworkInterfaces"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "rds-db:connect",
        "rds:CreateDBProxy",
        "rds:CreateDBInstance",
        "rds:CreateDBSubnetGroup",
        "rds:DescribeDBClusters",
        "rds:DescribeDBInstances",
        "rds:DescribeDBSubnetGroups",
        "rds:DescribeDBProxies",
        "rds:DescribeDBProxyTargets",
        "rds:DescribeDBProxyTargetGroups",
        "rds:RegisterDBProxyTargets",
        "rds:ModifyDBInstance",
        "rds:ModifyDBProxy"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "lambda:CreateFunction",
        "lambda:ListFunctions",
        "lambda:UpdateFunctionConfiguration"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "iam:AttachRolePolicy",
        "iam:CreateRole",
        "iam:CreatePolicy"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetResourcePolicy",
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret",
        "secretsmanager:ListSecretVersionIds",
        "secretsmanager:CreateSecret"
      ],
      "Resource": "*"
    }
  ]
}
```

------

Amazon RDS 會按資料庫執行個體大小收取代理程式的小時費率，請參閱 [RDS 代理定價](https://aws.amazon.com/rds/proxy/pricing/)以了解詳細資訊。如需代理連線的一般詳細資訊，請參閱《Amazon RDS 使用者指南》中的[使用 Amazon RDS Proxy](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html)。

### Amazon RDS 連線的 SSL/TLS 要求
<a name="rds-lambda-certificates"></a>

若要對 Amazon RDS 資料庫執行個體進行安全的 SSL/TLS 連線，您的 Lambda 函式必須使用信任的憑證來驗證資料庫伺服器的身分。Lambda 會根據您的部署套件類型，採用不同方式處理這些憑證：
+ [.zip 壓縮檔](configuration-function-zip.md)：憑證處理方式會因執行時期而異：
  + **Node.js 18 及更早版本**：Lambda 會自動包含 CA 憑證與 RDS 憑證。
  + **Node.js 20 及更新版本**：Lambda 預設不再載入額外的 CA 憑證。將 `NODE_EXTRA_CA_CERTS` 環境變數設為 `/var/runtime/ca-cert.pem`。

   AWS 區域 將新的 Amazon RDS 憑證新增至 Lambda 受管執行期最多可能需要 4 週的時間。
+ [容器映像](images-create.md)： AWS 基礎映像僅包含 CA 憑證。如果您的函式需連線至 Amazon RDS 資料庫執行個體，必須在容器映像中包含對應的憑證。在您的 Dockerfile 中，下載[與您託管資料庫 AWS 區域 的 對應的憑證套件。](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesDownload)範例：

  ```
  RUN curl https://truststore.pki.rds.amazonaws.com/us-east-1/us-east-1-bundle.pem -o /us-east-1-bundle.pem
  ```

此命令會下載 Amazon RDS 憑證套件，並將其儲存在容器根目錄中的絕對路徑 `/us-east-1-bundle.pem`。在函式程式碼中設定資料庫連線時，必須參考此確切路徑。範例：

------
#### [ Node.js ]

由於 Node.js 資料庫用戶端需要記憶體中的實際憑證內容，而不只是憑證檔案的路徑，因此 `readFileSync` 函式為必要項目。如果沒有 `readFileSync` 函式，用戶端會將路徑字串解譯為憑證內容，導致「憑證鏈中存在自我簽署憑證」錯誤。

**Example OCI 函式的 Node.js 連線組態**  

```
import { readFileSync } from 'fs';

// ...

let connectionConfig = {
    host: process.env.ProxyHostName,
    user: process.env.DBUserName,
    password: token,
    database: process.env.DBName,
    ssl: {
        ca: readFileSync('/us-east-1-bundle.pem') // Load RDS certificate content from file into memory
    }
};
```

------
#### [ Python ]

**Example OCI 函式的 Python 連線組態**  

```
connection = pymysql.connect(
    host=proxy_host_name,
    user=db_username,
    password=token,
    db=db_name,
    port=port,
    ssl={'ca': '/us-east-1-bundle.pem'}  #Path to the certificate in container
)
```

------
#### [ Java ]

對於使用 JDBC 連線的 Java 函式，連線字串必須包含：
+ `useSSL=true`
+ `requireSSL=true`
+ 指向容器映像中 Amazon RDS 憑證位置的 `sslCA` 參數

**Example OCI 函式的 Java 連線字串**  

```
// Define connection string
String connectionString = String.format("jdbc:mysql://%s:%s/%s?useSSL=true&requireSSL=true&sslCA=/us-east-1-bundle.pem", // Path to the certificate in container
        System.getenv("ProxyHostName"),
        System.getenv("Port"),
        System.getenv("DBName"));
```

------
#### [ .NET ]

**Example OCI 函式中 MySQL 連線的 .NET 連線字串**  

```
/// Build the Connection String with the Token 
string connectionString = $"Server={Environment.GetEnvironmentVariable("RDS_ENDPOINT")};" +
                         $"Port={Environment.GetEnvironmentVariable("RDS_PORT")};" +
                         $"Uid={Environment.GetEnvironmentVariable("RDS_USERNAME")};" +
                         $"Pwd={authToken};" +
                         "SslMode=Required;" +
                         "SslCa=/us-east-1-bundle.pem";  // Path to the certificate in container
```

------
#### [ Go ]

對於使用 MySQL 連線的 Go 函式，請將 Amazon RDS 憑證載入憑證集區，並向 MySQL 驅動程式註冊該憑證。連線字串隨後必須透過 `tls` 參數引用此組態。

**Example OCI 函式中 MySQL 連線的 Go 程式碼**  

```
import (
    "crypto/tls"
    "crypto/x509"
    "os"
    "github.com/go-sql-driver/mysql"
)

...

// Create certificate pool and register TLS config
rootCertPool := x509.NewCertPool()
pem, err := os.ReadFile("/us-east-1-bundle.pem")  // Path to the certificate in container
if err != nil {
    panic("failed to read certificate file: " + err.Error())
}
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
    panic("failed to append PEM")
}

mysql.RegisterTLSConfig("custom", &tls.Config{
    RootCAs: rootCertPool,
})

dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?allowCleartextPasswords=true&tls=custom",
    dbUser, authenticationToken, dbEndpoint, dbName,
)
```

------
#### [ Ruby ]

**Example OCI 函式的 Ruby 連線組態**  

```
conn = Mysql2::Client.new(
    host: endpoint,
    username: user,
    password: token,
    port: port,
    database: db_name,
    sslca: '/us-east-1-bundle.pem',  # Path to the certificate in container
    sslverify: true
)
```

------

## 連線至 Lambda 函數中的 Amazon RDS 資料庫
<a name="rds-connection"></a>

以下程式碼範例示範如何實作連線至 Amazon RDS 資料庫的 Lambda 函式。該函數會提出簡單的資料庫請求並傳回結果。

**注意**  
這些程式碼範例僅適用於 [.zip 部署套件](configuration-function-zip.md)。如果使用[容器映像](images-create.md)部署函式，您必須在函式程式碼中指定 Amazon RDS 憑證檔案，如[上一節](#oci-certificate)所述。

------
#### [ .NET ]

**適用於 .NET 的 SDK**  
 GitHub 上提供更多範例。尋找完整範例，並了解如何在[無伺服器範例](https://github.com/aws-samples/serverless-snippets/tree/main/lambda-function-connect-rds-iam)儲存庫中設定和執行。
使用 .NET 連線至 Lambda 函式中的 Amazon RDS 資料庫。  

```
using System.Data;
using System.Text.Json;
using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using MySql.Data.MySqlClient;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace aws_rds;

public class InputModel
{
    public string key1 { get; set; }
    public string key2 { get; set; }
}

public class Function
{
    /// <summary>
    // Handles the Lambda function execution for connecting to RDS using IAM authentication.
    /// </summary>
    /// <param name="input">The input event data passed to the Lambda function</param>
    /// <param name="context">The Lambda execution context that provides runtime information</param>
    /// <returns>A response object containing the execution result</returns>

    public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context)
    {
        // Sample Input: {"body": "{\"key1\":\"20\", \"key2\":\"25\"}"}
        var input = JsonSerializer.Deserialize<InputModel>(request.Body);

        /// Obtain authentication token
        var authToken = RDSAuthTokenGenerator.GenerateAuthToken(
            Environment.GetEnvironmentVariable("RDS_ENDPOINT"),
            Convert.ToInt32(Environment.GetEnvironmentVariable("RDS_PORT")),
            Environment.GetEnvironmentVariable("RDS_USERNAME")
        );

        /// Build the Connection String with the Token 
        string connectionString = $"Server={Environment.GetEnvironmentVariable("RDS_ENDPOINT")};" +
                                  $"Port={Environment.GetEnvironmentVariable("RDS_PORT")};" +
                                  $"Uid={Environment.GetEnvironmentVariable("RDS_USERNAME")};" +
                                  $"Pwd={authToken};";


        try
        {
            await using var connection = new MySqlConnection(connectionString);
            await connection.OpenAsync();

            const string sql = "SELECT @param1 + @param2 AS Sum";

            await using var command = new MySqlCommand(sql, connection);
            command.Parameters.AddWithValue("@param1", int.Parse(input.key1 ?? "0"));
            command.Parameters.AddWithValue("@param2", int.Parse(input.key2 ?? "0"));

            await using var reader = await command.ExecuteReaderAsync();
            if (await reader.ReadAsync())
            {
                int result = reader.GetInt32("Sum");

                //Sample Response: {"statusCode":200,"body":"{\"message\":\"The sum is: 45\"}","isBase64Encoded":false}
                return new APIGatewayProxyResponse
                {
                    StatusCode = 200,
                    Body = JsonSerializer.Serialize(new { message = $"The sum is: {result}" })
                };
            }

        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }

        return new APIGatewayProxyResponse
        {
            StatusCode = 500,
            Body = JsonSerializer.Serialize(new { error = "Internal server error" })
        };
    }
}
```

------
#### [ Go ]

**SDK for Go V2**  
 GitHub 上提供更多範例。尋找完整範例，並了解如何在[無伺服器範例](https://github.com/aws-samples/serverless-snippets/tree/main/lambda-function-connect-rds-iam)儲存庫中設定和執行。
使用 Go 連線至 Lambda 函數中的 Amazon RDS 資料庫。  

```
/*
Golang v2 code here.
*/

package main

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"os"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
	_ "github.com/go-sql-driver/mysql"
)

type MyEvent struct {
	Name string `json:"name"`
}

func HandleRequest(event *MyEvent) (map[string]interface{}, error) {

	var dbName string = os.Getenv("DatabaseName")
	var dbUser string = os.Getenv("DatabaseUser")
	var dbHost string = os.Getenv("DBHost") // Add hostname without https
	var dbPort int = os.Getenv("Port")      // Add port number
	var dbEndpoint string = fmt.Sprintf("%s:%d", dbHost, dbPort)
	var region string = os.Getenv("AWS_REGION")

	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		panic("configuration error: " + err.Error())
	}

	authenticationToken, err := auth.BuildAuthToken(
		context.TODO(), dbEndpoint, region, dbUser, cfg.Credentials)
	if err != nil {
		panic("failed to create authentication token: " + err.Error())
	}

	dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?tls=true&allowCleartextPasswords=true",
		dbUser, authenticationToken, dbEndpoint, dbName,
	)

	db, err := sql.Open("mysql", dsn)
	if err != nil {
		panic(err)
	}

	defer db.Close()

	var sum int
	err = db.QueryRow("SELECT ?+? AS sum", 3, 2).Scan(&sum)
	if err != nil {
		panic(err)
	}
	s := fmt.Sprint(sum)
	message := fmt.Sprintf("The selected sum is: %s", s)

	messageBytes, err := json.Marshal(message)
	if err != nil {
		return nil, err
	}

	messageString := string(messageBytes)
	return map[string]interface{}{
		"statusCode": 200,
		"headers":    map[string]string{"Content-Type": "application/json"},
		"body":       messageString,
	}, nil
}

func main() {
	lambda.Start(HandleRequest)
}
```

------
#### [ Java ]

**SDK for Java 2.x**  
 GitHub 上提供更多範例。尋找完整範例，並了解如何在[無伺服器範例](https://github.com/aws-samples/serverless-snippets/tree/main/lambda-function-connect-rds-iam)儲存庫中設定和執行。
使用 Java 連線至 Lambda 函數中的 Amazon RDS 資料庫。  

```
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.rdsdata.RdsDataClient;
import software.amazon.awssdk.services.rdsdata.model.ExecuteStatementRequest;
import software.amazon.awssdk.services.rdsdata.model.ExecuteStatementResponse;
import software.amazon.awssdk.services.rdsdata.model.Field;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class RdsLambdaHandler implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

    @Override
    public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent event, Context context) {
        APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();

        try {
            // Obtain auth token
            String token = createAuthToken();

            // Define connection configuration
            String connectionString = String.format("jdbc:mysql://%s:%s/%s?useSSL=true&requireSSL=true",
                    System.getenv("ProxyHostName"),
                    System.getenv("Port"),
                    System.getenv("DBName"));

            // Establish a connection to the database
            try (Connection connection = DriverManager.getConnection(connectionString, System.getenv("DBUserName"), token);
                 PreparedStatement statement = connection.prepareStatement("SELECT ? + ? AS sum")) {

                statement.setInt(1, 3);
                statement.setInt(2, 2);

                try (ResultSet resultSet = statement.executeQuery()) {
                    if (resultSet.next()) {
                        int sum = resultSet.getInt("sum");
                        response.setStatusCode(200);
                        response.setBody("The selected sum is: " + sum);
                    }
                }
            }

        } catch (Exception e) {
            response.setStatusCode(500);
            response.setBody("Error: " + e.getMessage());
        }

        return response;
    }

    private String createAuthToken() {
        // Create RDS Data Service client
        RdsDataClient rdsDataClient = RdsDataClient.builder()
                .region(Region.of(System.getenv("AWS_REGION")))
                .credentialsProvider(DefaultCredentialsProvider.create())
                .build();

        // Define authentication request
        ExecuteStatementRequest request = ExecuteStatementRequest.builder()
                .resourceArn(System.getenv("ProxyHostName"))
                .secretArn(System.getenv("DBUserName"))
                .database(System.getenv("DBName"))
                .sql("SELECT 'RDS IAM Authentication'")
                .build();

        // Execute request and obtain authentication token
        ExecuteStatementResponse response = rdsDataClient.executeStatement(request);
        Field tokenField = response.records().get(0).get(0);

        return tokenField.stringValue();
    }
}
```

------
#### [ JavaScript ]

**適用於 JavaScript (v3) 的 SDK**  
 GitHub 上提供更多範例。尋找完整範例，並了解如何在[無伺服器範例](https://github.com/aws-samples/serverless-snippets/tree/main/lambda-function-connect-rds-iam)儲存庫中設定和執行。
使用 JavaScript 連線至 Lambda 函數中的 Amazon RDS 資料庫。  

```
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
/* 
Node.js code here.
*/
// ES6+ example
import { Signer } from "@aws-sdk/rds-signer";
import mysql from 'mysql2/promise';

async function createAuthToken() {
  // Define connection authentication parameters
  const dbinfo = {

    hostname: process.env.ProxyHostName,
    port: process.env.Port,
    username: process.env.DBUserName,
    region: process.env.AWS_REGION,

  }

  // Create RDS Signer object
  const signer = new Signer(dbinfo);

  // Request authorization token from RDS, specifying the username
  const token = await signer.getAuthToken();
  return token;
}

async function dbOps() {

  // Obtain auth token
  const token = await createAuthToken();
  // Define connection configuration
  let connectionConfig = {
    host: process.env.ProxyHostName,
    user: process.env.DBUserName,
    password: token,
    database: process.env.DBName,
    ssl: 'Amazon RDS'
  }
  // Create the connection to the DB
  const conn = await mysql.createConnection(connectionConfig);
  // Obtain the result of the query
  const [res,] = await conn.execute('select ?+? as sum', [3, 2]);
  return res;

}

export const handler = async (event) => {
  // Execute database flow
  const result = await dbOps();
  // Return result
  return {
    statusCode: 200,
    body: JSON.stringify("The selected sum is: " + result[0].sum)
  }
};
```
使用 TypeScript 連線至 Lambda 函數中的 Amazon RDS 資料庫。  

```
import { Signer } from "@aws-sdk/rds-signer";
import mysql from 'mysql2/promise';

// RDS settings
// Using '!' (non-null assertion operator) to tell the TypeScript compiler that the DB settings are not null or undefined,
const proxy_host_name = process.env.PROXY_HOST_NAME!
const port = parseInt(process.env.PORT!)
const db_name = process.env.DB_NAME!
const db_user_name = process.env.DB_USER_NAME!
const aws_region = process.env.AWS_REGION!


async function createAuthToken(): Promise<string> {

    // Create RDS Signer object
    const signer = new Signer({
        hostname: proxy_host_name,
        port: port,
        region: aws_region,
        username: db_user_name
    });

    // Request authorization token from RDS, specifying the username
    const token = await signer.getAuthToken();
    return token;
}

async function dbOps(): Promise<mysql.QueryResult | undefined> {
    try {
        // Obtain auth token
        const token = await createAuthToken();
        const conn = await mysql.createConnection({
            host: proxy_host_name,
            user: db_user_name,
            password: token,
            database: db_name,
            ssl: 'Amazon RDS' // Ensure you have the CA bundle for SSL connection
        });
        const [rows, fields] = await conn.execute('SELECT ? + ? AS sum', [3, 2]);
        console.log('result:', rows);
        return rows;
    }
    catch (err) {
        console.log(err);
    }
}

export const lambdaHandler = async (event: any): Promise<{ statusCode: number; body: string }> => {
    // Execute database flow
    const result = await dbOps();

    // Return error is result is undefined
    if (result == undefined)
        return {
            statusCode: 500,
            body: JSON.stringify(`Error with connection to DB host`)
        }

    // Return result
    return {
        statusCode: 200,
        body: JSON.stringify(`The selected sum is: ${result[0].sum}`)
    };
};
```

------
#### [ PHP ]

**適用於 PHP 的 SDK**  
 GitHub 上提供更多範例。尋找完整範例，並了解如何在[無伺服器範例](https://github.com/aws-samples/serverless-snippets/tree/main/lambda-function-connect-rds-iam)儲存庫中設定和執行。
使用 PHP 連線至 Lambda 函數中的 Amazon RDS 資料庫。  

```
<?php
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

# using bref/bref and bref/logger for simplicity

use Bref\Context\Context;
use Bref\Event\Handler as StdHandler;
use Bref\Logger\StderrLogger;
use Aws\Rds\AuthTokenGenerator;
use Aws\Credentials\CredentialProvider;

require __DIR__ . '/vendor/autoload.php';

class Handler implements StdHandler
{
    private StderrLogger $logger;
    public function __construct(StderrLogger $logger)
    {
        $this->logger = $logger;
    }


    private function getAuthToken(): string {
        // Define connection authentication parameters
        $dbConnection = [
            'hostname' => getenv('DB_HOSTNAME'),
            'port' => getenv('DB_PORT'),
            'username' => getenv('DB_USERNAME'),
            'region' => getenv('AWS_REGION'),
        ];

        // Create RDS AuthTokenGenerator object
        $generator = new AuthTokenGenerator(CredentialProvider::defaultProvider());

        // Request authorization token from RDS, specifying the username
        return $generator->createToken(
            $dbConnection['hostname'] . ':' . $dbConnection['port'],
            $dbConnection['region'],
            $dbConnection['username']
        );
    }

    private function getQueryResults() {
        // Obtain auth token
        $token = $this->getAuthToken();

        // Define connection configuration
        $connectionConfig = [
            'host' => getenv('DB_HOSTNAME'),
            'user' => getenv('DB_USERNAME'),
            'password' => $token,
            'database' => getenv('DB_NAME'),
        ];

        // Create the connection to the DB
        $conn = new PDO(
            "mysql:host={$connectionConfig['host']};dbname={$connectionConfig['database']}",
            $connectionConfig['user'],
            $connectionConfig['password'],
            [
                PDO::MYSQL_ATTR_SSL_CA => '/path/to/rds-ca-2019-root.pem',
                PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
            ]
        );

        // Obtain the result of the query
        $stmt = $conn->prepare('SELECT ?+? AS sum');
        $stmt->execute([3, 2]);

        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * @param mixed $event
     * @param Context $context
     * @return array
     */
    public function handle(mixed $event, Context $context): array
    {
        $this->logger->info("Processing query");

        // Execute database flow
        $result = $this->getQueryResults();

        return [
            'sum' => $result['sum']
        ];
    }
}

$logger = new StderrLogger();
return new Handler($logger);
```

------
#### [ Python ]

**適用於 Python 的 SDK (Boto3)**  
 GitHub 上提供更多範例。尋找完整範例，並了解如何在[無伺服器範例](https://github.com/aws-samples/serverless-snippets/tree/main/lambda-function-connect-rds-iam)儲存庫中設定和執行。
使用 Python 連線至 Lambda 函數中的 Amazon RDS 資料庫。  

```
import json
import os
import boto3
import pymysql

# RDS settings
proxy_host_name = os.environ['PROXY_HOST_NAME']
port = int(os.environ['PORT'])
db_name = os.environ['DB_NAME']
db_user_name = os.environ['DB_USER_NAME']
aws_region = os.environ['AWS_REGION']


# Fetch RDS Auth Token
def get_auth_token():
    client = boto3.client('rds')
    token = client.generate_db_auth_token(
        DBHostname=proxy_host_name,
        Port=port
        DBUsername=db_user_name
        Region=aws_region
    )
    return token

def lambda_handler(event, context):
    token = get_auth_token()
    try:
        connection = pymysql.connect(
            host=proxy_host_name,
            user=db_user_name,
            password=token,
            db=db_name,
            port=port,
            ssl={'ca': 'Amazon RDS'}  # Ensure you have the CA bundle for SSL connection
        )
        
        with connection.cursor() as cursor:
            cursor.execute('SELECT %s + %s AS sum', (3, 2))
            result = cursor.fetchone()

        return result
        
    except Exception as e:
        return (f"Error: {str(e)}")  # Return an error message if an exception occurs
```

------
#### [ Ruby ]

**SDK for Ruby**  
 GitHub 上提供更多範例。尋找完整範例，並了解如何在[無伺服器範例](https://github.com/aws-samples/serverless-snippets/tree/main/lambda-function-connect-rds-iam)儲存庫中設定和執行。
使用 Ruby 連線至 Lambda 函數中的 Amazon RDS 資料庫。  

```
# Ruby code here.

require 'aws-sdk-rds'
require 'json'
require 'mysql2'

def lambda_handler(event:, context:)
  endpoint = ENV['DBEndpoint'] # Add the endpoint without https"
  port = ENV['Port']           # 3306
  user = ENV['DBUser']
  region = ENV['DBRegion']     # 'us-east-1'
  db_name = ENV['DBName']

  credentials = Aws::Credentials.new(
    ENV['AWS_ACCESS_KEY_ID'],
    ENV['AWS_SECRET_ACCESS_KEY'],
    ENV['AWS_SESSION_TOKEN']
  )
  rds_client = Aws::RDS::AuthTokenGenerator.new(
    region: region, 
    credentials: credentials
  )

  token = rds_client.auth_token(
    endpoint: endpoint+ ':' + port,
    user_name: user,
    region: region
  )

  begin
    conn = Mysql2::Client.new(
      host: endpoint,
      username: user,
      password: token,
      port: port,
      database: db_name,
      sslca: '/var/task/global-bundle.pem', 
      sslverify: true,
      enable_cleartext_plugin: true
    )
    a = 3
    b = 2
    result = conn.query("SELECT #{a} + #{b} AS sum").first['sum']
    puts result
    conn.close
    {
      statusCode: 200,
      body: result.to_json
    }
  rescue => e
    puts "Database connection failed due to #{e}"
  end
end
```

------
#### [ Rust ]

**適用於 Rust 的 SDK**  
 GitHub 上提供更多範例。尋找完整範例，並了解如何在[無伺服器範例](https://github.com/aws-samples/serverless-snippets/tree/main/lambda-function-connect-rds-iam)儲存庫中設定和執行。
使用 Rust 連線至 Lambda 函數中的 Amazon RDS 資料庫。  

```
use aws_config::BehaviorVersion;
use aws_credential_types::provider::ProvideCredentials;
use aws_sigv4::{
    http_request::{sign, SignableBody, SignableRequest, SigningSettings},
    sign::v4,
};
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde_json::{json, Value};
use sqlx::postgres::PgConnectOptions;
use std::env;
use std::time::{Duration, SystemTime};

const RDS_CERTS: &[u8] = include_bytes!("global-bundle.pem");

async fn generate_rds_iam_token(
    db_hostname: &str,
    port: u16,
    db_username: &str,
) -> Result<String, Error> {
    let config = aws_config::load_defaults(BehaviorVersion::v2024_03_28()).await;

    let credentials = config
        .credentials_provider()
        .expect("no credentials provider found")
        .provide_credentials()
        .await
        .expect("unable to load credentials");
    let identity = credentials.into();
    let region = config.region().unwrap().to_string();

    let mut signing_settings = SigningSettings::default();
    signing_settings.expires_in = Some(Duration::from_secs(900));
    signing_settings.signature_location = aws_sigv4::http_request::SignatureLocation::QueryParams;

    let signing_params = v4::SigningParams::builder()
        .identity(&identity)
        .region(&region)
        .name("rds-db")
        .time(SystemTime::now())
        .settings(signing_settings)
        .build()?;

    let url = format!(
        "https://{db_hostname}:{port}/?Action=connect&DBUser={db_user}",
        db_hostname = db_hostname,
        port = port,
        db_user = db_username
    );

    let signable_request =
        SignableRequest::new("GET", &url, std::iter::empty(), SignableBody::Bytes(&[]))
            .expect("signable request");

    let (signing_instructions, _signature) =
        sign(signable_request, &signing_params.into())?.into_parts();

    let mut url = url::Url::parse(&url).unwrap();
    for (name, value) in signing_instructions.params() {
        url.query_pairs_mut().append_pair(name, &value);
    }

    let response = url.to_string().split_off("https://".len());

    Ok(response)
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    run(service_fn(handler)).await
}

async fn handler(_event: LambdaEvent<Value>) -> Result<Value, Error> {
    let db_host = env::var("DB_HOSTNAME").expect("DB_HOSTNAME must be set");
    let db_port = env::var("DB_PORT")
        .expect("DB_PORT must be set")
        .parse::<u16>()
        .expect("PORT must be a valid number");
    let db_name = env::var("DB_NAME").expect("DB_NAME must be set");
    let db_user_name = env::var("DB_USERNAME").expect("DB_USERNAME must be set");

    let token = generate_rds_iam_token(&db_host, db_port, &db_user_name).await?;

    let opts = PgConnectOptions::new()
        .host(&db_host)
        .port(db_port)
        .username(&db_user_name)
        .password(&token)
        .database(&db_name)
        .ssl_root_cert_from_pem(RDS_CERTS.to_vec())
        .ssl_mode(sqlx::postgres::PgSslMode::Require);

    let pool = sqlx::postgres::PgPoolOptions::new()
        .connect_with(opts)
        .await?;

    let result: i32 = sqlx::query_scalar("SELECT $1 + $2")
        .bind(3)
        .bind(2)
        .fetch_one(&pool)
        .await?;

    println!("Result: {:?}", result);

    Ok(json!({
        "statusCode": 200,
        "content-type": "text/plain",
        "body": format!("The selected sum is: {result}")
    }))
}
```

------

## 處理來自 Amazon RDS 的事件通知
<a name="rds-events"></a>

您可以使用 Lambda 來處理 Amazon RDS 資料庫的事件通知。Amazon RDS 會將通知傳送到 Amazon Simple Notification Service (Amazon SNS) 主題，您可以進行設定，透過該主題叫用 Lambda 函數。Amazon SNS 會將來自 Amazon RDS 的訊息包裝在自己的事件文件中，並將其傳送到函數。

如需設定 Amazon RDS 資料庫以傳送通知的詳細資訊，請參閱[使用 Amazon RDS 事件通知](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html)。

**Example Amazon SNS 事件中的 Amazon RDS 訊息**  

```
{
        "Records": [
          {
            "EventVersion": "1.0",
            "EventSubscriptionArn": "arn:aws:sns:us-east-2:123456789012:rds-lambda:21be56ed-a058-49f5-8c98-aedd2564c486",
            "EventSource": "aws:sns",
            "Sns": {
              "SignatureVersion": "1",
              "Timestamp": "2023-01-02T12:45:07.000Z",
              "Signature": "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==",
              "SigningCertUrl": "https://sns.us-east-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem",
              "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
              "Message": "{\"Event Source\":\"db-instance\",\"Event Time\":\"2023-01-02 12:45:06.000\",\"Identifier Link\":\"https://console.aws.amazon.com/rds/home?region=eu-west-1#dbinstance:id=dbinstanceid\",\"Source ID\":\"dbinstanceid\",\"Event ID\":\"http://docs.amazonwebservices.com/AmazonRDS/latest/UserGuide/USER_Events.html#RDS-EVENT-0002\",\"Event Message\":\"Finished DB Instance backup\"}",
              "MessageAttributes": {},
              "Type": "Notification",
              "UnsubscribeUrl": "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&amp;SubscriptionArn=arn:aws:sns:us-east-2:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486",
              "TopicArn":"arn:aws:sns:us-east-2:123456789012:sns-lambda",
              "Subject": "RDS Notification Message"
            }
          }
        ]
      }
```

## 完成 Lambda 和 Amazon RDS 教學課程
<a name="rds-database-samples"></a>
+ [使用 Lambda 函數來存取 Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-lambda-tutorial.html)：在《Amazon RDS 誰用著指南》中，學習如何使用 Lambda 函數並透過 Amazon RDS Proxy 將資料寫入 Amazon RDS 資料庫。每當新增訊息，您的 Lambda 函數將從 Amazon SQS 佇列中讀取記錄，然後將新項目寫入資料庫中的資料表。

# 為基於 Lambda 的應用程式選擇資料庫服務
<a name="ddb-rds-database-decision"></a>

許多無伺服器應用程式需要存放和擷取資料。 AWS 提供多種資料庫選項，可與 Lambda 函數搭配使用。最熱門的兩個選擇是 NoSQL 資料庫服務 Amazon DynamoDB，以及傳統的關聯式資料庫解決方案 Amazon RDS。下列各節介紹了這兩種服務與 Lambda 搭配使用時的主要差異，並協助您為無伺服器應用程式選擇合適的資料庫服務。

若要進一步了解 提供的其他資料庫服務 AWS，以及更普遍地了解其使用案例和權衡，請參閱[選擇 AWS 資料庫服務](https://docs.aws.amazon.com/decision-guides/latest/databases-on-aws-how-to-choose/databases-on-aws-how-to-choose.html)。所有 AWS 資料庫服務都與 Lambda 相容，但並非所有資料庫服務都適合自身特定使用案例。

## 選取與 Lambda 搭配使用的資料庫服務時，您有哪些選擇？
<a name="w2aad101d101c19b9"></a>

AWS 提供多個資料庫服務。對於無伺服器應用程式，最熱門的兩個選擇是 DynamoDB 和 Amazon RDS。
+ **DynamoDB** 是一種全受管 NoSQL 資料庫服務，針對無伺服器應用程式進行了最佳化。該服務可在任何規模下提供無縫擴展且穩定的個位數毫秒級效能。
+ **Amazon RDS** 是一種受管關聯式資料庫服務，支援包括 MySQL 與 PostgreSQL 在內的多種資料庫引擎。該服務透過受管基礎結構提供熟悉的 SQL 功能。

## 您已知道自己需求時的建議
<a name="w2aad101d101c19c11"></a>

如果您已經清楚自己的需求，以下是基本建議：

對於需要穩定低延遲效能、自動擴展且不涉及複雜聯結或交易的無伺服器應用程式，建議選擇 [DynamoDB](with-ddb.md)。由於其無伺服器特性，該服務特別適合基於 Lambda 的應用程式。

若需複雜的 SQL 查詢、聯結操作，或是現有應用程式已使用關聯式資料庫時，[Amazon RDS](services-rds.md) 是更佳選擇。但請注意，將 Lambda 函式連線至 Amazon RDS 需要額外的組態，並且可能會影響冷啟動時間。

## 選取資料庫服務時應考量的事項
<a name="w2aad101d101c19c13"></a>

為 Lambda 應用程式選取 DynamoDB 或是 Amazon RDS 時，建議考量下列因素：
+ 連線管理與冷啟動
+ 資料存取模式
+ 查詢複雜性
+ 資料一致性需求
+ 擴展特性
+ 成本模型

透過了解這些因素，您可以選取最符合自身特定使用案例需求的選項。

### 連線管理與冷啟動
<a name="w2aad101d101c19c13b9b1"></a>
+ DynamoDB 對所有操作均使用 HTTP API。Lambda 函式無需維護連線即可發出即時請求，具備更佳的冷啟動效能。每個請求都會使用 AWS 登入資料進行身分驗證，而不會產生連線額外負荷。
+ 由於 Amazon RDS 使用的是傳統資料庫連線，因此需要管理連線集區。這可能會影響冷啟動，因為新的 Lambda 執行個體需要建立連線。您需實作連線集區策略，並且可能要使用 [Amazon RDS Proxy](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy.html) 來有效管理連線。請注意，使用 Amazon RDS Proxy 會額外產生費用。

### 資料存取模式
<a name="w2aad101d101c19c13b9b3"></a>
+ DynamoDB 最適用於已知的存取模式和單一資料表設計。其非常適合需要根據主索引鍵或次要索引，以穩定低延遲方式存取資料的 Lambda 應用程式。
+ Amazon RDS 具備彈性，能因應複雜查詢與多變的存取模式。當您的 Lambda 函式需要跨多個資料表執行獨特的客製化查詢或複雜的跨表聯結時，其適用性更佳。

### 查詢複雜性
<a name="w2aad101d101c19c13b9b5"></a>
+ DynamoDB 在基於鍵值的簡易操作及預先定義的存取模式方面表現出色。複雜的查詢必須圍繞索引結構進行設計，並且必須在應用程式程式碼中處理聯結。
+ Amazon RDS 支援包含聯結、子查詢與彙總運算的複雜 SQL 查詢。當需要複雜的資料操作時，此優勢可以簡化您的 Lambda 函式程式碼。

### 資料一致性需求
<a name="w2aad101d101c19c13b9b7"></a>
+ DynamoDB 提供最終一致性與強一致性選項，其中強一致性適用於單一項目讀取。雖然支援交易功能，但存在部分限制。
+ Amazon RDS 提供完整的原子性、一致性、隔離性與持久性 (ACID) 合規保障，支援複雜交易。如果您的 Lambda 函式需要複雜的交易或跨多筆記錄的強一致性，Amazon RDS 可能更加合適。

### 擴展特性
<a name="w2aad101d101c19c13b9b9"></a>
+ DynamoDB 會根據您的工作負載自動擴展。其無需預先佈建，即可處理 Lambda 函式中的流量突增情況。您可以使用隨需容量模式，僅需為所使用的容量付費，完全符合 Lambda 的擴展模型。
+ Amazon RDS 會根據您選擇的執行個體規格，提供固定的容量。如果多個 Lambda 函式同時嘗試連線，則可能會超過連線配額。您需謹慎管理連線集區，並可能要實作重試邏輯。

### 成本模型
<a name="w2aad101d101c19c13b9c11"></a>
+ DynamoDB 的定價模式與無伺服器應用程式高度契合。若使用隨需容量，您只需為 Lambda 函式執行的實際讀取與寫入作業付費。閒置時間不收取任何費用。
+ Amazon RDS 會針對執行中的執行個體收費，無論用量如何。對於無伺服器應用程式中典型的零星工作負載，這可能較不具成本效益。不過，對於輸送量高且用量穩定的工作負載，其可能更經濟實惠。

## 開始使用所選資料庫服務
<a name="w2aad101d101c19c15"></a>

您已了解在 DynamoDB 和 Amazon RDS 之間進行選擇的標準，以及二者之間的主要差異。接下來，您可以選取最符合自身需求的選項，並使用下列資源來協助自己開始使用所選的資料庫服務。

------
#### [ DynamoDB ]

**透過下列資源開始使用 DynamoDB**
+ 如需 DynamoDB 服務的簡介，請參閱 *Amazon DynamoDB Developer Guide* 中的 [What is DynamoDB?](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html) 部分。
+ 請依照[搭配 API Gateway 使用 Lambda](services-apigateway-tutorial.md) 教學課程，了解如何透過 Lambda 函式在回應 API 請求時，對 DynamoDB 資料表執行 CRUD 操作的範例。
+ 請參閱《Amazon [ DynamoDB 開發人員指南》中的使用 DynamoDB 和 AWS SDKs 進行程式設計](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.html)，以進一步了解如何使用其中一個 SDK 從 Lambda 函數內存取 AWS SDKs DynamoDB。 * DynamoDB * 

------
#### [ Amazon RDS ]

**透過下列資源開始使用 Amazon RDS**
+ 如需 Amazon RDS 服務簡介，請參閱 *Amazon Relational Database Service User Guide* 中的 [What is Amazon Relational Database Service (Amazon RDS)?](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Welcome.html) 部分。
+ 請依照 *Amazon Relational Database Service User Guide* 中的教學課程 [Using a Lambda function to access an Amazon RDS database](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-lambda-tutorial.html) 執行操作。
+ 請參閱 [AWS Lambda 搭配 Amazon RDS 使用](services-rds.md)，了解有關搭配 Amazon RDS 使用 Lambda 的詳細資訊。

------