Use custom Bicep templates

When you're targeting Azure as your desired cloud provider, you can use Bicep to define your infrastructure as code. Bicep is a domain-specific language (DSL) for deploying Azure resources declaratively. It aims to drastically simplify the authoring experience with a cleaner syntax and better support for modularity and code reuse.

While .NET Aspire provides a set of pre-built Bicep templates so that you don't need to write them, there might be times when you either want to customize the templates or create your own. This article explains the concepts and corresponding APIs that you can use to customize the Bicep templates.

Important

This article is not intended to teach Bicep, but rather to provide guidance on how to create customize Bicep templates for use with .NET Aspire.

As part of the Azure deployment story for .NET Aspire, the Azure Developer CLI (azd) provides an understanding of your .NET Aspire project and the ability to deploy it to Azure. The azd CLI uses the Bicep templates to deploy the application to Azure.

Install App Host package

To use any of this functionality, you must install the Aspire.Hosting.Azure NuGet package:

dotnet add package Aspire.Hosting.Azure

For more information, see dotnet add package or Manage package dependencies in .NET applications.

All of the examples in this article assume that you've installed the Aspire.Hosting.Azure package and imported the Aspire.Hosting.Azure namespace. Additionally, the examples assume you've created an IDistributedApplicationBuilder instance:

using Aspire.Hosting.Azure;

var builder = DistributedApplication.CreateBuilder(args);

// Examples go here...

builder.Build().Run();

Tip

By default, when you call any of the Bicep-related APIs, a call is also made to AddAzureProvisioning that adds support for generating Azure resources dynamically during application startup.

Reference Bicep files

Imagine that you've defined a Bicep template in a file named storage.bicep that provisions an Azure Storage Account:

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'
  }
}

To add a reference to the Bicep file on disk, call the AddBicepTemplate method. Consider the following example:

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

The preceding code adds a reference to a Bicep file located at ../infra/storage.bicep. The file paths should be relative to the app host project. This reference results in an AzureBicepResource being added to the application's resources collection with the "storage" name, and the API returns an IResourceBuilder<AzureBicepResource> instance that can be used to further customize the resource.

Reference Bicep inline

While having a Bicep file on disk is the most common scenario, you can also add Bicep templates inline. Inline templates can be useful when you want to define a template in code or when you want to generate the template dynamically. To add an inline Bicep template, call the AddBicepTemplateString method with the Bicep template as a string. Consider the following example:

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
            }
          }
        }
        """
    );

In this example, the Bicep template is defined as an inline string and added to the application's resources collection with the name "ai". This example provisions an Azure AI resource.

Pass parameters to Bicep templates

Bicep supports accepting parameters, which can be used to customize the behavior of the template. To pass parameters to a Bicep template from .NET Aspire, chain calls to the WithParameter method as shown in the following example:

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

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

The preceding code:

  • Adds a parameter named "region" to the builder instance.
  • Adds a reference to a Bicep file located at ../infra/storage.bicep.
  • Passes the "region" parameter to the Bicep template, which is resolved using the standard parameter resolution.
  • Passes the "storageName" parameter to the Bicep template with a hardcoded value.
  • Passes the "tags" parameter to the Bicep template with an array of strings.

For more information, see External parameters.

Well-known parameters

.NET Aspire provides a set of well-known parameters that can be passed to Bicep templates. These parameters are used to provide information about the application and the environment to the Bicep templates. The following well-known parameters are available:

Field Description Value
AzureBicepResource.KnownParameters.KeyVaultName The name of the key vault resource used to store secret outputs. "keyVaultName"
AzureBicepResource.KnownParameters.Location The location of the resource. This is required for all resources. "location"
AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId The resource ID of the log analytics workspace. "logAnalyticsWorkspaceId"
AzureBicepResource.KnownParameters.PrincipalId The principal ID of the current user or managed identity. "principalId"
AzureBicepResource.KnownParameters.PrincipalName The principal name of the current user or managed identity. "principalName"
AzureBicepResource.KnownParameters.PrincipalType The principal type of the current user or managed identity. Either User or ServicePrincipal. "principalType"

To use a well-known parameter, pass the parameter name to the WithParameter method, such as WithParameter(AzureBicepResource.KnownParameters.KeyVaultName). You don't pass values for well-known parameters, as they're resolved automatically by .NET Aspire.

Consider an example where you want to setup an Azure Event Grid webhook. You might define the Bicep template as follows:

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

This Bicep template defines several parameters, including the topicName, webHookEndpoint, principalId, principalType, and the optional location. To pass these parameters to the Bicep template, you can use the following code snippet:

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);
  • The webHookApi project is added as a reference to the builder.
  • The topicName parameter is passed a hardcoded name value.
  • The webHookEndpoint parameter is passed as an expression that resolves to the URL from the api project references' "https" endpoint with the /hook route.
  • The principalId and principalType parameters are passed as well-known parameters.

The well-known parameters are convention-based and shouldn't be accompanied with a corresponding value when passed using the WithParameter API. Well-known parameters simplify some common functionality, such as role assignments, when added to the Bicep templates, as shown in the preceding example. Role assignments are required for the Event Grid webhook to send events to the specified endpoint. For more information, see EventGrid Data Sender role assignment.

Get outputs from Bicep references

In addition to passing parameters to Bicep templates, you can also get outputs from the Bicep templates. Consider the following Bicep template, as it defines an output named endpoint:

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

The Bicep defines an output named endpoint. To get the output from the Bicep template, call the GetOutput method on an IResourceBuilder<AzureBicepResource> instance as demonstrated in following C# code snippet:

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

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

In this example, the output from the Bicep template is retrieved and stored in an endpoint variable. Typically, you would pass this output as an environment variable to another resource that relies on it. For instance, if you had an ASP.NET Core Minimal API project that depended on this endpoint, you could pass the output as an environment variable to the project using the following code snippet:

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);

For more information, see Bicep outputs.

Get secret outputs from Bicep references

It's important to avoid outputs for secrets when working with Bicep. If an output is considered a secret, meaning it shouldn't be exposed in logs or other places, you can treat it as such. This can be achieved by storing the secret in Azure Key Vault and referencing it in the Bicep template. .NET Aspire's Azure integration provides a pattern for securely storing outputs from the Bicep template by allows resources to use the keyVaultName parameter to store secrets in Azure Key Vault.

Consider the following Bicep template as an example the helps to demonstrate this concept of securing secret outputs:

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}'
        }
    }
}

The preceding Bicep template expects a keyVaultName parameter, among several other parameters. It then defines an Azure Cosmos DB resource and stashes a secret into Azure Key Vault, named connectionString which represents the fully qualified connection string to the Cosmos DB instance. To access this secret connection string value, you can use the following code snippet:

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);

In the preceding code snippet, the cosmos Bicep template is added as a reference to the builder. The connectionString secret output is retrieved from the Bicep template and stored in a variable. The secret output is then passed as an environment variable (ConnectionStrings__cosmos) to the api project. This environment variable is used to connect to the Cosmos DB instance.

When this resource is deployed, the underlying deployment mechanism with automatically Reference secrets from Azure Key Vault. To guarantee secret isolation, .NET Aspire creates a Key Vault per source.

Note

In local provisioning mode, the secret is extracted from Key Vault and set it in an environment variable. For more information, see Local Azure provisioning.

See also

For continued learning, see the following resources as they relate to .NET Aspire and Azure deployment: