Aspire 是一个用于构建、运行、调试和部署分布式应用的工具链。 Aspire Azure Functions 集成使您能够开发、调试和协调 Azure Functions 项目,作为 Aspire AppHost 的一部分。 本文中的 .NET 示例使用了孤立工作者模型。
先决条件
设置用于将 Azure Functions 与 Aspire 配合使用的开发环境:
安装Aspire的前置条件,包括你的AppHost要求的.NET SDK。
从 AppHost 目录安装 Aspire Azure Functions 托管集成。
aspire add Aspire.Hosting.Azure.Functions
如果你使用 Visual Studio,请安装最新的 Visual Studio 和 Azure Functions 工具更新:
- 转到 “工具>选项”。
- 在 “项目和解决方案”下,选择 “Azure Functions”。
- 选择“检查更新”并按提示安装更新。
有关集成包及支持 AppHost API 的更多信息,请参见 App Host 中的 Set up Azure Functions。
解决方案结构
使用 Azure Functions 和 Aspire 的解决方案包含多个项目,包括一个 AppHost 和一个或多个 Functions 项目。
AppHost是你应用的入口。 它协调应用程序的组件(包括 Functions 项目)的设置。
解决方案通常还包括 服务默认 项目。 此项目提供一组默认服务和配置,用于应用程序中的项目。
AppHost 项目
要成功配置集成,请确保 AppHost 项目满足以下要求:
- AppHost 引用 Aspire.Hosting.Azure.Functions。 该软件包定义了集成。
- C# AppHost 引用一个 Functions 项目并调用
AddAzureFunctionsProject<TProject>(),或调用AddAzureFunctionsProject(name, projectPath)并传入项目文件的路径。 TypeScript 应用宿主使用addAzureFunctionsProject的项目路径形式。 - 使用
AddAzureFunctionsProject而不是AddProject。 通过使用AddProject添加的函数项目无法正常启动。
以下示例展示了一个C# AppHost项目的最小 AppHost.cs 文件:
var builder = DistributedApplication.CreateBuilder(args);
builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject");
builder.Build().Run();
Azure Functions项目
若要成功配置集成,请确保 Azure Functions 项目满足以下要求:
目标是 .NET 8 或更高版本,使用 .NET 9 SDK 或更高版本,并使用隔离工作者模型。
参考 Microsoft.Azure.Functions.Worker、Microsoft.Azure.Functions.Worker.Sdk,以及对于 HTTP 触发器的 Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore。
您的
Program.cs文件必须使用IHostApplicationBuilder的版本。 此要求意味着必须使用FunctionsApplication.CreateBuilder(args)。如果解决方案包含服务默认项目,请确保 Functions 项目配置为使用它:
- Functions 项目应包含对服务默认设置项目的项目引用。
- 在
IHostApplicationBuilder中生成Program.cs之前,请包括对builder.AddServiceDefaults()的调用。
以下示例演示 Aspire 中使用的 Functions 项目的最小 Program.cs 文件:
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.Hosting;
var builder = FunctionsApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.ConfigureFunctionsWebApplication();
builder.Build().Run();
此示例不包括在许多其他Program.cs示例中以及Azure Functions模板中显示的默认 Application Insights 配置。 相反,通过在 Aspire 中调用builder.AddServiceDefaults()方法来配置 OpenTelemetry 集成。
若要充分利用集成,请考虑以下准则:
- 不要在 Functions 项目中包括任何直接的 Application Insights 集成。 Aspire 中的监视相反由 OpenTelemetry 支持来实现。 可以将 Aspire 配置为通过服务默认项目将数据导出到 Azure Monitor。
- 当Aspire运行函数项目时,优先选择由AppHost注入的设置。 你可以在
local.settings.json中保留相应的设置,以便使用func start独立运行该项目;Aspire 注入的环境变量会覆盖这些设置。
使用 Aspire 的连接配置
AppHost 定义资源,并通过代码帮助你建立资源之间的连接。 本部分介绍如何配置和自定义 Azure Functions 项目使用的连接。
Aspire 包括有助于入门的默认连接权限。 但是,这些权限可能不适合或足以满足应用程序要求。
对于使用Azure基于角色的访问控制(RBAC)的方案,可以通过对项目资源调用 WithRoleAssignments() 方法来自定义权限。 调用 WithRoleAssignments()时,将删除所有默认角色分配,并且必须显式定义所需的完整角色分配。 如果在 Azure 容器应用 上托管应用程序,则使用 WithRoleAssignments() 还需要在 AddAzureContainerAppEnvironment() 上调用 DistributedApplicationBuilder。
Azure Functions 主机存储
Azure Functions需要主机存储连接(AzureWebJobsStorage)来实现一些核心功能。 在 AppHost 中调用 AddAzureFunctionsProject<TProject>() 时,默认会创建一个 AzureWebJobsStorage 连接,并将其提供给 Functions 项目。 这个默认连接使用Azure 存储模拟器进行本地开发运行,部署时会自动配置存储账户。 为了更好地控制,可以调用 .WithHostStorage() Functions 项目资源来替换该连接。
Aspire 为主机存储连接设置的默认权限取决于你是否调用 WithHostStorage() 。 添加 WithHostStorage() 会删除存储帐户参与者分配。 下表列出了 Aspire 为主机存储连接设置的默认权限:
| 主机存储连接 | 默认角色 |
|---|---|
不调用 WithHostStorage() |
存储 Blob 数据贡献者、 存储队列数据贡献者、 存储表数据参与者: 存储帐户贡献者 |
调用 WithHostStorage() |
存储 Blob 数据贡献者、 存储队列数据贡献者、 存储表数据贡献者 |
以下示例展示了 AppHost.cs 一个最小文件,它替代了主机存储并指定了角色分配:
using Azure.Provisioning.Storage;
var builder = DistributedApplication.CreateBuilder(args);
builder.AddAzureContainerAppEnvironment("myEnv");
var myHostStorage = builder.AddAzureStorage("myHostStorage");
builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
.WithHostStorage(myHostStorage)
.WithRoleAssignments(myHostStorage, StorageBuiltInRole.StorageBlobDataOwner);
builder.Build().Run();
注释
“存储 Blob 数据所有者”是我们针对主机存储连接的基本需求推荐的角色。 如果与 Blob 服务的连接只有 存储 Blob 数据参与者的 Aspire 默认值,则应用可能会遇到问题。
对于生产场景,请包括对 WithHostStorage() 和 WithRoleAssignments() 的调用。 然后,可以显式设置此角色,同时设置所需的任何其他角色。
触发器和绑定连接
触发器和绑定按名称引用连接。 以下 Aspire 集成通过调用项目资源中的 WithReference() 来提供这些连接:
| Aspire 集成 | 默认角色 |
|---|---|
| Azure Blob 存储 |
存储 Blob 数据贡献者、 存储队列数据贡献者、 存储表数据贡献者 |
| Azure 队列存储 |
存储 Blob 数据贡献者、 存储队列数据贡献者、 存储表数据贡献者 |
| Azure 事件中心 | Azure 事件中心数据所有者 |
| Azure 服务总线 | Azure 服务总线数据所有者 |
以下示例展示了一个配置队列触发器的最小 AppHost.cs 文件。 在此示例中,相应的队列触发器的 Connection 属性设置为 MyQueueTriggerConnection,因此调用时指定了 WithReference() 的名称。
var builder = DistributedApplication.CreateBuilder(args);
var myAppStorage = builder.AddAzureStorage("myAppStorage").RunAsEmulator();
var queues = myAppStorage.AddQueues("queues");
builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
.WithReference(queues, "MyQueueTriggerConnection");
builder.Build().Run();
对于其他集成,调用 WithReference 以不同的方式设置配置。 它们使配置可用于 Aspire 客户端集成,但不能用于触发器和绑定。 对于这些集成,调用 WithEnvironment() 传递触发器或绑定的连接信息以进行解析。
以下示例演示如何为公开连接字符串表达式的资源设置环境变量 MyBindingConnection :
builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
.WithEnvironment("MyBindingConnection", otherIntegration.Resource.ConnectionStringExpression);
如果希望 Aspire 客户端集成和触发器和绑定系统都使用连接,则可以同时配置 WithReference() 和 WithEnvironment()。
对于某些资源,连接的结构在在本地运行时和发布到Azure时可能会有所不同。 在前面的示例中,otherIntegration可能是作为模拟器运行的资源,因此ConnectionStringExpression将返回模拟器连接字符串。 但是,发布资源时,Aspire 可能会设置基于标识的连接,并 ConnectionStringExpression 返回服务的 URI。 在这种情况下,若要为 Azure Functions 设置基于身份的连接,您可能需要提供不同的环境变量名称。
以下示例使用 builder.ExecutionContext.IsPublishMode 以有条件地添加必要的后缀:
builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
.WithEnvironment("MyBindingConnection" + (builder.ExecutionContext.IsPublishMode ? "__serviceUri" : ""), otherIntegration.Resource.ConnectionStringExpression);
有关每个绑定支持的连接格式以及这些格式所需的权限的详细信息,请参阅绑定的 参考页。
关于函数代码如何读取注入的WithReference值的更多信息,请参见 Azure Functions 运行时配置。
托管应用程序
Aspire 支持 Functions 项目的 Azure 容器应用 部署。 你也可以使用单独的预览版 App Service 集成来针对支持容器的功能应用:
在这两种情况下,您的项目都被部署为容器化。 Aspire 负责生成容器映像并将其推送到Azure 容器注册表。
将其部署为容器应用
当你的 AppHost 针对 Azure 容器应用 时,Aspire 会用 KEDA 为你的 Functions 项目设置扩展规则。 使用 Azure 容器应用 时,你需要为函数键进行额外的设置。 欲了解更多信息,请参见 Azure 容器应用 上的访问密钥。
通过运行 aspire deploy. 来部署配置好的 AppHost。 更多信息请参见 Deploy to Azure 容器应用 和 aspire deploy。
Azure 容器应用上的访问密钥
多个 Azure Functions 方案使用访问密钥来针对不需要的访问提供基本缓解措施。 例如,默认情况下,HTTP 触发器函数需要调用访问密钥,但可以使用该属性禁用AuthLevel此要求。 有关可能需要密钥的方案,请参阅 在 Azure Functions 中使用访问密钥。
当你通过 Aspire 部署 Functions 项目到 Azure 容器应用 时,系统不会自动创建或管理 Functions 访问密钥。 如果你需要使用访问密钥,可以在AppHost设置中管理它们。 本节会展示如何创建扩展方法,你可以从AppHost的文件 AppHost.cs 中调用它来创建和管理访问密钥。 此方法使用 Azure 密钥保管库 存储密钥,并将其作为机密装载到容器应用中。
注释
此处的行为取决于 ContainerApps 机密提供程序,而该提供程序要求 Functions 主机版本 4.1044.0 或更高版本。
这些步骤需要 Bicep 版本 0.38.3 或更高版本。 可以通过在命令提示符运行 bicep --version 来检查 Bicep 的版本。 如果已安装Azure CLI,可以使用 az bicep upgrade 快速将Bicep更新到最新版本。
为您的 AppHost 项目添加以下 NuGet 包:
在你的 AppHost 项目中创建一个新类,并包含以下代码:
using Aspire.Hosting.Azure;
using Azure.Provisioning.AppContainers;
namespace Aspire.Hosting;
internal static class Extensions
{
private record SecretMapping(string OriginalName, IAzureKeyVaultSecretReference Reference);
public static IResourceBuilder<T> PublishWithContainerAppSecrets<T>(
this IResourceBuilder<T> builder,
IResourceBuilder<AzureKeyVaultResource>? keyVault = null,
string[]? hostKeyNames = null,
string[]? systemKeyExtensionNames = null)
where T : AzureFunctionsProjectResource
{
if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
{
return builder;
}
keyVault ??= builder.ApplicationBuilder.AddAzureKeyVault("functions-keys");
var hostKeysToAdd = (hostKeyNames ?? []).Append("default").Select(k => $"host-function-{k}");
var systemKeysToAdd = systemKeyExtensionNames?.Select(k => $"host-systemKey-{k}_extension") ?? [];
var secrets = hostKeysToAdd.Union(systemKeysToAdd)
.Select(secretName => new SecretMapping(
secretName,
CreateSecretIfNotExists(builder.ApplicationBuilder, keyVault, secretName.Replace("_", "-"))
)).ToList();
return builder
.WithReference(keyVault)
.WithEnvironment("AzureWebJobsSecretStorageType", "ContainerApps")
.PublishAsAzureContainerApp((infra, app) => ConfigureFunctionsContainerApp(infra, app, builder.Resource, secrets));
}
private static void ConfigureFunctionsContainerApp(
AzureResourceInfrastructure infrastructure,
ContainerApp containerApp,
IResource resource,
List<SecretMapping> secrets)
{
const string volumeName = "functions-keys";
const string mountPath = "/run/secrets/functions-keys";
var appIdentityAnnotation = resource.Annotations.OfType<AppIdentityAnnotation>().Last();
var containerAppIdentityId = appIdentityAnnotation.IdentityResource.Id.AsProvisioningParameter(infrastructure);
var containerAppSecretsVolume = new ContainerAppVolume
{
Name = volumeName,
StorageType = ContainerAppStorageType.Secret
};
foreach (var mapping in secrets)
{
var secret = mapping.Reference.AsKeyVaultSecret(infrastructure);
containerApp.Configuration.Secrets.Add(new ContainerAppWritableSecret()
{
Name = mapping.Reference.SecretName.ToLowerInvariant(),
KeyVaultUri = secret.Properties.SecretUri,
Identity = containerAppIdentityId
});
containerAppSecretsVolume.Secrets.Add(new SecretVolumeItem
{
Path = mapping.OriginalName.Replace("-", "."),
SecretRef = mapping.Reference.SecretName.ToLowerInvariant()
});
}
containerApp.Template.Containers[0].Value!.VolumeMounts.Add(new ContainerAppVolumeMount
{
VolumeName = volumeName,
MountPath = mountPath
});
containerApp.Template.Volumes.Add(containerAppSecretsVolume);
}
public static IAzureKeyVaultSecretReference CreateSecretIfNotExists(
IDistributedApplicationBuilder builder,
IResourceBuilder<AzureKeyVaultResource> keyVault,
string secretName)
{
var secretParameter = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"param-{secretName}", special: false);
builder.AddBicepTemplateString($"key-vault-key-{secretName}", """
param location string = resourceGroup().location
param keyVaultName string
param secretName string
@secure()
param secretValue string
// Reference the existing Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: keyVaultName
}
// Deploy the secret only if it does not already exist
@onlyIfNotExists()
resource newSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
parent: keyVault
name: secretName
properties: {
value: secretValue
}
}
""")
.WithParameter("keyVaultName", keyVault.GetOutput("name"))
.WithParameter("secretName", secretName)
.WithParameter("secretValue", secretParameter);
return keyVault.GetSecret(secretName);
}
}
然后你可以在AppHost的文件 AppHost.cs 中使用这个方法:
builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
.WithHostStorage(storage)
.WithExternalHttpEndpoints()
.PublishWithContainerAppSecrets(systemKeyExtensionNames: ["mcp"]);
此示例使用扩展方法创建的默认密钥保管库。 这会生成用于 模型上下文协议扩展的默认密钥和系统密钥。
若要从客户端使用这些密钥,需要从密钥保管库中检索它们。
作为函数应用部署
注释
部署为函数应用需要 Aspire Azure 应用服务 集成,而该集成当前仍处于预览版阶段。
你可以使用 Aspire Azure 应用服务 集成将 Aspire 配置为部署到函数应用。 由于Aspire将Functions项目部署为容器,功能应用的托管计划必须支持部署容器化应用。
要将您的 Aspire Functions 项目部署为函数应用,请遵循以下步骤:
- 从 AppHost 目录中,运行
aspire add Aspire.Hosting.Azure.AppService以添加 Aspire.Hosting.Azure.AppService NuGet 套件。 - 在
AppHost.cs文件中,调用AddAzureAppServiceEnvironment()实例的IDistributedApplicationBuilder以创建应用服务计划。 请注意,尽管名称如此,但这不会预配应用服务环境资源。 - 在 Functions 项目资源上,调用
.WithExternalHttpEndpoints()。 这是使用 Aspire Azure 应用服务集成进行部署所必需的。 - 对于 Functions 项目资源,调用
.PublishAsAzureAppServiceWebsite((infra, app) => app.Kind = "functionapp,linux")将该项目自定义为该计划中的函数应用。
Important
请确保将 app.Kind 属性设置为 "functionapp,linux". 此设置可确保将资源创建为函数应用,这会影响使用应用程序的体验。
以下示例显示了一个最小的 AppHost.cs 文件,用于将 Functions 项目部署为函数应用:
var builder = DistributedApplication.CreateBuilder(args);
builder.AddAzureAppServiceEnvironment("functions-env");
builder.AddAzureFunctionsProject<Projects.MyFunctionsProject>("MyFunctionsProject")
.WithExternalHttpEndpoints()
.PublishAsAzureAppServiceWebsite((infra, app) => app.Kind = "functionapp,linux");
builder.Build().Run();
此配置创建高级 V3 计划。 使用专用应用服务计划 SKU 时,缩放不是基于事件的。 而是通过应用服务计划设置管理扩展。
注意事项和最佳做法
在评估 Azure Functions 与 Aspire 的集成时,请考虑以下几点:
使用 Aspire 的触发器和绑定配置目前仅限于特定的集成方案。 有关详细信息,请参阅本文中的 Aspire 连接配置 。
函数项目的
Program.cs文件应使用IHostApplicationBuilder的版本。 通过使用IHostApplicationBuilder,你可以调用builder.AddServiceDefaults(),将 Aspire Service Defaults 添加到你的 Functions 项目中。Aspire 使用 OpenTelemetry 进行监视。 可以将 Aspire 配置为通过服务默认项目将数据导出到 Azure Monitor。
在许多其他 Azure Functions 上下文中,可以通过注册工作器服务来实现与 Application Insights 的直接集成。 使用Aspire服务默认时,不要注册第二个直接应用洞察流水线。
对于被列入 Aspire 编排的函数项目,AppHost 应提供大部分应用配置。 你可以使用
local.settings.json配合func start独立运行 Functions 项目。 当 Aspire 运行项目时,Aspire 注入的环境变量会覆盖local.settings.json中的同名值。避免为AppHost管理的连接启动第二个Azure 存储模拟器。 竞争的模拟器实例可能导致端口和存储冲突。
欲了解更多信息,请参见 Azure Functions runtime configuration 和 Aspire telemetry。