使用 Node.js、AWS Lambda 和 MongoDB Atlas 进行无服务器开发





0/5 (0投票)
在这篇文章中,我将向您展示如何轻松地将 AWS Lambda Node.js 函数与托管在 MongoDB Atlas 上的 MongoDB 数据库集成,MongoDB Atlas 是 MongoDB 的 DBaaS(数据库即服务)
近年来,开发人员的格局发生了巨大变化。过去,我们开发人员相当普遍地在自己的机器上运行所有工具(数据库、Web 服务器、开发 IDE 等),但像 GitHub、MongoDB Atlas 和 AWS Lambda 这样的云服务正在极大地改变游戏规则。它们使开发人员越来越容易地在任何地方、任何设备上编写和运行代码,几乎没有(或很少)依赖项。
几年前,如果您损坏了机器、丢失了机器或仅仅是没电了,可能需要几天时间才能在新机器上重新启动并正常运行所有必需的设置和配置。
有了云中的开发人员工具,您现在可以以最少的干扰从一台笔记本电脑切换到另一台。但是,这并不意味着一切都很顺利。在云中编写和调试代码仍然具有挑战性;作为开发人员,我们知道拥有本地开发环境,虽然更轻量级,但仍然非常有价值。
这正是我将在本博文中尝试向您展示的内容:如何轻松地将 AWS Lambda Node.js 函数与托管在 MongoDB Atlas 上的 MongoDB 数据库集成,MongoDB Atlas 是 MongoDB 的 DBaaS(数据库即服务)。更具体地说,我们将编写一个简单的 Lambda 函数,该函数将在存储在 MongoDB Atlas 数据库中的集合中创建单个文档。我将一步一步地指导您完成本教程,您应该在一个小时内完成。
让我们开始必要的准备工作
- 拥有一个具有 IAM 和 Lambda 服务管理访问权限的用户的 Amazon Web Services 账户。如果您还没有,请注册一个免费 AWS 账户。
- 一台装有 Node.js 的本地计算机(我告诉过您,我们不会轻易摆脱本地开发环境……)。在下面的教程中,我们将使用 Mac OS X,但在 Windows 或 Linux 上执行相同的任务应该相对容易。
- 一个运行正常的 MongoDB Atlas 集群。如果您还没有,请注册一个免费 MongoDB Atlas 账户,然后只需单击几下即可创建集群。您甚至可以尝试我们的 M0,免费集群套餐,非常适合小型开发项目!
现在您已经了解了要求,让我们来谈谈我们将要采取的具体步骤来编写、测试和部署我们的 Lambda 函数
- MongoDB Atlas 默认是安全的,但是作为应用程序开发人员,我们应该采取一些步骤来确保我们的应用程序符合最小权限访问最佳实践。具体来说,我们将通过创建一个仅具有对我们的应用程序数据库读/写访问权限的 MongoDB Atlas 数据库用户来微调权限。
- 我们将在本地计算机上设置一个 Node.js 项目,并确保在将其部署到 Amazon Web Services 之前在本地端到端测试我们的 lambda 代码。
- 然后,我们将创建我们的 AWS Lambda 函数,并上传我们的 Node.js 项目来初始化它。
- 最后但同样重要的是,我们将对我们的 Lambda 函数进行一些修改,以加密一些敏感数据(例如 MongoDB Atlas 连接字符串),并从函数代码中解密它。
关于 VPC Peering 的简短说明
我不会深入探讨在我们的 MongoDB Atlas 集群和 AWS Lambda 之间设置 VPC Peering 的详细信息,原因如下:1) 我们已经有一个详细的 VPC Peering 文档页面 和一篇 Atlas 中的 VPC Peering 文章,我强烈推荐;2) M0 集群(我用来构建该演示)不支持 VPC Peering。
如果您不设置 VPC Peering,会发生什么情况
- 您将不得不将臭名昭著的
0.0.0.0/0
CIDR 块添加到您的 MongoDB Atlas 集群 IP 白名单中,因为您不知道 AWS Lambda 将使用哪个 IP 地址来调用您的 Atlas 数据库。 - 您的 Lambda 函数和 Atlas 集群之间的带宽使用将收费。
如果您只是想让此演示代码正常运行,那么这两点可能都没问题,但如果您计划部署生产就绪的 Lambda-Atlas 集成,那么设置 VPC Peering 是我们强烈推荐的安全最佳实践。M0 是我们当前的免费产品;请查看我们的 MongoDB Atlas 定价页面 以获取所有可用的实例大小。
提醒一下,对于开发环境和低流量网站,M0、M10 和 M20 实例大小应该没问题。但是,对于支持高流量应用程序或大型数据集的生产环境,建议使用 M30 或更大实例大小。
在 MongoDB Atlas 集群中设置安全性
确保您的应用程序符合最小权限访问策略对于保护您的数据免受恶意威胁至关重要。这就是为什么我们将设置一个特定的数据库用户,该用户将仅对我们的travel数据库具有读/写访问权限。让我们看看如何在 MongoDB Atlas 中实现这一点
在Clusters页面上,选择Security选项卡,然后按Add New User按钮

在打开的弹出窗口中,添加您选择的用户名(例如lambdauser
)

在User Privileges部分,选择Show Advanced Options链接。这使我们能够对特定数据库分配读/写权限,而不是对任何数据库。

然后,您将可以选择分配更精细的访问控制权限

在Select Role下拉列表中,选择readWrite
,并在Database字段中填写您将用于存储文档的数据库名称。我选择将其命名为travel
。

在Password部分,使用Autogenerate Secure Password按钮(并记下生成的密码)或设置您喜欢的密码。然后按Add User按钮确认此用户创建。
我们顺便获取集群连接字符串,因为我们将在 Lambda 代码中连接到 MongoDB Atlas 数据库
假设您已经创建了 MongoDB Atlas 集群,请按集群旁边的Connect按钮

复制URI Connection String值并将其安全地存储在文本文件中。稍后我们将在代码中使用它,以及您刚刚设置的密码。

此外,如果您不使用 VPC Peering,请导航到IP Whitelist选项卡并添加0.0.0.0/0
CIDR 块,或按Allow access from anywhere按钮。提醒一下,强烈不建议在生产环境中使用此设置,这可能会使您的 MongoDB Atlas 集群容易受到恶意攻击。

创建本地 Node.js 项目
虽然 Lambda 函数支持多种语言,但我选择使用 Node.js,这得益于 JavaScript 日益增长的普及度 作为一种多功能编程语言,以及 MEAN 和 MERN 堆栈(分别是MongoDB、Express.js、Angular/React、Node.js 的首字母缩写 - 请查看 Andrew Morgan 关于此主题的出色面向开发人员的博客系列)。此外,老实说,我喜欢它是一种解释性的、轻量级的语言,不需要繁重的开发工具和编译器。
现在是时候写代码了,所以让我们继续使用 Node.js 作为我们 Lambda 函数的首选语言。
首先创建一个文件夹,例如 lambda-atlas-create-doc
mkdir lambda-atlas-create-doc && cd lambda-atlas-create-doc
接下来,从 Terminal 控制台运行以下命令,使用 package.json
文件初始化我们的项目
npm init
系统会提示您配置一些字段。我将把它们留给您的创意,但请注意,我选择将入口点设置为*app.js*(而不是默认的*index.js*),所以您也可以这样做。
我们需要使用 MongoDB Node.js 驱动程序,以便从 Lambda 函数连接到我们的 MongoDB 数据库(在 Atlas 上),因此让我们通过在项目根目录中运行以下命令来安装它
npm install mongodb --save
我们还希望在本地编写和测试我们的 Lambda 函数,以加快开发速度并简化调试,因为每次在 Amazon Web Services 中实例化 lambda 函数都不算快(而且几乎没有调试功能,除非您是console.log()
函数的粉丝)。我选择使用 lambda-local 包,因为它支持环境变量(我们稍后将使用它)
(sudo) npm install lambda-local -g
创建一个*app.js*文件。这将是包含我们的 lambda 函数的文件
touch app.js
现在您已经导入了所有必需的依赖项并创建了 Lambda 代码文件,请在您选择的代码编辑器(Atom、Sublime Text、Visual Studio Code…)中打开*app.js*文件,并使用以下代码片段对其进行初始化
'use strict' var MongoClient = require('mongodb').MongoClient; let atlas_connection_uri; let cachedDb = null; exports.handler = (event, context, callback) => { var uri = process.env['MONGODB_ATLAS_CLUSTER_URI']; if (atlas_connection_uri != null) { processEvent(event, context, callback); } else { atlas_connection_uri = uri; console.log('the Atlas connection string is ' + atlas_connection_uri); processEvent(event, context, callback); } }; function processEvent(event, context, callback) { console.log('Calling MongoDB Atlas from AWS Lambda with event: ' + JSON.stringify(event)); }
让我们稍微停顿一下,注释掉上面的代码,因为您可能已经注意到一些特殊的结构
- 该文件编写方式完全符合 Lambda 代码 Amazon Web Services 的预期(例如,带有“exports.handler”函数)。这是因为我们使用 lambda-local 在本地测试我们的 lambda 函数,它方便地让我们编写的代码与 AWS Lambda 的要求完全一致。稍后将对此进行更多介绍。
- 我们正在声明将帮助我们连接和查询 MongoDB 数据库的 MongoDB Node.js 驱动程序。
- 另请注意,我们将
cachedDb
对象声明在 handler 函数的*外部*。顾名思义,这是一个我们计划在 AWS Lambda 为我们的函数实例化的底层容器的生命周期内缓存的对象。这使我们能够节省宝贵的毫秒(甚至几秒钟)用于在 Lambda 和 MongoDB Atlas 之间建立数据库连接。有关更多信息,请参阅我后续的博文 如何使用 MongoDB Atlas 和 Node.js 优化 AWS Lambda 性能。 - 我们使用一个名为
MONGODB_ATLAS_CLUSTER_URI
的环境变量来传递我们的 Atlas 数据库的 uri 连接字符串,主要是出于安全原因:我们显然不希望将此 uri 与用户名和密码等非常敏感的信息硬编码在我们的函数代码中。由于 AWS Lambda 自 2016 年 11 月起支持环境变量(正如 lambda-local NPM 包所做的那样),我们不使用它们就显得失职了。 - 函数代码看起来有点复杂,带有看似无用的 if-else 语句和 processEvent 函数,但当我们添加使用 AWS Key Management Service (KMS) 的解密例程时,这一切都将变得清晰。实际上,我们不仅希望将 MongoDB Atlas 连接字符串存储在环境变量中,还希望对其进行加密(使用 AWS KMS),因为它包含高度敏感的数据(请注意,即使您拥有免费的 AWS 账户,使用 AWS KMS 时也可能会产生费用)。
现在我们完成了代码注释,让我们创建一个*event.json*文件(在项目根目录下)并用以下数据填充它
{ "address" : { "street" : "2 Avenue", "zipcode" : "10075", "building" : "1480", "coord" : [ -73.9557413, 40.7720266 ] }, "borough" : "Manhattan", "cuisine" : "Italian", "grades" : [ { "date" : "2014-10-01T00:00:00Z", "grade" : "A", "score" : 11 }, { "date" : "2014-01-16T00:00:00Z", "grade" : "B", "score" : 17 } ], "name" : "Vella", "restaurant_id" : "41704620" }
(如果您想知道,该 JSON 文件就是我们将发送到 MongoDB Atlas 来创建我们的 BSON 文档)
接下来,请确保您已正确设置,方法是在 Terminal 控制台中运行以下命令
lambda-local -l app.js -e event.json -E {\"MONGODB_ATLAS_CLUSTER_URI\":\"mongodb://lambdauser:$PASSWORD@lambdademo-shard-00-00-7xh42.mongodb.net:27017\,lambdademo-shard-00-01-7xh42.mongodb.net:27017\,lambdademo-shard-00-02-7xh42.mongodb.net:27017/$DATABASE?ssl=true\&replicaSet=lambdademo-shard-0\&authSource=admin\"}
如果您想使用您自己的集群URI Connection String进行测试(我确定您会的),请不要忘记转义E
参数中的双引号、逗号和与号字符,否则 lambda-local 将会抛出错误(您还应该将$PASSWORD
和$DATABASE
关键字替换为您自己的值)。
运行本地命令后,您应该会看到以下控制台输出

如果您收到错误,请检查您的连接字符串以及双引号/逗号/与号转义(如上所述)。
现在,让我们通过自定义processEvent()
函数并添加createDoc()
函数来深入研究我们的函数代码
function processEvent(event, context, callback) { console.log('Calling MongoDB Atlas from AWS Lambda with event: ' + JSON.stringify(event)); var jsonContents = JSON.parse(JSON.stringify(event)); //date conversion for grades array if(jsonContents.grades != null) { for(var i = 0, len=jsonContents.grades.length; i < len; i++) { //use the following line if you want to preserve the original dates //jsonContents.grades[i].date = new Date(jsonContents.grades[i].date); //the following line assigns the current date so we can more easily differentiate between similar records jsonContents.grades[i].date = new Date(); } } //the following line is critical for performance reasons to allow re-use of database connections across calls to this Lambda function and avoid closing the database connection. The first call to this lambda function takes about 5 seconds to complete, while subsequent, close calls will only take a few hundred milliseconds. context.callbackWaitsForEmptyEventLoop = false; try { if (cachedDb == null) { console.log('=> connecting to database'); MongoClient.connect(atlas_connection_uri, function (err, client) { cachedDb = client.db('travel'); return createDoc(cachedDb, jsonContents, callback); }); } else { createDoc(cachedDb, jsonContents, callback); } } catch (err) { console.error('an error occurred', err); } } function createDoc (db, json, callback) { db.collection('restaurants').insertOne( json, function(err, result) { if(err!=null) { console.error("an error occurred in createDoc", err); callback(null, JSON.stringify(err)); } else { console.log("Kudos! You just created an entry into the restaurants collection with id: " + result.insertedId); callback(null, "SUCCESS"); } //we don't need to close the connection thanks to context.callbackWaitsForEmptyEventLoop = false (above) //this will let our function re-use the connection on the next called (if it can re-use the same Lambda container) //db.close(); }); };
请注意连接到 MongoDB Atlas 数据库和插入文档有多么容易,以及我添加的一小段代码,用于将 JSON 日期(格式化为符合 ISO 标准的字符串)转换为 MongoDB 可以存储为BSON 日期的实际 JavaScript 日期。
您可能还注意到我的性能优化注释和context.callbackWaitsForEmptyEventLoop = false
的调用。如果您有兴趣了解它们的含义(我认为您应该!),请参阅我后续的博文 如何使用 MongoDB Atlas 和 Node.js 优化 AWS Lambda 性能。
您现在已准备好在本地完全测试您的 Lambda 函数。使用与之前相同的 lambda-local 命令,希望您能收到一条很好的“Kudos”成功消息

如果在本地机器上一切顺利,让我们将本地 Node.js 项目发布为一个新的 Lambda 函数!
创建 Lambda 函数
我们要做的第一件事是压缩我们的 Node.js 项目,因为我们不会在 Lambda 代码编辑器中编写 Lambda 代码函数。相反,我们将选择 zip 上传方法,将我们的代码推送到 AWS Lambda。
我在 Terminal 控制台中使用了zip
命令行工具,但任何方法都可以(只要您压缩顶级文件夹内的文件,而不是顶级文件夹本身!)
zip -r archive.zip node_modules/ app.js package.json
接下来,登录 AWS 控制台并导航到IAM Roles页面,创建一个具有AWSLambdaBasicExecutionRole权限策略的角色(例如LambdaBasicExecRole
)

现在让我们导航到 AWS Lambda 页面。单击Get Started Now(如果您从未创建过 Lambda 函数)或单击Create a Lambda function按钮。我们不会使用任何蓝图,也不会配置任何触发器,因此请直接选择左侧导航栏中的Configure function

在Configure function页面上,为您的函数输入一个名称(例如MongoDB_Atlas_CreateDoc
)。运行时会自动设置为Node.js 4.3
,这对我们来说很完美,因为这是我们将使用的语言。在Code entry type列表中,选择Upload a .ZIP file
,如下面的屏幕截图所示

单击Upload按钮,然后选择您之前创建的已压缩的 Node.js 项目文件。
在Lambda function handler and role部分,将Handler字段值修改为app.handler
(为什么?这里有一个提示:我为我的 Lambda 函数代码使用了*app.js*文件,而不是*index.js*文件……)并选择我们刚刚创建的现有LambdaBasicExecRole
角色

在Advanced Settings部分,您可能想将Timeout值增加到 5 或 10 秒,但这总是可以稍后调整的。将 VPC 和 KMS 密钥字段保留为默认值(除非您想使用 VPC 和/或 KMS 密钥),然后按Next。
最后,查看您的 Lambda 函数,然后在底部按Create function。恭喜,您的 Lambda 函数已上线,您应该会看到一个类似于以下屏幕截图的页面

但您还记得我们对环境变量的使用吗?现在是时候配置它们并使用AWS Key Management Service来保护它们!
配置和保护您的 Lambda 环境变量
在 Lambda 函数的Code选项卡中向下滚动,并创建一个具有以下属性的环境变量
名称 | 值 |
---|---|
MONGODB_ATLAS_CLUSTER_URI | YOUR_ATLAS_CLUSTER_URI_VALUE |

此时,您可以按页面顶部的Save and test按钮,但为了额外的(推荐的)安全性,我们将加密该连接字符串。
勾选Enable encryption helpers复选框,如果您已经创建了一个加密密钥,请选择它(否则,您可能需要创建一个 - 这非常简单)

接下来,选择 MONGODB_ATLAS_CLUSTER_URI 变量的Encrypt按钮

回到内联代码编辑器,在顶部添加以下行
const AWS = require('aws-sdk');
并将“exports.handler”方法中“else”语句的内容替换为以下代码
const kms = new AWS.KMS(); kms.decrypt({ CiphertextBlob: new Buffer(uri, 'base64') }, (err, data) => { if (err) { console.log('Decrypt error:', err); return callback(err); } atlas_connection_uri = data.Plaintext.toString('ascii'); processEvent(event, context, callback); });
(希望我们最初编写的那些复杂的代码现在有意义了!)
如果您想查看我使用的整个函数代码,请查看以下Gist。对于 Git 粉丝,完整的 Node.js 项目源代码也可在 GitHub 上找到。
现在按Save and test按钮,在Input test event文本编辑器中,粘贴我们*event.json*文件的内容

滚动并按Save and test按钮。
如果一切配置正确,您应该会在 Lambda 日志输出中收到以下成功消息

恭喜!您可以在继续阅读之前享受几分钟的成功。
接下来是什么?
我希望这个 AWS Lambda-MongoDB Atlas 集成教程能为您提供入门第一个 Lambda 项目的正确步骤。您现在应该能够在本地编写和测试 Lambda 函数,并将敏感数据(例如您的 MongoDB Atlas 连接字符串)安全地存储在 AWS KMS 中。
那么接下来您可以做什么?
- 如果您还没有 MongoDB Atlas 账户,现在创建还不算晚!
- 如果您不熟悉 MongoDB Node.js 驱动程序,请查看我们的 Node.js 驱动程序文档,了解如何充分利用 MongoDB API。此外,我们还为刚开始使用 MongoDB 的 Node.js 开发人员提供了一个在线 Node.js 课程。
- 了解如何可视化使用 Lambda 函数创建的数据,下载 MongoDB Compass 并阅读使用 MongoDB Compass 可视化您的数据,了解如何将其连接到 MongoDB Atlas。
- 计划构建许多 Lambda 函数?了解如何使用 AWS Step Functions 编排它们,请阅读我们的将 MongoDB Atlas、Twilio 和 AWS Simple Email Service 与 AWS Step Functions 集成文章。
- 了解如何在更复杂的场景中集成 MongoDB 和 AWS Lambda,请查看我们更高级的博文:使用 AWS Lambda 和 MongoDB Atlas 开发 Facebook 聊天机器人。
当然,请随时在下面的评论中提出您的任何问题或留下您的反馈。祝您编码愉快!