亚马逊AWS官方博客

配合 AWS Lambda 使用 Amazon RDS Proxy

Original URL: https://amazonaws-china.com/cn/blogs/compute/using-amazon-rds-proxy-with-aws-lambda/

 

更新 – 2020年6月30日: Amazon RDS Proxy对MySQL与PostgreSQL的支持现已全面发布

更新 – 2020年4月8日: 我们公布Amazon RDS Proxy对Postgres的兼容性已发布预览版,目前支持10.11与11.5版本。

借助AWS无服务器平台,您可以构建起根据业务需求自动实现规模伸缩的应用程序。在负载峰值期间,Amazon API Gateway与AWS Lambda会根据传入的负载自动完成规模扩展。

一般来说,开发人员需要从Lambda函数访问关系数据库中存储的数据。但其中的难题在于,如何确保Lambda调用活动不会产生过多连接,并最终导致数据库过载。关系数据库的最大并发连接数,由数据库的自身大小决定。

这是因为每条连接都会消耗数据库服务器上的内存与CPU资源。Lambda函数可能在扩展当中产生成千上万条并发连接,这意味着数据库需要调拨更多资源以维护连接,而非实际执行查询。

关于规模伸缩的更多详细信息,请参阅架构阐释博文《如何面向大规模负载设计无服务器应用程序》。

无服务器架构与RDS

正是由于Lambda能够轻松扩展至成千上万条并发请求,因此这样的设计会给您的后端关系数据库带来极高负载。在大多数情况下,关系数据库一方在设计层面都无法承载如此恐怖的并发连接数量。

专为Amazon RDS服务的数据库代理

今天,我们高兴地宣布Amazon RDS Proxy已推出预览版。RDS Proxy将充当您的应用程序与RDS数据库之间的中介。RDS Proxy负责建立并管理与数据库之间的必要连接池,保证应用程序只需要创建少部分数据库连接,即可正常运行起效。

您可以使用RDS Proxy代理对数据库执行SQL调用的任何应用程序。但在无服务器架构下,我们的主要目标在于改善Lambda使用体验。代理将负责处理以往需要由Lambda函数直接流向数据库的一切传输流。

配合RDS Proxy,您的Lambda函数将不再与数据库实例直接交互。RDS Proxy将为Lambda函数创建的大量并发连接建立起连接池,并实现对连接池的规模伸缩。以此为基础,您的Lambda应用程序将能够复用现有连接,而不再为每一次新的函数调用创建对应的新连接。

RDS Proxy还具有自动规模伸缩功能,确保您的数据库实例只需要较低的内存与CPU资源用量,即可完成连接管理。它还使用热连接池以提高性能。使用RDS Proxy,您不再需要额外编写代码以清理闲置连接并管理连接池,这将使您的函数代码更简洁、也更易于维护。

立即使用

目前, RDS Database proxy尚处于预览阶段,因此大家需要注意以下几点:

  • 我们目前支持在MySQL 5.6或5.7版本之上运行的Amazon RDS MySQL或Aurora MySQL。
  • 预览版只适用于亚太区域(东京)、欧盟区域(爱尔兰)、美国东部区域(俄亥俄州)、美国东部区域(北弗吉尼亚州)以及美国西部区域(俄勒冈州)。
  • 在公开预览期间,您应使用AWS管理控制台与RDS代理进行交互。
  • 请不要将这项服务应用于生产级工作负载,期间我们可能对预览版本做出一系列调整。

查看预览指南以了解关于此项服务的更多详细信息。

先决条件

从现有数据库(Amazon RDS MySQL或Aurora MySQL)开始,将您的数据库凭证以密钥形式存储在AWS Secrets Manager当中,而后创建允许RDS Proxy读取此密钥的IAM策略。

要创建secret:

  • 登录至AWS Secrets Manager 并选择Store a new Secret
  • 选择 Credentials for RDS Database
  • 输入 user name以及password
  • 选择使用此secret的RDS数据库,而后选择 Next保存新的secret
  • 输入一条 Secret Name,而后选择 Next保存secret
  • 接受所有默认设置,并选择 Store。请注意系统分配给此secret的ARN,我们将在稍后再次用到。Secret细节信息
  • 现在,创建一个IAM角色,允许RDS Proxy读取此secret。RDS Proxy使用这条secret维护与数据库的连接池。请前往您的IAM控制台并创建一个新角色。接下来,为您在上一步中创建完成的secret添加一条secretmanager授权策略。例如:
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "VisualEditor0",
          "Effect": "Allow",
          "Action": [
            "secretsmanager:GetResourcePolicy",
            "secretsmanager:GetSecretValue",
            "secretsmanager:DescribeSecret",
            "secretsmanager:ListSecretVersionIds"
          ],
          "Resource": [
            "arn:aws:secretsmanager:us-east-2:[your-account-number]:secret:gmao-rds-secret-YZ2MMN"
          ]
        },
        {
          "Sid": "VisualEditor1",
          "Effect": "Allow",
          "Action": [
            "secretsmanager:GetRandomPassword",
            "secretsmanager:ListSecrets"
          ],
          "Resource": "*"
        }
      ]
    }
  • 添加以下Trust Policy,允许RDS使用该角色。保存角色并记录IAM角色ARN,我们稍后还将用到。
    {
     "Version": "2012-10-17",
     "Statement": [
      {
       "Sid": "",
       "Effect": "Allow",
       "Principal": {
        "Service": "rds.amazonaws.com"
       },
       "Action": "sts:AssumeRole"
      }
     ]
    }

创建代理,并将其附加至Lambda函数上

接下来,使用Lambda控制台,选择Add a Database proxy为Lambda函数添加一个数据库代理。

  • 登录至AWS Lambda控制台,打开要启用RDS Proxy的Lambda函数。请配置此Lambda函数,保证其有权访问您RDS数据库所处的VPC与子网。
  • 下滚至Lambda配置页面底部,选择 Add Database Proxy添加数据库代理
  • 按照 Add database proxy向导程序的指引操作,填写 Proxy Identifier 并选择您的 RDS Database。而后选择大家之前创建完成的 Secrets Manager secretIAM role。RDS Proxy会使用此secret接入您的数据库。最后选择 Add配置数据库代理
  • 等待几分钟,RDS Proxy将置备完成且状态更新为Available。数据库代理置备完成
  • 选择您的代理以查看细节信息。请记录 Proxy endpoint,我们稍后将在Lambda函数代码当中用到。Available Proxy配置

现在,Lambda函数已经具有使用已配置RDS Proxy的权限。到这里,我们就做好了接入该代理的全部准备。

使用代理

相较于直接接入RDS实例,现在我们转而接入RDS代理端点。在这方面,您有两个安全选项。其一,您可以使用IAM身份验证;其二,您也可以使用存储在Secrets Manager中的本机数据库凭证。本文建议大家使用IAM身份验证,因为其无需在函数代码中嵌入或者读取任何凭证。以下示例也将在RDS Proxy中使用IAM身份验证。

大家可以使用Lambda所支持的任何编程语言。以下示例使用Node.js:

let AWS = require('aws-sdk');
var mysql2 = require('mysql2'); //https://www.npmjs.com/package/mysql2
let fs  = require('fs');

let connection;

exports.handler = async(event) => {
	const promise = new Promise(function(resolve, reject) {
        
		console.log("Starting query ...\n");
	  	console.log("Running iam auth ...\n");
      
      	//
    	var signer = new AWS.RDS.Signer({
	        region: '[insert your region here]', // example: us-east-2
	        hostname: '[insert your RDS Proxy endpoint here]',
	        port: 3306,
	        username: '[Your RDS User name]'
  		});
        
	    let token = signer.getAuthToken({
	      username: '[Your RDS User name]'
	    });
    
    	console.log ("IAM Token obtained\n");
    
        let connectionConfig = {
          host: process.env['endpoint'], // Store your endpoint as an env var
          user: '[Your RDS User name]',
          database: process.env['my_db'], // Store your DB schema name as an env var
          ssl: { rejectUnauthorized: false},
          password: token,
          authSwitchHandler: function ({pluginName, pluginData}, cb) {
              console.log("Setting new auth handler.");
          }
        };
   
		// Adding the mysql_clear_password handler
        connectionConfig.authSwitchHandler = (data, cb) => {
            if (data.pluginName === 'mysql_clear_password') {
              // See https://dev.mysql.com/doc/internals/en/clear-text-authentication.html
              console.log("pluginName: "+data.pluginName);
              let password = token + '\0';
              let buffer = Buffer.from(password);
              cb(null, password);
            }
        };
        connection = mysql2.createConnection(connectionConfig);
		
		connection.connect(function(err) {
			if (err) {
				console.log('error connecting: ' + err.stack);
				return;
			}
			
			console.log('connected as id ' + connection.threadId + "\n");
		 });

		connection.query("SELECT * FROM contacts", function (error, results, fields) {
			if (error){ 
		  		//throw error;
		  		reject ("ERROR " + error);
			}
		  	
			if(results.length > 0){
				let result = results[0].email + ' ' + results[0].firstname + ' ' + results[0].lastname;
				console.log(result);
				
				let response = {
			        "statusCode": 200,
			        "statusDescription": "200 OK",
			        "isBase64Encoded": false,
			        "headers":{
			        	"Content-Type": "text/html"
			        },
			        body: result,
			    };
				
				connection.end(function(error, results) {
					  if(error){
					    //return "error";
					    reject ("ERROR");
					  }
					  // The connection is terminated now 
					  console.log("Connection ended\n");
					  
					  resolve(response);
				});
			}
		});
	});
	return promise;
};

 

大家需要将NodeJS MySQL客户端模块与您的函数打包起来。在本文的演练中,我们使用MySQL2模块,您可以点击此处获取。以此为基础,只需要执行标准的“npm install -save mysql2”命令,并将依赖项包含在Lambda打包中即可。这里我们使用Lambda环境变量存储连接信息。这是一项数据库配置最佳实践,因为我们可以随时更改这些细节信息,而无需更新代码本体。其中endpoint环境变更即为之前提到的RDS Proxy端点,而userpassword则为数据库凭证,db变量为数据库schema名称。

请确保您的Lambda执行角色中包含rds-db:connect权限,详见此处概述。Lambda控制台会自动为您执行此项操作,帮助您从IAM中检索临时令牌替代本机数据库凭证,轻松对数据库进行身份验证。

确认您的RDS Proxy正在使用IAM身份验证

  • 导航至RDS Proxies控制台。
  • 选择您的Proxy并选择“Actions”按钮,而后选择Modify。
  • 在Connectivity模块下,确认“IAM Authentication”选项被设置为Required。确认IAM身份验证

如果大家选择使用本机数据库凭证接入RDS Proxy,则可以直接跳过以上步骤。

总结

RDS Proxy能够建立起指向数据库的热连接池,借此帮助您管理从Lambda到RDS数据库的大量连接。您的Lambda还可以根据业务需求任意扩展,由RDS Proxy随时为大量并发应用程序请求提供支持。这不仅降低了对于数据库CPU及内存资源的需求,同时也消除了在代码中添加连接管理逻辑的复杂因素。最后,感兴趣的朋友请点击此处参阅RDS Proxy的费率标准。

我们期待您在体验预览版本后的反馈与建议!

 

本篇作者

George Mao

AWS WWCS解决方案架构师,无服务器技术负责人