自定义 Azure 资源

如果您使用 Azure 托管 Aspire 解决方案的资源,就可以精细控制这些资源。 Aspire 如果其默认属性符合您的需求,可以让其自行配置,或者您可以选项覆盖默认值以控制其行为。 让我们来看看如何通过Azure代码自定义Aspire基础结构。

Azure 的 .NET SDK 提供 📦Azure.Provisioning NuGet 包和一套特定于服务的Azure 预配包。 这些 Azure 预配库可以轻松地在 Azure中以声明方式指定 .NET 基础结构。 借助其 API,可以在 C# 中编写面向对象的基础结构,从而生成 Bicep。 Bicep 是一种特定领域语言 (DSL), 用于声明式部署 Azure 资源。

虽然可以手动预配 Azure 资源,但 Aspire 通过提供一组 API 来表达 Azure 资源来简化该过程。 这些 API 可用作托管库 AspireAzure 中的扩展方法,从而扩展 IDistributedApplicationBuilder 接口。 将资源添加到 Azure AppHost 时,它们会隐式添加适当的预配功能。 换句话说,无需直接调用任何预配 API。

由于 Aspire 在 Azure 托管集成中模型化Azure 资源,因此使用 Azure SDK 来预配这些资源。 生成定义所需 Azure 资源的 Bicep 文件。 发布应用时,生成的 Bicep 文件与清单文件一起输出。

有多种方式可以影响生成的 Bicep 文件:

本地预配和 Azure.Provisioning

为了避免混淆术语并帮助消除“预配”的歧义,请务必了解 本地预配Azure 预配之间的区别:

  • 本地预配:

    默认情况下,调用任何用于添加 Azure 资源的 Azure 托管集成 API 时,会隐式调用 AddAzureProvisioning(IDistributedApplicationBuilder) API。 此 API 在依赖项注入(DI)容器中注册服务,用于在 AppHost 启动时预配 Azure 资源。 此概念称为 本地预配。 有关详细信息,请参阅 本地Azure 预配

  • Azure.Provisioning

    Azure.Provisioning 指 NuGet 包,是一组可用于使用 C# 生成 Bicep 的库。 Azure 中的 Aspire 托管集成在幕后使用这些库来生成定义所需 Azure 资源的 Bicep 文件。 有关详细信息,请参阅 Azure.Provisioning 自定义

Azure.Provisioning 自定义

所有 .NET AspireAzure 托管集成都公开各种 Azure 资源,它们都是 AzureProvisioningResource 类型的子类,而 AzureProvisioningResource 类型本身是继承自 。 这样可以启用对该类型进行泛型类型约束的扩展,允许通过“Fluent API”将基础结构自定义成您喜欢的样子。 虽然 Aspire 提供默认值,但你可以使用这些 API 自由地影响生成的 Bicep。

配置基础结构

不论使用哪个 Azure 资源,要配置它的底层基础设施,需要串联调用 ConfigureInfrastructure 扩展方法。 此方法允许您通过传递一个 Azure 类型的 configure 委托来自定义 Action<AzureResourceInfrastructure> 资源的基础架构。 AzureResourceInfrastructure 类型是 Azure.Provisioning.Infrastructure的子类。 此类型公开了一个庞大的 API 接口,用于配置 Azure 资源的底层基础设施。

请看下面的示例:

var sku = builder.AddParameter("storage-sku");

var storage = builder.AddAzureStorage("storage")
    .ConfigureInfrastructure(infra =>
    {
        var resources = infra.GetProvisionableResources();

        var storageAccount = resources.OfType<StorageAccount>().Single();

        storageAccount.Sku = new StorageSku
        {
            Name = sku.AsProvisioningParameter(infra)
        };
    });

前面的代码:

  • 添加名为 storage-sku的参数。
  • 使用名为 Azure 的 AddAzureStorage API 添加 storage 存储。
  • 链接对 ConfigureInfrastructure 的调用以自定义 Azure 存储基础架构:

这演示了将 外部参数 流向 Azure 存储基础结构,从而生成反映所需配置的 Bicep 文件。

添加 Azure 基础结构

并非所有 Azure 服务都以 Aspire 集成的形式公开。 尽管它们可能会在较晚的时候推出,但您仍然可以配置 Azure.Provisioning.* 库中可用的服务。 想象一下你有一个负责管理 Azure 容器注册表的辅助角色服务的场景。 现在,假设 AppHost 项目依赖于该📦Azure项目。Provisioning.ContainerRegistry NuGet 包。

可以使用 AddAzureInfrastructure API 将 Azure 容器注册表基础结构添加到 AppHost:

var acr = builder.AddAzureInfrastructure("acr", infra =>
{
    var registry = new ContainerRegistryService("acr")
    {
        Sku = new()
        {
            Name = ContainerRegistrySkuName.Standard
        },
    };
    infra.Add(registry);

    var output = new ProvisioningOutput("registryName", typeof(string))
    {
        Value = registry.Name
    };
    infra.Add(output);
});

builder.AddProject<Projects.WorkerService>("worker")
       .WithEnvironment(
            "ACR_REGISTRY_NAME",
            new BicepOutputReference("registryName", acr.Resource));

前面的代码:

  • 调用名称为 AddAzureInfrastructureacr
  • 提供 configureInfrastructure 委托以自定义 Azure 容器注册表基础架构:
    • 实例化一个名为 ContainerRegistryServiceacr,以及一个标准 SKU。
    • 将 Azure 容器注册表服务添加到 infra 变量。
    • 实例化一个名为 ProvisioningOutput、类型为 registryName 且值与 string 容器注册表名称对应的 Azure。
    • 将输出添加到 infra 变量。
  • 将名为 worker 的项目添加到生成器。
  • 链接对 WithEnvironment 的调用,以将项目的 ACR_REGISTRY_NAME 环境变量设置为 registryName 的输出值。

此功能演示如何将基础结构添加到 Azure AppHost 项目,即使 Azure 服务未直接公开为 Aspire 集成也是如此。 它进一步演示如何将 Azure 容器注册表的输出流到依赖项目的环境中。

查看生成的 Bicep 文件:

@description('The location for the resource(s) to be deployed.')
param location string = resourceGroup().location

resource acr 'Microsoft.ContainerRegistry/registries@2025-04-01' = {
  name: take('acr${uniqueString(resourceGroup().id)}', 50)
  location: location
  sku: {
    name: 'Standard'
  }
}

output registryName string = acr.name

Bicep 文件反映了 Azure API 定义的 AddAzureInfrastructure 容器注册表的所需配置。

使用基础结构解析程序自定义 Azure 预配选项

可用于自定义 Azure 预配的另一种方法是创建一个 InfrastructureResolver 并在其中编写代码以实现你的要求。 然后将该类添加到 AppHost 的配置选项。

自定义基础结构解析程序是继承自 InfrastructureResolver 的类,可以替代其一个或多个虚拟成员来应用所需的自定义。 在此示例中,方法 ResolveProperties 被重写以设置 Cosmos DB 资源的名称,但根据需求也可以重写其他成员。

internal sealed class FixedNameInfrastructureResolver : InfrastructureResolver
{
    public override void ResolveProperties(ProvisionableConstruct construct, ProvisioningBuildOptions options)
    {
        if (construct is CosmosDBAccount account)
        {
            account.Name = "ContosoCosmosDb";
        }

        base.ResolveProperties(construct, options);
    }
}

创建该类后,使用 AppHost 中的类似代码将其添加到配置选项:

var builder = DistributedApplication.CreateBuilder(args);

builder.Services.Configure<AzureProvisioningOptions>(options =>
{
    options.ProvisioningBuildOptions.InfrastructureResolvers.Insert(0, new FixedNameInfrastructureResolver());
});

builder.Build().Run();

小窍门

InfrastructureResolverConfigureInfrastructure方法都使您可以在AppHost代码中自定义Azure资源。 其优点 InfrastructureResolver 是,你可以使用它自定义许多 Azure 资源,而不仅仅是一个资源。

使用自定义 Bicep 模板

将 Azure 作为你期望的云提供商时,可以使用 Bicep 将基础结构定义为代码。 它旨在通过更简洁的语法大幅简化创作体验,并更好地支持模块化和代码重用。

虽然 Aspire 提供了一组预生成的 Bicep 模板,但有时可能需要自定义模板或创建自己的模板。 本部分介绍可用于自定义 Bicep 模板的概念和相应的 API。

重要

本部分不是要教你 Bicep,而是为你如何创建可与 Aspire 使用的自定义 Bicep 模板提供指导。

AzureAspire 的 部署案例中,通过 Azure Developer CLI (azd),可以了解 Aspire 项目,并能够将其部署到 Azure。 azd CLI 使用 Bicep 模板将应用程序部署到 Azure。

安装 Aspire.Hosting.Azure

当您想引用 Bicep 文件时,你可能没有使用任何 Azure 托管集成。 在这种情况下,您仍然可以通过安装 Aspire.Hosting.Azure 包来引用 Bicep 文件。 此包提供引用 Bicep 文件和自定义 Azure 资源所需的 API。

小窍门

如果使用任何 Azure 托管集成,则无需安装 Aspire.Hosting.Azure 包,因为它是可传递依赖项。

若要使用这些功能中的任何一项,必须安装 📦Aspire.Hosting.Azure NuGet 包:

dotnet add package Aspire.Hosting.Azure

有关详细信息,请参阅 dotnet 添加包管理 .NET 应用程序中的包依赖性

示例中可以预期的内容

本节中的所有示例都假定你使用的是 Aspire.Hosting.Azure 命名空间。 此外,这些示例假定你有一个 IDistributedApplicationBuilder 实例:

using Aspire.Hosting.Azure;

var builder = DistributedApplication.CreateBuilder(args);

// Examples go here...

builder.Build().Run();

默认情况下,当您调用任何与 Bicep 相关的 API 时,还会调用 AddAzureProvisioning,以支持在应用程序启动期间动态生成 Azure 资源。 有关详细信息,请参阅 本地预配和 Azure.Provisioning

引用 Bicep 文件

假设在名为 storage.bicep 的文件中有一个 Bicep 模板,用于配置 Azure 存储帐户:

param location string = resourceGroup().location
param storageAccountName string = 'toylaunch${uniqueString(resourceGroup().id)}'

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-06-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    accessTier: 'Hot'
  }
}

若要在磁盘上添加对 Bicep 文件的引用,请调用 AddBicepTemplate 方法。 请看下面的示例:

builder.AddBicepTemplate(
    name: "storage",
    bicepFile: "../infra/storage.bicep");

前面的代码添加对位于 ../infra/storage.bicep的 Bicep 文件的引用。 文件路径应相对于 AppHost 项目。 此引用会将名称为 AzureBicepResource"storage" 添加到应用程序的资源集合中,API 会返回一个 IResourceBuilder<AzureBicepResource> 实例,以便进一步自定义该资源。

以内联方式引用 Bicep

虽然在磁盘上拥有 Bicep 文件是最常见的情况,但也可以直接添加 Bicep 模板。 如果想要在代码中定义模板,或者想要动态生成模板,则内联模板非常有用。 若要添加内联 Bicep 模板,请调用 AddBicepTemplateString 方法,并使用 Bicep 模板作为 string。 请看下面的示例:

builder.AddBicepTemplateString(
        name: "ai",
        bicepContent: """
        @description('That name is the name of our application.')
        param cognitiveServiceName string = 'CognitiveService-${uniqueString(resourceGroup().id)}'

        @description('Location for all resources.')
        param location string = resourceGroup().location

        @allowed([
          'S0'
        ])
        param sku string = 'S0'

        resource cognitiveService 'Microsoft.CognitiveServices/accounts@2021-10-01' = {
          name: cognitiveServiceName
          location: location
          sku: {
            name: sku
          }
          kind: 'CognitiveServices'
          properties: {
            apiProperties: {
              statisticsEnabled: false
            }
          }
        }
        """
    );

在此示例中,Bicep 模板被定义为内联 string,并将其添加到名为 "ai"的应用程序资源集合中。 此示例配置 Azure AI 资源。

将参数传递给 Bicep 模板

Bicep 支持接受参数,可用于自定义模板的行为。 若要从 AspireBicep 模板传递参数,请链接对方法的 WithParameter 调用,如以下示例所示:

var region = builder.AddParameter("region");

builder.AddBicepTemplate("storage", "../infra/storage.bicep")
       .WithParameter("region", region)
       .WithParameter("storageName", "app-storage")
       .WithParameter("tags", ["latest","dev"]);

前面的代码:

  • 将名为 "region" 的参数添加到 builder 实例。
  • 添加对位于 ../infra/storage.bicep 的 Bicep 文件的引用。
  • "region" 参数传递给通过标准参数解析来解析的 Bicep 模板。
  • 使用"storageName"值将 参数传递给 Bicep 模板。
  • 使用字符串数组将 "tags" 参数传递给 Bicep 模板。

有关详细信息,请参阅 外部参数

已知参数

Aspire 提供一组可传递给 Bicep 模板的已知参数。 这些参数用于向 Bicep 模板提供有关应用程序和环境的信息。 以下已知参数可用:

字段 DESCRIPTION 价值
AzureBicepResource.KnownParameters.KeyVaultName 用于存储机密输出的密钥保管库资源的名称。 "keyVaultName"
AzureBicepResource.KnownParameters.Location 资源的位置。 所有资源都需要执行此步骤。 "location"
AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId Log Analytics 工作区的资源 ID。 "logAnalyticsWorkspaceId"
AzureBicepResource.KnownParameters.PrincipalId 当前用户或托管标识的主体 ID。 "principalId"
AzureBicepResource.KnownParameters.PrincipalName 当前用户或托管身份的主体名称。 "principalName"
AzureBicepResource.KnownParameters.PrincipalType 当前用户或托管标识的主体类型。 UserServicePrincipal "principalType"

若要使用已知参数,请将参数名称传递给 WithParameter 方法,例如 WithParameter(AzureBicepResource.KnownParameters.KeyVaultName)。 不传递已知参数的值,因为 Aspire 代表你解析这些参数。

请考虑一个你想要设置 Azure 事件网格 Webhook 的示例。 你可以将 Bicep 模板定义为如下所示:

param topicName string
param webHookEndpoint string
param principalId string
param principalType string
param location string = resourceGroup().location

// The topic name must be unique because it's represented by a DNS entry. 
// must be between 3-50 characters and contain only values a-z, A-Z, 0-9, and "-".

resource topic 'Microsoft.EventGrid/topics@2023-12-15-preview' = {
  name: toLower(take('${topicName}${uniqueString(resourceGroup().id)}', 50))
  location: location

  resource eventSubscription 'eventSubscriptions' = {
    name: 'customSub'
    properties: {
      destination: {
        endpointType: 'WebHook'
        properties: {
          endpointUrl: webHookEndpoint
        }
      }
    }
  }
}

resource EventGridRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(topic.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'd5a91429-5739-47e2-a06b-3470a27159e7'))
  scope: topic
  properties: {
    principalId: principalId
    principalType: principalType
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'd5a91429-5739-47e2-a06b-3470a27159e7')
  }
}

output endpoint string = topic.properties.endpoint

此 Bicep 模板定义了多个参数,包括 topicNamewebHookEndpointprincipalIdprincipalType和可选 location。 若要将这些参数传递给 Bicep 模板,可以使用以下代码片段:

var webHookApi = builder.AddProject<Projects.WebHook_Api>("webhook-api");

var webHookEndpointExpression = ReferenceExpression.Create(
        $"{webHookApi.GetEndpoint("https")}/hook");

builder.AddBicepTemplate("event-grid-webhook", "../infra/event-grid-webhook.bicep")
       .WithParameter("topicName", "events")
       .WithParameter(AzureBicepResource.KnownParameters.PrincipalId)
       .WithParameter(AzureBicepResource.KnownParameters.PrincipalType)
       .WithParameter("webHookEndpoint", () => webHookEndpointExpression);
  • webHookApi 项目添加为对 builder的引用。
  • topicName 参数传递一个固定的名称值。
  • webHookEndpoint 参数作为一个表达式传递,该表达式会被解析为 api 项目引用中具有 /hook 路由的“https”端点的 URL。
  • principalIdprincipalType 参数作为已知参数传递。

已知参数基于约定,在使用 WithParameter API 传递时不应附带相应的值。 众所周知的参数能够简化一些常见功能,例如将 角色分配添加到 Bicep 模板时,如前面的示例所示。 事件网格 Webhook 需要进行角色分配才能将事件发送到指定的终结点。 有关详细信息,请参阅 事件网格数据发送方角色分配

获取来自 Bicep 引用的输出

除了将参数传递给 Bicep 模板之外,还可以从 Bicep 模板获取输出。 请考虑以下 Bicep 模板,因为它定义了名为 outputendpoint

param storageName string
param location string = resourceGroup().location

resource myStorageAccount 'Microsoft.Storage/storageAccounts@2019-06-01' = {
  name: storageName
  location: location
  kind: 'StorageV2'
  sku:{
    name:'Standard_LRS'
    tier: 'Standard'
  }
  properties: {
    accessTier: 'Hot'
  }
}

output endpoint string = myStorageAccount.properties.primaryEndpoints.blob

Bicep 脚本定义了一个名为 "endpoint" 的输出变量。 若要从 Bicep 模板获取输出,请对 GetOutput 实例调用 IResourceBuilder<AzureBicepResource> 方法,如以下 C# 代码片段所示:

var storage = builder.AddBicepTemplate(
        name: "storage",
        bicepFile: "../infra/storage.bicep"
    );

var endpoint = storage.GetOutput("endpoint");

在此示例中,检索 Bicep 模板的输出并将其存储在 endpoint 变量中。 通常,将此输出作为环境变量传递给依赖于它的另一个资源。 例如,如果你有一个依赖于此终结点的 ASP.NET Core 最小 API 项目,则可以使用以下代码片段将输出作为环境变量传递给项目:

var storage = builder.AddBicepTemplate(
                name: "storage",
                bicepFile: "../infra/storage.bicep"
            );

var endpoint = storage.GetOutput("endpoint");

var apiService = builder.AddProject<Projects.AspireSample_ApiService>(
        name: "apiservice"
    )
    .WithEnvironment("STORAGE_ENDPOINT", endpoint);

有关详细信息,请参阅 Bicep 输出

从 Bicep 引用中获取机密的输出

使用 Bicep 时,请务必 避免 机密信息的输出。 如果输出被视为 机密,这意味着它不应在日志或其他位置公开,则可以将其视为此类。 这可以通过将机密存储在 Azure Key Vault 中并在 Bicep 模板中引用它来实现。 Aspire的 Azure 集成提供了一种模式,允许资源使用 keyVaultName 参数将机密存储在 Azure Key Vault中,从而安全地存储来自 Bicep 模板的输出。

请考虑以下 Bicep 模板作为示例,以演示保护秘密输出的概念:

param databaseAccountName string
param keyVaultName string

param databases array = []

@description('Tags that will be applied to all resources')
param tags object = {}

param location string = resourceGroup().location

var resourceToken = uniqueString(resourceGroup().id)

resource cosmosDb 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = {
    name: replace('${databaseAccountName}-${resourceToken}', '-', '')
    location: location
    kind: 'GlobalDocumentDB'
    tags: tags
    properties: {
        consistencyPolicy: { defaultConsistencyLevel: 'Session' }
        locations: [
            {
                locationName: location
                failoverPriority: 0
            }
        ]
        databaseAccountOfferType: 'Standard'
    }

    resource db 'sqlDatabases@2023-04-15' = [for name in databases: {
        name: '${name}'
        location: location
        tags: tags
        properties: {
            resource: {
                id: '${name}'
            }
        }
    }]
}

var primaryMasterKey = cosmosDb.listKeys(cosmosDb.apiVersion).primaryMasterKey

resource vault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
    name: keyVaultName

    resource secret 'secrets@2023-07-01' = {
        name: 'connectionString'
        properties: {
            value: 'AccountEndpoint=${cosmosDb.properties.documentEndpoint};AccountKey=${primaryMasterKey}'
        }
    }
}

上述 Bicep 模板需要 keyVaultName 参数,以及多个其他参数。 然后,它定义一个 Azure Cosmos DB 资源,并将机密存储到 Azure Key Vault中,该机密名为 connectionString,表示 Cosmos DB 实例的完全限定连接字符串。 若要访问此机密连接字符串值,可以使用以下代码片段:

var cosmos = builder.AddBicepTemplate("cosmos", "../infra/cosmosdb.bicep")
    .WithParameter("databaseAccountName", "fallout-db")
    .WithParameter(AzureBicepResource.KnownParameters.KeyVaultName)
    .WithParameter("databases", ["vault-33", "vault-111"]);

var connectionString =
    cosmos.GetSecretOutput("connectionString");

builder.AddProject<Projects.WebHook_Api>("api")
    .WithEnvironment(
        "ConnectionStrings__cosmos",
        connectionString);

在前面的代码片段中,作为对 cosmos的引用,builder Bicep 模板被添加。 connectionString 机密输出从 Bicep 模板中检索并存储在某个变量中。 然后,机密输出作为环境变量(ConnectionStrings__cosmos)传递给 api 项目。 此环境变量用于连接到 Cosmos DB 实例。

部署此资源后,基础部署机制将自动从 Azure Key Vault 引用机密。 若要保证机密隔离,Aspire 为每个源创建一个 Key Vault。

注释

本地预配 模式下,机密将从 Key Vault 中提取,并将其设置为环境变量。 有关详细信息,请参阅 本地Azure 预配