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 thebuilder
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 thebuilder
. - The
topicName
parameter is passed a hardcoded name value. - The
webHookEndpoint
parameter is passed as an expression that resolves to the URL from theapi
project references' "https" endpoint with the/hook
route. - The
principalId
andprincipalType
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: