使用 AWS 进行无服务器速成课程 - 使用 Lambda 和 Aurora 无服务器构建 API 启动 MySQL 更改用户身份验证 根据堆栈溢出需要编辑密码身份验证 确保更改 db.js DB_PASSWORD

2025-06-04

AWS 无服务器速成课程 - 使用 Lambda 和 Aurora 无服务器构建 API

启动 MySQL

更改用户授权

每个堆栈溢出需要编辑密码验证

确保更改 db.js DB_PASSWORD

自从 AWS正式发布 AWS RDS Aurora 的无服务器选项以来,我一直翘首以盼,热切地期待着各种可能性。这将意味着构建无服务器架构的突破。不再需要使用 SQL 管理连接池。不再需要担心可用连接数上限。这将使使用无服务器进行数据库密集型操作变得可行。

不久前,我写了一篇文章,解释如何使用 MongoDB 构建无服务器 API。但是,我发现了一种模式。系统中的吞吐量较高,即使数据库实例可以承担负载,也会导致连接断开,因为可用连接数有限。

TL;DR

我已经将 MongoDB 的示例重构为使用 SQL。无论您使用哪种数据库提供商,都无需担心,但本文展示的示例使用的是AWS Aurora Serverless。您可以在此处查看代码,或者,如果您想学习如何自行设置所有内容,请继续阅读。

我们会做什么?

本文将向您展示如何将 MySQL 数据库即服务连接到无服务器 API。我们将使用 AWS RDS 创建一个数据库集群,并配置 Aurora Serverless。这种设置的优点在于,您可以根据服务使用量付费,甚至包括数据库!这是理想的无服务器场景。

如果您是新手该怎么办?

让我们分解一下基础知识。我们将使用AWS Lambda作为计算服务。AWS Lambda 函数本质上是一个容器。一旦 Lambda 被调用,容器就会启动并运行代码。这时,我们需要初始化数据库连接,也就是函数第一次被调用时,也就是容器第一次初始化的时候。之后对 Lambda 函数的每个请求都将使用现有的数据库连接。分解一下,听起来很简单。让我们开始吧。

配置项目

我假设你已经对无服务器框架有了基本的了解。我还希望你已经设置了一个 AWS 账户。如果没有,请查看此处

1.创建服务

与往常一样,我们需要一个新的服务来保存我们所有的代码。

$ sls create -t aws-nodejs -p web-api && cd web-api
Enter fullscreen mode Exit fullscreen mode

运行此命令,您将获得一个不错的样板,可以开始开发您的函数。它将设置基本函数和事件触发器。您还会看到我们web-api立即进入了目录。我们进入此目录是为了设置所需的依赖项。

2.安装模块

总共我们只需要三个依赖项。MySQL ORM 调用 Sequelize 来处理连接和映射关系,并serverless-offline用于本地开发。

确保您位于web-api目录中。首先,安装serverless-offline,然后mysql2安装sequelize

$ npm init -y
$ npm i --save-dev serverless-offline
$ npm i --save mysql2 sequelize
Enter fullscreen mode Exit fullscreen mode

就是这样,让我们​​暂时离开终端,跳转到 AWS 来创建数据库集群。

3. 在 AWS RDS 上创建数据库

我一直讨厌通过 Web 控制台设置资源,但这却是必要之恶。请耐心等待,跳转到您的 AWS 账户并选择RDS。您将进入类似这样的页面。

rds-1

单击橙色的“创建数据库”按钮并继续操作。

rds-select-引擎

首先,您需要选择要使用的引擎。选择 Amazon Aurora,然后单击“下一步”。

rds-create-1

指定引擎后,将 Aurora 配置为无服务器,为集群添加名称并配置主用户名和密码。

rds-create-2

只需再配置一组高级设置,就可以完成了。

rds-advanced-config-1

您将在此处设置容量和网络。由于 Aurora Serverless 位于 VPC 内,因此您必须配置访问权限。此处您将选择默认 VPC。

rds-advanced-config-2

通过附加配置,您可以设置您的备份。完成所有设置后,点击橙色的“创建数据库”按钮。

最后,它将开始配置集群。您将被重定向并看到类似这样的输出。

rds-cluster-created-1

但是,我们感兴趣的是细节部分。

rds-cluster-created-2

在这里,您可以访问安全组和子网。您需要将它们添加到AWS Lambda函数中,以便它们能够与数据库通信。下一节将详细介绍。

注意:如果您想访问 AWS Aurora Serverless 集群,请按照本教程了解如何使用 CLI 进行操作,或者按照 AWS 官方教程通过Cloud9进行连接。您需要连接到集群并手动创建数据库,因为 Sequelize 无法为您创建它们。

Statsbot的工程师们向我展示了AWS RDS在处理海量数据方面有多么出色。有传言说,他们正在开发一个名为Cube.js的完全无服务器开源分析框架。我迫不及待地想亲自使用它。希望 Aurora Serverless 能有所帮助。

编写代码

在 AWS 上配置数据库资源总是一件很麻烦的事,尤其是新推出的无服务器数据库产品。幸运的是,配置部分已经完成了。现在我们需要在serverless.yml文件中编写无服务器资源的配置,并将实际的 CRUD 方法添加到handler.js中。

4.配置 YAML

无服务器框架在设置初始项目结构方面非常有帮助。它可以搭建起您刚开始工作时所需的几乎所有基础框架。返回到您创建无服务器项目的目录,并使用您喜欢的代码编辑器打开它。

首先打开serverless.yml文件,稍作休息。在这里,您会看到一堆带有示例配置的注释。我喜欢简洁的代码,所以我们先删除它们,然后粘贴这段代码。

service: web-api

custom:
  secrets: ${file(secrets.json)}

provider:
  name: aws
  runtime: nodejs8.10
  timeout: 30
  stage: ${self:custom.secrets.NODE_ENV}
  environment: 
    NODE_ENV: ${self:custom.secrets.NODE_ENV}
    DB_NAME: ${self:custom.secrets.DB_NAME}
    DB_USER: ${self:custom.secrets.DB_USER}
    DB_PASSWORD: ${self:custom.secrets.DB_PASSWORD}
    DB_HOST: ${self:custom.secrets.DB_HOST}
    DB_PORT: ${self:custom.secrets.DB_PORT}
  vpc:
    securityGroupIds:
      - ${self:custom.secrets.SECURITY_GROUP_ID}
    subnetIds:
      - ${self:custom.secrets.SUBNET1_ID}
      - ${self:custom.secrets.SUBNET2_ID}
      - ${self:custom.secrets.SUBNET3_ID}
      - ${self:custom.secrets.SUBNET4_ID}

functions:
  healthCheck:
    handler: handler.healthCheck
    events:
      - http:
          path: /
          method: get
          cors: true
  create:
    handler: handler.create
    events:
      - http:
          path: notes
          method: post
          cors: true
  getOne:
    handler: handler.getOne
    events:
      - http:
          path: notes/{id}
          method: get
          cors: true
  getAll:
    handler: handler.getAll
    events:
     - http:
         path: notes
         method: get
         cors: true
  update:
    handler: handler.update
    events:
     - http:
         path: notes/{id}
         method: put
         cors: true
  destroy:
    handler: handler.destroy
    events:
     - http:
         path: notes/{id}
         method: delete
         cors: true

plugins:
  - serverless-offline
Enter fullscreen mode Exit fullscreen mode

让我们逐一分析一下。此provider部分包含有关云提供商的所有基本信息,以及运行时、函数超时、环境变量和 VPC 配置的数据。这里需要设置 VPC,因为 AWS Aurora Serverless 只能在 VPC 后运行,因此您需要将AWS Lambda 函数放在同一个 VPC 中才能访问数据库。

继续,custom顶部有一个部分,我们在其中加载环境变量,然后将它们传递给该environment部分。

最后,我们来谈谈函数部分。我们总共添加了 6 个函数:healthCheckcreategetOnegetAllupdatedestroy。它们都指向handler.js文件中同名的导出函数。它们的事件触发器与由AWS API Gateway处理的 HTTP 事件挂钩。这意味着,对指定路径的 HTTP 请求将触发引用的函数。

差不多就是这样了,最后一件事是添加一个插件部分和serverless-offline。我们上面安装了这个模块,我们将在部署到 AWS 之前用它来测试服务。

5. 添加机密

您在serverless.yml中看到我们引用了一个 secrets 文件。在继续下一步之前,我们需要创建它并确保它能够正确加载我们的 secrets。

在项目的根目录中创建一个secrets.json并粘贴此代码片段。

{
  "DB_NAME": "test",
  "DB_USER": "root",
  "DB_PASSWORD": "root",
  "DB_HOST": "127.0.0.1",
  "DB_PORT": 3306,
  "NODE_ENV": "dev",
  "SECURITY_GROUP_ID": "sg-xx",
  "SUBNET1_ID": "subnet-xx",
  "SUBNET2_ID": "subnet-xx",
  "SUBNET3_ID": "subnet-xx",
  "SUBNET4_ID": "subnet-xx"
}
Enter fullscreen mode Exit fullscreen mode

此设置在本地开发环境中运行良好。一旦您想要将其部署到 AWS,则必须添加您自己的密钥。AWS Aurora Serverless 将输出如上图所示的所有内容。这里重要的密钥是数据库连接参数、安全组 ID 和子网 ID。

关于 YAML 的所有内容就到这里。接下来我们来试试handler.js吧。终于,来点真正的代码吧!

6. 充实功能

首先,我们将定义 6 个函数。它们将描述我们想要的初始布局和行为。完成后,我们将使用Sequelize创建数据库连接和数据库交互逻辑。

切换到handler.js文件。你会看到默认的样板代码。猜怎么着?删除所有内容,然后添加下面的代码。

module.exports.healthCheck = async () => {
  await connectToDatabase()
  console.log('Connection successful.')
  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'Connection successful.' })
  }
}

module.exports.create = async (event) => {
  try {
    const { Note } = await connectToDatabase()
    const note = await Note.create(JSON.parse(event.body))
    return {
      statusCode: 200,
      body: JSON.stringify(note)
    }
  } catch (err) {
    return {
      statusCode: err.statusCode || 500,
      headers: { 'Content-Type': 'text/plain' },
      body: 'Could not create the note.'
    }
  }
}

module.exports.getOne = async (event) => {
  try {
    const { Note } = await connectToDatabase()
    const note = await Note.findById(event.pathParameters.id)
    if (!note) throw new HTTPError(404, `Note with id: ${event.pathParameters.id} was not found`)
    return {
      statusCode: 200,
      body: JSON.stringify(note)
    }
  } catch (err) {
    return {
      statusCode: err.statusCode || 500,
      headers: { 'Content-Type': 'text/plain' },
      body: err.message || 'Could not fetch the Note.'
    }
  }
}

module.exports.getAll = async () => {
  try {
    const { Note } = await connectToDatabase()
    const notes = await Note.findAll()
    return {
      statusCode: 200,
      body: JSON.stringify(notes)
    }
  } catch (err) {
    return {
      statusCode: err.statusCode || 500,
      headers: { 'Content-Type': 'text/plain' },
      body: 'Could not fetch the notes.'
    }
  }
}

module.exports.update = async (event) => {
  try {
    const input = JSON.parse(event.body)
    const { Note } = await connectToDatabase()
    const note = await Note.findById(event.pathParameters.id)
    if (!note) throw new HTTPError(404, `Note with id: ${event.pathParameters.id} was not found`)
    if (input.title) note.title = input.title
    if (input.description) note.description = input.description
    await note.save()
    return {
      statusCode: 200,
      body: JSON.stringify(note)
    }
  } catch (err) {
    return {
      statusCode: err.statusCode || 500,
      headers: { 'Content-Type': 'text/plain' },
      body: err.message || 'Could not update the Note.'
    }
  }
}

module.exports.destroy = async (event) => {
  try {
    const { Note } = await connectToDatabase()
    const note = await Note.findById(event.pathParameters.id)
    if (!note) throw new HTTPError(404, `Note with id: ${event.pathParameters.id} was not found`)
    await note.destroy()
    return {
      statusCode: 200,
      body: JSON.stringify(note)
    }
  } catch (err) {
    return {
      statusCode: err.statusCode || 500,
      headers: { 'Content-Type': 'text/plain' },
      body: err.message || 'Could destroy fetch the Note.'
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

我理解你觉得这段代码有点长。别担心,这只是 6 个基本async函数。该connectToDatabase()函数解析为一个包含所有数据库模型的对象。本教程中我们只实现该Note模型。该函数背后的逻辑是连接到 SQL 数据库,缓存连接,并确保在现有连接处于活动状态时永不重试连接。

但是等等,我们还没有定义或创建任何这些。好吧,我是故意这么做的,首先我想让你明白这并不复杂,与使用 Node.js 和 Express 创建 API 也没什么不同。

7.添加数据库连接

下一步是添加数据库连接。当 AWS Lambda 函数首次调用时(称为冷启动),AWS 会启动一个容器来运行代码。这时我们就可以连接到数据库了。所有后续请求都将使用现有的数据库连接。从概念上讲,这很容易理解,但当我们需要在代码中理解它时,就会变得非常棘手。就这样吧!

在服务的根目录中,与handler.js相邻的位置创建一个新文件。给它一个合乎逻辑的名称db.js,并添加以下代码。

const Sequelize = require('sequelize')
const NoteModel = require('./models/Note')
const sequelize = new Sequelize(
  process.env.DB_NAME,
  process.env.DB_USER,
  process.env.DB_PASSWORD,
  {
    dialect: 'mysql',
    host: process.env.DB_HOST,
    port: process.env.DB_PORT
  }
)
const Note = NoteModel(sequelize, Sequelize)
const Models = { Note }
const connection = {}

module.exports = async () => {
  if (connection.isConnected) {
    console.log('=> Using existing connection.')
    return Models
  }

  await sequelize.sync()
  await sequelize.authenticate()
  connection.isConnected = true
  console.log('=> Created a new connection.')
  return Models
}
Enter fullscreen mode Exit fullscreen mode

在顶部,我们需要SequelizeNoteModel

注意:我们尚未创建模型,但请稍等,我们将在此部分之后立即完成该操作。

然后我们初始化sequelize,传入连接变量并建立连接。调用NoteModel将初始化模型,然后将其传递给Models常量。任意connections对象仅用作连接的缓存。确保在非必要情况下不要同步数据库。await使用.sync().authenticate()方法的第二个原因是确保在初始函数调用期间(处理任何业务逻辑之前)建立数据库连接。

创建db.js文件后,我们在handler.js中引入它。只需将此代码片段添加到处理程序的顶部即可。

const connectToDatabase = require('./db') // initialize connection

// simple Error constructor for handling HTTP error codes
function HTTPError (statusCode, message) {
  const error = new Error(message)
  error.statusCode = statusCode
  return error
}
Enter fullscreen mode Exit fullscreen mode

8. 添加 Note 模型

跳回到handler.js。你可以看到我们正在调用const { Note } = await connectToDatabase()函数来检索数据,但是没有定义 Note 模型。好了,现在是时候了。

在服务根目录中创建一个新文件夹,并将其命名为models。在其中创建另一个文件并将其命名为Note.js。这只是一个简单的模型定义。

module.exports = (sequelize, type) => {
  return sequelize.define('note', {
    id: {
      type: type.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    title: type.STRING,
    description: type.STRING
  })
}
Enter fullscreen mode Exit fullscreen mode

就这样。是时候尝试一下了。

注意:请确保将您的添加secrets.json到您的.gitignore文件中。

进行一些测试怎么样?

我们已经准备好测试 API 了。为了保险起见,我们先在本地运行一下。需要注意的一点是,运行时需要添加一个标志serverless-offline

$ sls offline start --skipCacheInvalidation
Enter fullscreen mode Exit fullscreen mode

注意: Serverless Offline 默认在每次运行时都会使 Node 所需的缓存失效,我们添加了此标志来禁用它。在 Node.js 中,当您使用require()一个模块时,它会存储该模块的缓存版本,这样所有后续调用require()都无需从文件系统重新加载该模块。

在终端中运行该命令后,您应该会看到类似这样的内容。

sls-离线启动

所有路线均已启动并运行。我使用Insomniahttp://localhost:3000/notes创建了一个JSON 格式的POST 请求。

胰岛素-1

检查终端,你会看到=> using new database connectionget logs 消息,这意味着初始数据库连接已建立。再次发送 POST 请求,你会看到=> using existing database connectionget logs 消息。

太棒了,添加新笔记成功了。让我们使用getAll方法检索刚刚添加的所有笔记。

胰岛素-2

亲自尝试其他端点。玩完后再回来这里。

部署到 AWS

现在到了棘手的部分。请务必记下您将在 AWS Aurora Serverless 控制台中获取的所有数据库参数,当然还有安全组 ID 和子网 ID。通常,您会有一个安全组和三个子网。但是,实际数量可能会有所不同,所以不用担心。获取这些值后,将它们添加到您的secrets.json。就这样!您已准备好部署。

无服务器框架让部署变得快速而轻松。您只需运行一个命令即可。

$ sls deploy
Enter fullscreen mode Exit fullscreen mode

它将自动在 AWS 上配置资源,打包并将所有代码推送到 S3,然后从 S3 发送到 Lambda 函数。终端应该会显示类似如下的输出。

sls部署

注意:您可以使用提供的端点重复上述测试过程。但是,请耐心等待冷启动时间。AWS Aurora Serverless 启动可能需要 10 秒左右。VPC 中的 AWS Lambda 也会出现类似的问题。

这就是部署过程的全部内容。很简单吧?这就是我如此热爱无服务器框架的原因。

总结

这是一个不断发展的架构。随着无服务器关系数据库的兴起,创建完全无服务器基础设施的可能性无穷无尽。我已尽力解释创建合适 API 的过程。希望您能从中获益良多。欢迎深入探索无服务器架构及其带来的一切可能性!

当然,这里再次分享一下我的代码库,如果你想让更多人在 GitHub 上看到它,就点个 Star 吧。如果你想阅读我之前关于无服务器的一些思考,请访问我的个人资料订阅我的无服务器新闻通讯!

我彻底体验了 AWS Aurora Serverless。希望大家喜欢阅读这篇文章,就像我喜欢写这篇文章一样。如果喜欢,就拍拍那只小独角兽,这样 dev.to 上会有更多人看到这篇文章。如果你需要一个无服务器 SQL 分析框架,别忘了给 Cube.js 的朋友们点个赞!下次再见,保持好奇心,玩得开心。


免责声明:Tracetest赞助了这篇博文。使用可观察性可以将测试创建和故障排除的时间和精力减少 80%。


文章来源:https://dev.to/adnanrahic/a-crash-course-on-serverless-with-aws---building-apis-with-lambda-and-aurora-serverless-3fn3
PREV
😱 Static HTML Export with i18n compatibility in Next.js 😱 BUT, you may have heard about this: So what can we do now? The recipe The outcome The voluntary part 🎉🥳 Congratulations 🎊🎁 👍 Looking for an optimized Next.js translations setup?
NEXT
在 Laravel 中实现 UUID 主键,以及它的好处