Customizing the combo of Azure Developer CLI and .NET Aspire
Inlining extra code
Inlining via Aspire
var clientId = builder.AddParameter(“ClientId”);
var clientSecret = builder.AddParameter(“ClientSecret”,secret:true);
var weatherapi = builder.AddProject<Projects.WeatherAPI>(“weatherapi”)
.WithEnvironment(“TenantId”,tenantId)
.WithEnvironment(“ClientId”,clientId);
var appRegistration = builder.AddBicepTemplate(
name: “Graph”,
bicepFile: “../infra/Graph/app-registration.bicep”
);
var tenantId = appRegistration.GetOutput(“tenantId”);
var clientId = appRegistration.GetOutput(“clientId”);
var weatherapi = builder.AddProject<Projects.WeatherAPI>(“weatherapi”)
.WithEnvironment(“TenantId”,tenantId)
.WithEnvironment(“ClientId”,clientId);
resource app ‘Microsoft.Graph/applications@v1.0’ = {
displayName: ‘azd-custom-01’
uniqueName: ‘azd-custom-01’
}
output tenantId string = tenant().tenantId
output clientId string = app.appId
Inlining via Azure Developer CLI
resource vault ‘Microsoft.KeyVault/vaults@2024-04-01-preview’ = {
name: ‘kv-${resourceToken}’
location: location
properties: {
sku: {
name: ‘standard’
family: ‘A’
}
accessPolicies: []
enableRbacAuthorization: true
enabledForDeployment: true
tenantId: tenant().tenantId
}
}
resource kvMiRoleAssignment ‘Microsoft.Authorization/roleAssignments@2022-04-01’ = {
name: guid(vault.id, managedIdentity.id, subscriptionResourceId(‘Microsoft.Authorization/roleDefinitions’, ‘4633458b-17de-408a-b874-0445c86b69e6’))
scope: vault
properties: {
principalId: managedIdentity.properties.principalId
principalType: ‘ServicePrincipal’
roleDefinitionId: subscriptionResourceId(‘Microsoft.Authorization/roleDefinitions’, ‘4633458b-17de-408a-b874-0445c86b69e6’)
}
}
output KEYVAULT_URL string = vault.properties.vaultUri
– server: {{ .Env.AZURE_CONTAINER_REGISTRY_ENDPOINT }}
identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}
secrets:
– name: clientsecret
keyVaultUrl: ‘{{ .Env.KEYVAULT_URL }}secrets/clientSecret’
identity: {{ .Env.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID }}
…
template:
containers:
– image: {{ .Image }}
name: bff-web-app
env:
– name: ClientSecret
secretRef: clientsecret
var clientId = appRegistration.GetOutput(“clientId”);
var clientSecret = appRegistration.GetSecretOutput(“clientSecret”);
builder.AddProject<Projects.BFF_Web_App>(“bff-web-app”)
.WithReference(weatherapi)
.WithEnvironment(“TenantId”, tenantId)
.WithEnvironment(“ClientId”, clientId)
.WithEnvironment(“ClientSecret”, clientSecret);
CAF Primer
Level 1: Azure Policy, Entra ID, Log Analytics workspaces
Level 2: Virtual networks, DNS zones, Azure Firewall
Level 3: Azure Kubernetes Service, Azure Container App Environments, SQL Server
Level 4: Containers, SQL Databases
Pre-creating out of band
Pre-creating with hooks
hooks:
preprovision:
shell: pwsh
run: az stack sub create –name azd-level-2 –location norwayeast –template-file .infralevel-2main.bicep –parameters .infralevel-2main.bicepparam –action-on-unmanage ‘deleteAll’ –deny-settings-mode none
postdown:
shell: pwsh
run: az stack sub delete –name azd-level-2 –action-on-unmanage deleteAll –yes
services:
app:
language: dotnet
project: ./BFF_Aspire/BFF_Web_App.AppHost/BFF_Web_App.AppHost.csproj
host: containerapp
Complete sample
var clientId = builder.Configuration.GetValue<string>(“ClientId”);
var clientSecret = builder.Configuration.GetValue<string>(“ClientSecret”);
var keyvaultUrl = builder.Configuration.GetValue<string>(“KeyVaultUrl”) ?? “noVault”;
var keyvaultSecret = builder.Configuration.GetValue<string>(“KeyVaultSecret”) ?? “noVault”;
builder.AddServiceDefaults();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddCookie(“MicrosoftOidc”)
.AddMicrosoftIdentityWebApp(microsoftIdentityOptions =>
{
if (builder.Environment.IsDevelopment())
{
microsoftIdentityOptions.ClientCredentials = new CredentialDescription[] {
CertificateDescription.FromStoreWithDistinguishedName(“CN=MySelfSignedCertificate”,System.Security.Cryptography.X509Certificates.StoreLocation.CurrentUser)};
}
else
{
microsoftIdentityOptions.ClientCredentials = new CredentialDescription[] {
CertificateDescription.FromKeyVault(keyvaultUrl,keyvaultSecret)};
}
microsoftIdentityOptions.ClientId = clientId;
microsoftIdentityOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
microsoftIdentityOptions.CallbackPath = new PathString(“/signin-oidc”);
microsoftIdentityOptions.SignedOutCallbackPath = new PathString(“/signout-callback-oidc”);
microsoftIdentityOptions.Scope.Add($”api://{clientId}/Weather.Get”);
microsoftIdentityOptions.Authority = $”https://login.microsoftonline.com/{tenantId}/v2.0/”;
microsoftIdentityOptions.ResponseType = OpenIdConnectResponseType.Code;
microsoftIdentityOptions.MapInboundClaims = false;
microsoftIdentityOptions.TokenValidationParameters.NameClaimType = JwtRegisteredClaimNames.Name;
microsoftIdentityOptions.TokenValidationParameters.RoleClaimType = “role”;
}).EnableTokenAcquisitionToCallDownstreamApi(confidentialClientApplicationOptions =>
{
confidentialClientApplicationOptions.Instance = “https://login.microsoftonline.com/”;
confidentialClientApplicationOptions.TenantId = tenantId;
confidentialClientApplicationOptions.ClientId = clientId;
})
.AddInMemoryTokenCaches();
builder.Services.ConfigureCookieOidcRefresh(“Cookies”, OpenIdConnectDefaults.AuthenticationScheme);
builder.Services.AddAuthorization();
displayName: ‘azd-custom-03’
uniqueName: ‘azd-custom-03’
keyCredentials: [
{
displayName: ‘Credential from KV’
usage: ‘Verify’
type: ‘AsymmetricX509Cert’
key: createAddCertificate.properties.outputs.certKey
startDateTime: createAddCertificate.properties.outputs.certStart
endDateTime: createAddCertificate.properties.outputs.certEnd
}
]
//The default would be api://<appid> but this creates an invalid (for Bicep) self-referential value
identifierUris: [
identifierUri
]
web: {
redirectUris: [
‘https://localhost:7109/signin-oidc’
‘https://bff-web-app.${caeDomainName}/signin-oidc’
‘https://bff-web-app.internal.${caeDomainName}/signin-oidc’
]
}
api: {
oauth2PermissionScopes: [
{
adminConsentDescription: ‘Weather.Get’
adminConsentDisplayName: ‘Weather.Get’
value: ‘Weather.Get’
type: ‘User’
isEnabled: true
userConsentDescription: ‘Weather.Get’
userConsentDisplayName: ‘Weather.Get’
id: guid(‘Weather.Get’)
}
]
}
}
using Aspire.Hosting.Azure;
var builder = DistributedApplication.CreateBuilder(args);
//Replace with a verified domain in your tenant
var identifierUri = “api://contoso.com”;
//var appRegistration = builder.AddBicepTemplate(
// name: “Graph”,
// bicepFile: “../infra/Graph/app-registration.bicep”
//)
// .WithParameter(“identifierUri”, identifierUri)
// .WithParameter(“subjectName”, “CN=bff.contoso.com”)
// .WithParameter(“keyVaultName”)
// .WithParameter(“certificateName”)
// .WithParameter(“uamiName”)
// .WithParameter(“caeDomainName”);
//var tenantId = appRegistration.GetOutput(“tenantId”);
//var clientId = appRegistration.GetOutput(“clientId”);
//var keyVaultUrl = appRegistration.GetOutput(“keyVaultUrl”);
//var keyVaultSecret = appRegistration.GetOutput(“keyVaultSecret”);
var tenantId = builder.AddParameter(“TenantId”);
var clientId = builder.AddParameter(“ClientId”);
var keyVaultUrl = builder.AddParameter(“keyVaultUrl”);
var keyVaultSecret = builder.AddParameter(“keyVaultSecret”);
var weatherapi = builder.AddProject<Projects.WeatherAPI>(“weatherapi”)
.WithEnvironment(“TenantId”, tenantId)
.WithEnvironment(“ClientId”, clientId)
.WithEnvironment(“IdentifierUri”, identifierUri);
builder.AddProject<Projects.BFF_Web_App>(“bff-web-app”)
.WithReference(weatherapi)
.WithExternalHttpEndpoints()
.WithEnvironment(“TenantId”, tenantId)
.WithEnvironment(“ClientId”, clientId)
.WithEnvironment(“IdentifierUri”, identifierUri)
.WithEnvironment(“KeyVaultUrl”, keyVaultUrl)
.WithEnvironment(“KeyVaultSecret”, keyVaultSecret);
builder.Build().Run();
{
//azd infra synth will not generate code
}
if (!builder.Environment.IsDevelopment())
{
//azd infra synth will generate code
}
//main.bicep
output GRAPH_CLIENTID string = Graph.outputs.clientId
//bff-web-app.tmpl.yaml
– name: ClientId
value: ‘{{ .Env.GRAPH_CLIENTID }}’
/* Parameters */
//main.bicep
param ClientId string
//bff-web-app.tmpl.yaml
– name: ClientId
value: ‘{{ parameter “ClientId” }}’
/* Hard-coded values */
//Program.cs
var identifierUri = “api://contoso.com”;
//bff-web-app.tmpl.yaml
– name: IdentifierUri
value: api://contoso.com
param networkRGName string = ‘rg-azd-level-2’
@description(‘The name of the virtual network to attach resources to.’)
param vnetName string = ‘aca-azd-weu’
resource rg_vnet ‘Microsoft.Resources/resourceGroups@2024-03-01’ existing = {
name: networkRGName
}
resource vnet ‘Microsoft.Network/virtualNetworks@2023-09-01’ existing = {
scope: rg_vnet
name: vnetName
}
…
module resources ‘resources.bicep’ = {
scope: rg
name: ‘resources’
params: {
location: location
vnetId: vnet.id
dnsRGName: networkRGName
tags: tags
principalId: principalId
}
}
// name: replace(‘acr-${resourceToken}’, ‘-‘, ”)
// location: location
// sku: {
// name: ‘Basic’
// }
// tags: tags
// }
module containerRegistry ‘modules/containers/container-registry/main.bicep’ = {
name: replace(‘acr-${resourceToken}’, ‘-‘, ”)
params: {
resourceTags: tags
acrName: replace(‘acr-${resourceToken}’, ‘-‘, ”)
acrSku: ‘Premium’
adminUserEnabled: false
anonymousPullEnabled: false
location: location
managedIdentity: ‘SystemAssigned’
publicNetworkAccess: ‘Enabled’
}
}
resource scopeACR ‘Microsoft.ContainerRegistry/registries@2023-07-01’ existing = {
name: containerRegistry.name
}
//And use it as the scope for a role assignment
resource caeMiRoleAssignment ‘Microsoft.Authorization/roleAssignments@2022-04-01’ = {
name: guid(containerRegistry.name, managedIdentity.id, subscriptionResourceId(‘Microsoft.Authorization/roleDefinitions’, ‘7f951dda-4ed3-4680-a7ca-43fe172d538d’))
scope: scopeACR
properties: {
principalId: managedIdentity.properties.principalId
principalType: ‘ServicePrincipal’
roleDefinitionId: subscriptionResourceId(‘Microsoft.Authorization/roleDefinitions’, ‘7f951dda-4ed3-4680-a7ca-43fe172d538d’)
}
}
name: ‘cae-${resourceToken}’
params: {
resourceTags: tags
location: location
environmentName: ‘cae-${resourceToken}’
snetId: ‘${vnetId}/subnets/snet-cae-01’
//true for connecting CAE to snet (with private IPs)
//false for public IPs
vnetInternal: true
}
}
#resourceGroup: rg-03-E2E
hooks:
preprovision:
– shell: pwsh
run: az stack sub create –name azd-level-2 –location westeurope –template-file .infralevel-2main.bicep –parameters .infralevel-2main.bicepparam –action-on-unmanage ‘deleteAll’ –deny-settings-mode none
postprovision:
– shell: pwsh
run: az stack sub create –name azd-devCenter –location westeurope –template-file .infradevCentermain.bicep –parameters .infradevCentermain.bicepparam –action-on-unmanage ‘deleteAll’ –deny-settings-mode none
postdown:
– shell: pwsh
run: az stack sub delete –name azd-devCenter –action-on-unmanage deleteAll –yes
– shell: pwsh
run: az stack sub delete –name azd-level-2 –action-on-unmanage deleteAll –yes
services:
app:
language: dotnet
project: ./BFF_Web_App.AppHost/BFF_Web_App.AppHost.csproj
host: containerapp
? Enter a new environment name: 03-E2E
PS C:CodePOCsazd_customization3_End-to-EndBFF_Aspire> azd up
? Select an Azure Subscription to use: [Use arrows to move, type to filter]
“Logging”: {
“LogLevel”: {
“Default”: “Information”,
“Microsoft.AspNetCore”: “Warning”,
“Aspire.Hosting.Dcp”: “Warning”
}
},
“Parameters”: {
“TenantId”: “guid”,
“ClientId”: “guid”,
“ClientSecret”: “guid”,
“KeyVaultUrl”: “https://contoso.vault.azure.net”,
“KeyVaultSecret”: “certificateName”
}
}
Microsoft Tech Community – Latest Blogs –Read More