AWS Lambda와 Amazon RDS 사용하기

AWS Lambda와 Amazon RDS 사용하기 (Using AWS Lambda with Amazon RDS)

Lambda 함수를 Amazon Relational Database Service(Amazon RDS) 데이터베이스에 직접 또는 Amazon RDS Proxy를 통해 연결할 수 있어요. 직접 연결은 단순한 시나리오에 유용하고, 프로덕션에는 프록시를 권장해요. 데이터베이스 프록시는 공유 데이터베이스 연결 풀을 관리하므로 함수가 데이터베이스 연결을 소진하지 않고 높은 동시성 수준에 도달할 수 있게 해요.

워크로드에 Amazon RDS를 사용할지 DynamoDB를 사용할지 결정하는 데 도움이 필요하면 Lambda 기반 애플리케이션용 데이터베이스 서비스 선택을 참고하세요. 잦은 짧은 데이터베이스 연결을 만들거나 많은 수의 데이터베이스 연결을 열고 닫는 Lambda 함수에는 Amazon RDS Proxy를 사용할 것을 권장해요.

팁

Lambda 함수를 Amazon RDS 데이터베이스에 빠르게 연결하려면 콘솔 안내 마법사를 사용할 수 있어요. 마법사를 열려면 다음을 수행하세요.

  1. Lambda 콘솔의 Functions 페이지를 엽니다.
  2. 데이터베이스를 연결할 함수를 선택합니다.
  3. Configuration 탭에서 RDS databases를 선택합니다.
  4. Connect to RDS database를 선택합니다.

함수를 데이터베이스에 연결한 후 Add proxy를 선택해 프록시를 만들 수 있어요.

출처: AWS Lambda 개발자 안내서

본문

RDS 리소스와 함께 작동하도록 함수 구성

Lambda 콘솔에서 Amazon RDS 데이터베이스 인스턴스와 프록시 리소스를 프로비저닝·구성할 수 있어요. Configuration 탭 아래의 RDS databases로 이동해 이렇게 할 수 있어요. 또는 Amazon RDS 콘솔에서 Lambda 함수 연결을 만들고 구성할 수도 있어요. Lambda와 함께 사용할 RDS 데이터베이스 인스턴스를 구성할 때 다음 기준을 참고하세요.

  • 데이터베이스에 연결하려면 함수가 데이터베이스가 실행되는 것과 같은 Amazon VPC에 있어야 합니다.
  • MySQL, MariaDB, PostgreSQL, Microsoft SQL Server 엔진의 Amazon RDS 데이터베이스를 사용할 수 있습니다.
  • MySQL 또는 PostgreSQL 엔진의 Aurora DB 클러스터도 사용할 수 있습니다.
  • 데이터베이스 인증을 위해 Secrets Manager 시크릿을 제공해야 합니다.
  • IAM 역할이 시크릿 사용 권한을 제공하고, 신뢰 정책이 Amazon RDS가 역할을 수임하도록 허용해야 합니다.
  • 콘솔로 Amazon RDS 리소스를 구성하고 함수에 연결하는 IAM 주체는 다음 권한이 있어야 합니다. (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는 데이터베이스 인스턴스 크기에 따라 프록시에 대해 시간당 요금을 청구해요. 프록시 연결에 대한 일반 정보는 Amazon RDS 사용자 안내서의 Amazon RDS Proxy 사용을 참고하세요.

Amazon RDS 연결의 SSL/TLS 요구 사항

Amazon RDS 데이터베이스 인스턴스에 보안 SSL/TLS 연결을 하려면 Lambda 함수가 신뢰할 수 있는 인증서로 데이터베이스 서버의 신원을 검증해야 해요. Lambda는 배포 패키지 유형에 따라 이 인증서를 다르게 처리해요.

  • .zip 파일 아카이브: 인증서 처리는 런타임에 따라 다릅니다.
    • 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주가 걸릴 수 있습니다.
  • 컨테이너 이미지: AWS 베이스 이미지는 CA 인증서만 포함합니다. 함수가 Amazon RDS 데이터베이스 인스턴스에 연결한다면 컨테이너 이미지에 적절한 인증서를 포함해야 합니다. Dockerfile에서 데이터베이스를 호스팅하는 AWS 리전에 해당하는 인증서 번들을 다운로드하세요. 예시:
    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 예시(OCI 함수 연결 구성)

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
    }
};

readFileSync 함수가 필요한 이유는 Node.js 데이터베이스 클라이언트가 인증서 파일의 경로가 아니라 실제 인증서 내용을 메모리에 필요로 하기 때문이에요.

Python 예시(OCI 함수 연결 구성)

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 예시(OCI 함수 연결 문자열)

JDBC 연결을 사용하는 Java 함수의 연결 문자열에는 다음이 포함되어야 해요.

  • useSSL=true
  • requireSSL=true
  • 컨테이너 이미지의 Amazon RDS 인증서 위치를 가리키는 sslCA 파라미터
// 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 예시(OCI 함수의 MySQL 연결 문자열)

/// 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 예시(OCI 함수의 MySQL 연결)

MySQL 연결을 사용하는 Go 함수는 Amazon RDS 인증서를 인증서 풀에 로드하고 MySQL 드라이버에 등록해야 해요. 연결 문자열은 그런 다음 tls 파라미터로 이 구성을 참조해야 해요.

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 예시(OCI 함수 연결 구성)

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 데이터베이스에 연결

다음 코드 예제들은 Amazon RDS 데이터베이스에 연결하는 Lambda 함수 구현 방법을 보여줘요. 함수는 간단한 데이터베이스 요청을 하고 그 결과를 반환해요.

참고

이 코드 예제는 .zip 배포 패키지에만 유효해요. 컨테이너 이미지로 함수를 배포한다면 앞선 섹션에서 설명한 대로 함수 코드에 Amazon RDS 인증서 파일을 지정해야 해요.

JavaScript 예시 — SDK for JavaScript (v3)

// 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)
  }
};

Java 예시 — SDK for Java 2.x

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();
    }
}

Go 예시 — SDK for Go V2

/*
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)
}

.NET 예시 — SDK for .NET

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
{
    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" })
        };
    }
}

이 문서 안내서는 .NET, Go, Java, JavaScript(TypeScript), PHP, Python, Ruby, Rust용 코드 예제를 제공해요. 각 언어의 전체 예제와 설정·실행 방법은 AWS의 Serverless examples GitHub 저장소를 참고하세요.

더 알아보기 (Learn more)