在 Azure 上创建完整的无服务器云原生应用程序
在本文中,我们将演示如何更进一步,构建一个完整的云原生应用程序,该应用程序由多个 Azure Functions 托管的微服务组成。
在本系列的上一篇文章中,我们在 Azure Function 内部创建了一个简单的微服务。让我们进一步扩展这个概念,使用多个 Azure Functions 构建一个基于微服务的后端应用程序。
我们将简要介绍如何设计我们的应用程序、可以使用哪些组件、如何使用 TypeScript 构建函数以及如何使用 Azure DevOps 部署函数。到本文结束时,您将拥有许多可在应用程序中使用的 API,所有这些 API 都由 Azure Functions 提供支持。
背景
在本例中,我们使用 TypeScript 构建了一个简单的任务管理应用程序。这包括创建、更新、完成和删除任务,以及跟踪任务所处的阶段(未开始、进行中或已完成)。
我们不会担心实际的前端应用程序组件,只设计和交付后端 API 以供任何前端应用程序使用。我们也不会过于关注实现基于用户的安全或角色。
我们将代码存储在 Azure DevOps 中,并使用自动化管道将 Azure Functions 直接构建和部署到我们的 Azure 租户。
设计
我们应用程序设计的基本经验法则是每个函数一个指令。对于我们的示例应用程序,我们开发的指令是:
- 获取任务 – 检索单个任务
- 列出任务 – 列出与用户关联的所有任务
- 创建任务 – 为用户创建新任务
- 更新任务 – 更新用户的现有任务
- 删除任务 – 删除任务
这涵盖了基本任务管理应用程序的所有组件。一旦我们确定了基本功能,我们就可以查看一些额外的组件,例如 API 管理。为了简单起见,我们不会实现它们。
考虑到这一点,以下是我们将交付的组件的高级视图。
项目设置
让我们首先使用 Visual Studio Code 在 Azure DevOps 中设置我们的项目。您应该已经拥有一个 Azure DevOps 帐户和Visual Studio Code。首先,打开Azure DevOps并登录。创建一个具有唯一名称的新项目,例如 TaskManager,接受默认值,然后单击“创建”。
项目创建后,在“Repos”>“Files”下有“Clone in VS Code”选项。这会使用 Git 下载您的项目并在 VS Code 中打开结果。
完成后,当您的空白项目打开时,如果您尚未安装它,请搜索 Microsoft 提供的名为 Azure Functions 的 VS Code 扩展。安装后,您的左侧工具栏上会有一个 Azure 图标,使您可以在当前工作区中创建新的 Functions 项目。
最后,要在本地运行函数,请安装Azure Functions Core Tools以及存储模拟器,它们应该都是 Azure SDK 的一部分。
保存数据
首先,我们在 VS Code 中创建一个新的 TypeScript Functions 项目和 CreateTask 函数。此函数应只使用 HTTP 触发器,并将授权级别设置为匿名。这个基础项目包含了我们部署单个 Azure Function 所需的一切,类似于我们之前的文章。当我们运行此函数(使用 F5)时,我们会得到一个默认的返回语句,它会在 GET 和 POST 上触发。
我们将修改此函数,以使我们能够创建新任务并将其存储在Azure 表存储中。在此之前,我们需要配置函数和一些依赖项。运行以下 npm install 命令以安装两个额外的库
npm install azure-storage
npm install guid-typescript
我们还将更新 function.json 文件中的函数签名,使其只接受 POST 请求。我们的函数需要接收一个任务负载,并在我们的表存储帐户中创建、更新或删除该任务。代码如下:
import { AzureFunction, Context, HttpRequest } from "@azure/functions"
import * as AzureStorage from "azure-storage"
import { Guid } from "guid-typescript";
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
context.log('Create Task Triggered');
if(!req.body.username || req.body.username === ""){
context.res.status(400).json({ error: "No Username Defined" });
return;
}
var tableSvc = AzureStorage.createTableService();
var tableName = "Tasks";
var taskID = Guid.create().toString();
var entGen = AzureStorage.TableUtilities.entityGenerator;
var task = {
PartitionKey: entGen.String(req.body.username),
RowKey: entGen.String(taskID),
name: entGen.String(req.body.name),
dueDate: entGen.DateTime(new Date(Date.UTC(
req.body.dueYear, req.body.dueMonth, req.body.dueDay))),
completed: entGen.String(req.body.completed)
};
await apiFunctionWrapper(tableSvc, tableName, task).then(value => {
return context.res.status(201).json({ "taskId": req.body.taskID,
"result": value });
}, error => {
context.log(error);
return context.res.status(500).json({ taskId: req.body.taskID,
error: error });
});
};
export default httpTrigger;
function apiFunctionWrapper(tableSvc, tableName, task) {
return new Promise((res,err) => {
tableSvc.insertEntity(tableName, task, function (error, result) {
if (!error) {
return res(result);
} else {
return err(error);
}
});
});
}
此函数现在将接受 POST 正文中的任务信息负载,然后在表存储中创建任务。
最后一步是提供我们的表存储帐户的连接字符串。为了确保此函数正常工作,我们使用Azurite模拟器,并在 local.settings.json 文件中指定环境变量 AZURE_STORAGE_CONNECTION_STRING
。
现在,当我们在 VSCode 中运行我们的函数(使用 F5)时,它会启动并在 https://:7071/api/UpdateData 侦听任务。
我们可以使用 Postman 等工具测试我们的函数,方法是发送如下 JSON 负载:
{
"username" : "testuser",
"name" : "Finish Function Example",
"dueYear" : "2021",
"dueMonth" : "04",
"dueDay" : "01",
"completed" : "0"
}
现在当我们查看本地存储实例时,我们会看到为我们的用户添加到表中的任务实体。
在我们继续下一个函数之前,让我们将更改提交到 Azure DevOps,以便保存它们。切换到“源代码管理”部分,暂存并提交您的更改,然后将它们推送到 Azure DevOps。
获取函数
既然我们可以在数据存储中保存数据,那么让我们创建一个函数,使我们能够检索任务数据。我们创建一个 GetTask
函数,根据我们传递的用户名和 taskID 检索任务。首先,使用 HTTP 触发器和以下代码创建一个新函数
import { AzureFunction, Context, HttpRequest } from "@azure/functions"
import * as AzureStorage from "azure-storage"
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
context.log('Task Store Triggered');
if(!req.body.username || req.body.username === ""){
context.res.status(400).json({ error: "No Username Defined" });
return;
}
if(!req.body.taskID || req.body.taskID === ""){
context.res.status(400).json({ error: "No Task ID Defined" });
return;
}
var tableSvc = AzureStorage.createTableService();
var tableName = "Tasks";
await apiFunctionWrapper(tableSvc, tableName, req.body.username,
req.body.taskID).then(value => {
context.res.status(200).json({
username: value["PartitionKey"]._,
taskID: value["RowKey"]._,
name: value["name"]._,
dueDate: value["dueDate"]._,
completed: value["completed"]._
});
}, error => {
context.log(error);
context.res.status(500).json({ taskId: req.body.taskID,
error: "TaskID does not exist for user" });
});
};
export default httpTrigger;
function apiFunctionWrapper(tableSvc, tableName, username, taskID) {
return new Promise((res, err) => {
tableSvc.retrieveEntity(tableName, username, taskID,
function(error, result) {
if (!error) {
return res(result);
} else {
return err(error);
}
});
});
}
此函数遵循与上一个函数类似的模式,但这次它使用 retrieveEntity
方法返回单个任务,并将 JSON 输出格式化为更具可读性。这是以这种方式生成微服务的一大好处:各个组件易于阅读和实现,因为它们只执行一个任务。
构成我们应用程序的其他函数都非常相似(您可以在GitHub 存储库中找到代码),所以让我们继续将我们的函数部署到 Azure。
部署函数
一旦所有函数在本地运行,我们就可以将它们部署到 Azure。我们有几个选项(例如 Azure Pipelines),但首先,让我们直接从 VSCode 部署。在此之前,我们需要确保已创建并配置了我们的函数应用和存储。
打开 Azure 门户,打开或创建资源组,然后添加新资源。找到函数应用并启动安装向导。选择一个适当的名称,并确保运行时堆栈选择了 Node.js。
单击下一步,指定一个新的存储帐户并命名。我们将重用 Azure Functions 所需的存储帐户,但在大多数情况下,您可能希望创建一个不同的存储帐户来仅保存数据。
单击查看和创建以创建函数应用。
函数应用程序及其资源创建后,我们需要像在本地一样指定 AZURE_STORAGE_CONNECTION_STRING
属性。
导航到存储帐户,在访问密钥下,复制连接字符串。返回到我们的函数应用,在配置下创建一个新的应用程序设置,名称为 AZURE_STORAGE_CONNECTION_STRING
,值为我们刚刚复制的连接字符串。
当我们回到 VS Code 并点击 Azure 扩展时,我们可以选择登录到我们的 Azure 帐户。完成登录步骤后,当您返回 VSCode 时,它应该列出您的帐户可用的 Azure 实例。
在此上方,有一个小的蓝色向上箭头图标,使您可以“部署到函数应用”
单击此图标,选择您的 Azure 实例,然后选择我们刚刚创建的函数应用。这将启动部署过程。
完成后,如果您切换回您的函数应用并单击部署中心选项,您应该会看到您的代码已部署。我们现在可以使用公开可用的 URL 测试我们的 API。
后续步骤
在本文中,我们采用了创建单个函数应用程序的概念,并将其构建为一套完整的简单任务管理应用程序函数。在 Azure Functions 中编写事件驱动的应用程序组件并快速部署它们非常容易。
但是,您可能会遇到一些需要额外组件或替代解决方案的挑战。特别是,长时间运行或密集的进程需要更传统的、始终运行的应用程序。此外,一些库需要特定版本或使函数过大而无法有效运行。为了解决这些挑战,可以将容器化应用程序与函数结合使用,以创建功能齐全的云原生应用程序。
在接下来的几篇文章中,我们将讨论容器化以及如何将集群和容器与我们的应用程序代码一起部署。