From 99430e3dbeb63fe29e58eda8e64e04afaf9a59f1 Mon Sep 17 00:00:00 2001 From: Kanchan-Microsoft Date: Tue, 16 Jun 2026 14:09:16 +0530 Subject: [PATCH 1/4] updated imagetag --- infra/bicep/main.bicep | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 22bf3b095..c85d272ab 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -122,7 +122,7 @@ param frontendImageName string = 'content-gen-app' param backendImageName string = 'content-gen-api' @description('Optional. Image tag. ACI is only deployed when a real tag (not empty / not "none") is provided.') -param imageTag string = '' +param imageTag string = 'latest' @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true @@ -529,7 +529,7 @@ module webSite 'modules/compute/app-service.bicep' = { // ========== Container Instance (Backend API) ========== // // Docker (bicep) flavor: inline ACI definition with managed identity auth for the created ACR. var containerInstanceName = 'aci-${solutionSuffix}' -var backendImageUrl = '${containerRegistry.outputs.loginServer}/${backendImageName}:${imageTag}' +var backendImageUrl = '${acrName}/${backendImageName}:${imageTag}' var aciPort = 8000 // Construct identity resource ID from known values (required for deployment-time calculation) var userAssignedIdentityResourceIdForACI = '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/${userAssignedIdentityResourceName}' @@ -722,7 +722,7 @@ output CONTAINER_INSTANCE_NAME string = shouldDeployACI ? containerInstance!.nam output CONTAINER_INSTANCE_FQDN string = shouldDeployACI ? containerInstance!.properties.ipAddress.fqdn : '' @description('Contains ACR Name') -output AZURE_ENV_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name +output AZURE_ENV_CONTAINER_REGISTRY_NAME string = acrName @description('Contains flag for Azure AI Foundry usage') output USE_FOUNDRY bool = useFoundryMode ? true : false From 833b81b2294d56dab48472431008ec96059cb7c6 Mon Sep 17 00:00:00 2001 From: nagshetti Date: Tue, 16 Jun 2026 19:08:38 +0530 Subject: [PATCH 2/4] take latest from repo --- .../ai/ai-foundry-model-deployment.bicep | 85 ++-- infra/avm/modules/ai/ai-foundry-project.bicep | 159 ++++-- infra/avm/modules/ai/ai-search.bicep | 138 +++-- .../modules/ai/existing-project-setup.bicep | 3 + .../modules/compute/app-service-plan.bicep | 56 ++- infra/avm/modules/compute/app-service.bicep | 473 +++++------------- .../modules/compute/container-instance.bicep | 162 +++--- .../modules/compute/container-registry.bicep | 120 +++-- infra/avm/modules/compute/kubernetes.bicep | 6 + .../avm/modules/compute/virtual-machine.bicep | 169 ++++--- infra/avm/modules/data/cosmos-db-nosql.bicep | 159 +++--- infra/avm/modules/data/storage-account.bicep | 162 +++--- .../avm/modules/monitoring/app-insights.bicep | 72 ++- .../monitoring/data-collection-rule.bicep | 133 +++-- .../modules/monitoring/log-analytics.bicep | 81 ++- .../avm/modules/networking/bastion-host.bicep | 87 ++-- .../modules/networking/private-dns-zone.bicep | 38 +- .../modules/networking/private-endpoint.bicep | 52 +- .../modules/networking/virtual-network.bicep | 293 ++++++----- .../ai/ai-foundry-model-deployment.bicep | 87 ++-- .../bicep/modules/ai/ai-foundry-project.bicep | 133 +++-- .../bicep/modules/ai/ai-search-identity.bicep | 8 + infra/bicep/modules/ai/ai-search.bicep | 127 +++-- .../modules/ai/existing-project-setup.bicep | 3 + .../modules/compute/app-service-plan.bicep | 50 +- infra/bicep/modules/compute/app-service.bicep | 247 ++++----- .../modules/compute/container-instance.bicep | 111 ++-- .../modules/compute/container-registry.bicep | 89 ++-- infra/bicep/modules/compute/kubernetes.bicep | 6 + .../bicep/modules/data/cosmos-db-nosql.bicep | 136 ++--- .../bicep/modules/data/storage-account.bicep | 121 +++-- .../modules/monitoring/app-insights.bicep | 74 ++- .../modules/monitoring/log-analytics.bicep | 53 +- 33 files changed, 2093 insertions(+), 1600 deletions(-) diff --git a/infra/avm/modules/ai/ai-foundry-model-deployment.bicep b/infra/avm/modules/ai/ai-foundry-model-deployment.bicep index 96595e093..1c534fd88 100644 --- a/infra/avm/modules/ai/ai-foundry-model-deployment.bicep +++ b/infra/avm/modules/ai/ai-foundry-model-deployment.bicep @@ -1,35 +1,64 @@ -@description('Required. Name of the AI Services account.') -param aiServicesName string +// ============================================================================ +// Module: Model Deployment +// Description: Deploys a single AI model to an existing AI Services account. +// Called repetitively from main.bicep for each model in the array. +// Generic, reusable across GSAs. +// ============================================================================ -@description('Required. Array of model deployments to create.') -param deployments array = [] +@description('Required. Name of the parent AI Services account.') +param aiServicesAccountName string -// Reference AI Services account (module is scoped to the correct resource group) -resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { - name: aiServicesName +@description('Required. Name for this model deployment.') +param deploymentName string + +@description('Optional. Model format (e.g., OpenAI).') +param modelFormat string = 'OpenAI' + +@description('Required. Model name (e.g., gpt-4o, text-embedding-ada-002).') +param modelName string + +@description('Optional. Model version. Empty string means latest.') +param modelVersion string = '' + +@description('Optional. RAI policy name.') +param raiPolicyName string = 'Microsoft.Default' + +@description('Required. SKU name (e.g., Standard, GlobalStandard).') +param skuName string + +@description('Required. SKU capacity (tokens per minute in thousands).') +param skuCapacity int + +// ============================================================================ +// Model Deployment +// ============================================================================ +resource aiServicesAccount 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { + name: aiServicesAccountName } -// Deploy models to AI Services account -// Using batchSize(1) to avoid concurrent deployment issues -@batchSize(1) -resource modelDeployments 'Microsoft.CognitiveServices/accounts/deployments@2025-12-01' = [ - for (deployment, index) in deployments: { - parent: aiServices - name: deployment.name - properties: { - model: { - format: deployment.format - name: deployment.model - version: deployment.version - } - raiPolicyName: deployment.raiPolicyName - } - sku: { - name: deployment.sku.name - capacity: deployment.sku.capacity +resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-12-01' = { + parent: aiServicesAccount + name: deploymentName + properties: { + model: { + format: modelFormat + name: modelName + version: !empty(modelVersion) ? modelVersion : null } + raiPolicyName: raiPolicyName + } + sku: { + name: skuName + capacity: skuCapacity } -] +} + +// ============================================================================ +// Outputs +// ============================================================================ + +@description('Name of the deployed model.') +output name string = modelDeployment.name -@description('The names of the deployed models.') -output deployedModelNames array = [for (deployment, i) in deployments: modelDeployments[i].name] +@description('Resource ID of the model deployment.') +output resourceId string = modelDeployment.id diff --git a/infra/avm/modules/ai/ai-foundry-project.bicep b/infra/avm/modules/ai/ai-foundry-project.bicep index 4c425e99e..69fc4fa7c 100644 --- a/infra/avm/modules/ai/ai-foundry-project.bicep +++ b/infra/avm/modules/ai/ai-foundry-project.bicep @@ -1,58 +1,141 @@ -@description('Required. Name of the AI Services project.') -param name string +// ============================================================================ +// Module: AI Foundry Project (Account + Project) +// Description: AVM wrapper for Azure AI Services account creation and +// AI Foundry project provisioning. Generic, reusable across GSAs. +// AVM Module: avm/res/cognitive-services/account +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-openai +// ============================================================================ -@description('Required. The location of the Project resource.') -param location string = resourceGroup().location +@description('Required. Solution name suffix used to generate resource names.') +param solutionName string -@description('Optional. The description of the AI Foundry project to create. Defaults to the project name.') -param desc string = name +@description('Optional. Override name for the AI Services account. Defaults to aif-{solutionName}.') +param name string = 'aif-${solutionName}' -@description('Required. Name of the existing Cognitive Services resource to create the AI Foundry project in.') -param aiServicesName string +@description('Optional. Override name for the AI Foundry project. Defaults to proj-{solutionName}.') +param projectName string = 'proj-${solutionName}' -@description('Required. Azure Existing AI Project ResourceID.') -param azureExistingAIProjectResourceId string = '' +@description('Required. Azure region for the resources.') +param location string -@description('Optional. Tags to be applied to the resources.') +@description('Optional. Tags to apply to resources.') param tags object = {} -var useExistingAiFoundryAiProject = !empty(azureExistingAIProjectResourceId) -var existingOpenAIEndpoint = useExistingAiFoundryAiProject - ? format('https://{0}.openai.azure.com/', split(azureExistingAIProjectResourceId, '/')[8]) - : '' +@description('Optional. SKU name for the AI Services account.') +param skuName string = 'S0' -// Reference to cognitive service in current resource group for new projects -resource cogServiceReference 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { - name: aiServicesName +@description('Optional. Whether to disable local (key-based) authentication.') +param disableLocalAuth bool = true + +@description('Optional. Whether to allow project management (AI Foundry hub).') +param allowProjectManagement bool = true + +@description('Optional. Public network access setting.') +param publicNetworkAccess string = 'Enabled' + +@description('Optional. Managed identity type for the resources.') +@allowed(['SystemAssigned', 'UserAssigned', 'SystemAssigned, UserAssigned', 'None']) +param identityType string = 'SystemAssigned' + +@description('Optional. Network ACLs default action.') +@allowed(['Allow', 'Deny']) +param networkAclsDefaultAction string = 'Allow' + +// --- WAF: Monitoring --- +@description('Optional. Diagnostic settings for the resource.') +param diagnosticSettings array? + +// --- WAF: Telemetry --- +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +// --- Role Assignments --- +@description('Optional. Array of role assignments to create on the AI Services account.') +param roleAssignments array? + +// ============================================================================ +// AI Services Account (AVM Module) +// ============================================================================ +module aiServicesAccount 'br/public:avm/res/cognitive-services/account:0.14.2' = { + name: take('avm.res.cognitive-services.account.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + sku: skuName + kind: 'AIServices' + disableLocalAuth: disableLocalAuth + allowProjectManagement: allowProjectManagement + customSubDomainName: name + networkAcls: { + defaultAction: networkAclsDefaultAction + virtualNetworkRules: [] + ipRules: [] + } + publicNetworkAccess: publicNetworkAccess + managedIdentities: { + systemAssigned: true + } + diagnosticSettings: diagnosticSettings + deployments: [] + roleAssignments: roleAssignments + // Private endpoints deployed separately to avoid AccountProvisioningStateInvalid + privateEndpoints: [] + } } -resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-12-01' = { - parent: cogServiceReference +// ============================================================================ +// AI Foundry Project +// ============================================================================ +resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { name: name - tags: tags + dependsOn: [aiServicesAccount] +} + +resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-12-01' = { + parent: aiServices + name: projectName location: location + tags: tags + kind: 'AIServices' identity: { - type: 'SystemAssigned' - } - properties: { - description: desc - displayName: name + type: identityType } + properties: {} + dependsOn: [aiServicesAccount] } -@description('Required. Name of the AI project.') -output name string = aiProject.name +// ============================================================================ +// Outputs +// ============================================================================ + +@description('Resource ID of the AI Services account.') +output resourceId string = aiServices.id + +@description('Name of the AI Services account.') +output name string = aiServices.name + +@description('Endpoint of the AI Services account (OpenAI Language Model Instance API).') +output endpoint string = aiServices.properties.endpoints['OpenAI Language Model Instance API'] + +@description('Endpoint of the AI Services account (Cognitive Services).') +output cognitiveServicesEndpoint string = aiServices.properties.endpoint + +@description('Azure OpenAI Content Understanding endpoint URL.') +output azureOpenAiCuEndpoint string = aiServices.properties.endpoints['Content Understanding'] + +@description('System-assigned identity principal ID of the AI Services account.') +output principalId string = aiServices.identity.principalId -@description('Required. Resource ID of the AI project.') -output resourceId string = aiProject.id +@description('Resource ID of the AI Foundry project.') +output projectResourceId string = aiProject.id -@description('Required. API endpoint for the AI project.') -output apiEndpoint string = aiProject!.properties.endpoints['AI Foundry API'] +@description('Name of the AI Foundry project.') +output projectName string = aiProject.name -@description('Contains AI Endpoint.') -output aoaiEndpoint string = !empty(existingOpenAIEndpoint) - ? existingOpenAIEndpoint - : cogServiceReference.properties.endpoints['OpenAI Language Model Instance API'] +@description('AI Foundry project endpoint.') +output projectEndpoint string = aiProject.properties.endpoints['AI Foundry API'] -@description('Required. Principal ID of the AI project system-assigned managed identity.') -output systemAssignedMIPrincipalId string = aiProject.identity.principalId +@description('System-assigned identity principal ID of the project.') +output projectIdentityPrincipalId string = aiProject.identity.principalId diff --git a/infra/avm/modules/ai/ai-search.bicep b/infra/avm/modules/ai/ai-search.bicep index 105392478..aa0843542 100644 --- a/infra/avm/modules/ai/ai-search.bicep +++ b/infra/avm/modules/ai/ai-search.bicep @@ -1,67 +1,129 @@ // ============================================================================ // Module: AI Search -// Description: AVM wrapper for an Azure AI Search service. +// Description: Deploys Azure AI Search with a two-step pattern: +// Step 1: Plain Bicep resource for fast initial creation (name, location, SKU) +// Step 2: AVM module update to enable managed identity & full configuration +// This reduces deployment time by making the resource available immediately +// while identity enablement proceeds separately. // AVM Module: avm/res/search/search-service:0.12.0 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-cognitive-search // ============================================================================ -@description('Required. Name of the AI Search service.') -param name string +@description('Solution name suffix used to derive the resource name.') +@minLength(3) +param solutionName string -@description('Required. Azure region for the resource.') +@description('Optional. Override name for the search service. Defaults to srch-{solutionName}.') +param name string = 'srch-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} +@description('SKU name for the search service.') +@allowed(['free', 'basic', 'standard', 'standard2', 'standard3', 'storage_optimized_l1', 'storage_optimized_l2']) +param skuName string = 'basic' + +@description('Number of replicas.') +param replicaCount int = 1 + +@description('Number of partitions.') +param partitionCount int = 1 + +@description('Hosting mode.') +@allowed(['Default', 'HighDensity']) +param hostingMode string = 'Default' + +@description('Semantic search tier.') +@allowed(['disabled', 'free', 'standard']) +param semanticSearch string = 'free' + +@description('Whether to disable local authentication.') +param disableLocalAuth bool = true + +@description('Optional. Authentication options for the search service (e.g., aadOrApiKey).') +param authOptions object = {} + +@description('Optional. Network rule set for the search service (e.g., bypass: AzureServices).') +param networkRuleSet object = {} + +@description('Managed identity type for the search service.') +param managedIdentityType string = 'SystemAssigned' + +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' + +// --- WAF: Telemetry --- @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Optional. SKU of the AI Search service.') -param sku string = 'basic' +// --- WAF: Monitoring --- +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] -@description('Optional. Number of replicas.') -param replicaCount int = 1 +// --- WAF: Private Networking --- +@description('Private endpoint configurations.') +param privateEndpoints array = [] -@description('Required. Principal ID of the managed identity to grant search data/service roles.') -param principalId string +// --- Role Assignments --- +@description('Optional. Array of role assignments to create on the AI Search service.') +param roleAssignments array = [] -@description('Optional. Diagnostic settings for monitoring.') -param diagnosticSettings array = [] +// ============================================================================ +// Step 1: Initial resource creation (plain Bicep — fast) +// ============================================================================ +resource searchService 'Microsoft.Search/searchServices@2025-05-01' = { + name: name + location: location + sku: { + name: skuName + } +} -module search 'br/public:avm/res/search/search-service:0.12.0' = { - name: take('avm.res.search.search-service.${name}', 64) +// ============================================================================ +// Step 2: AVM update — enables identity & full configuration +// ============================================================================ +module searchServiceUpdate 'br/public:avm/res/search/search-service:0.12.0' = { + name: take('avm.res.search.update.${name}', 64) params: { name: name location: location tags: tags enableTelemetry: enableTelemetry - sku: sku + sku: skuName replicaCount: replicaCount - partitionCount: 1 - hostingMode: 'Default' - semanticSearch: 'free' - managedIdentities: { systemAssigned: true } - disableLocalAuth: true - roleAssignments: [ - { - principalId: principalId - roleDefinitionIdOrName: 'Search Index Data Contributor' - principalType: 'ServicePrincipal' - } - { - principalId: principalId - roleDefinitionIdOrName: 'Search Service Contributor' - principalType: 'ServicePrincipal' - } - ] - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : null - // AI Search remains publicly accessible - accessed from ACI via managed identity - publicNetworkAccess: 'Enabled' + partitionCount: partitionCount + hostingMode: hostingMode + semanticSearch: semanticSearch + authOptions: !empty(authOptions) ? authOptions : null + disableLocalAuth: disableLocalAuth + networkRuleSet: !empty(networkRuleSet) ? networkRuleSet : null + publicNetworkAccess: publicNetworkAccess + managedIdentities: { + systemAssigned: managedIdentityType == 'SystemAssigned' + } + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + privateEndpoints: privateEndpoints + roleAssignments: !empty(roleAssignments) ? roleAssignments : [] } + dependsOn: [ + searchService + ] } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the AI Search service.') -output resourceId string = search.outputs.resourceId +output resourceId string = searchService.id @description('Name of the AI Search service.') -output name string = search.outputs.name +output name string = searchService.name + +@description('Endpoint URL of the AI Search service.') +output endpoint string = 'https://${searchService.name}.search.windows.net' + +@description('System-assigned identity principal ID.') +output identityPrincipalId string = searchServiceUpdate.outputs.?systemAssignedMIPrincipalId ?? '' diff --git a/infra/avm/modules/ai/existing-project-setup.bicep b/infra/avm/modules/ai/existing-project-setup.bicep index efdc5753f..cd0fe1f2c 100644 --- a/infra/avm/modules/ai/existing-project-setup.bicep +++ b/infra/avm/modules/ai/existing-project-setup.bicep @@ -37,6 +37,9 @@ output name string = aiServices.name @description('Endpoint of the AI Services account (OpenAI Language Model Instance API).') output endpoint string = aiServices.properties.endpoints['OpenAI Language Model Instance API'] +@description('Endpoint of the AI Services account (Cognitive Services).') +output cognitiveServicesEndpoint string = aiServices.properties.endpoint + @description('Azure OpenAI Content Understanding endpoint URL.') output azureOpenAiCuEndpoint string = aiServices.properties.endpoints['Content Understanding'] diff --git a/infra/avm/modules/compute/app-service-plan.bicep b/infra/avm/modules/compute/app-service-plan.bicep index 8a704cf4f..6e9e72d0c 100644 --- a/infra/avm/modules/compute/app-service-plan.bicep +++ b/infra/avm/modules/compute/app-service-plan.bicep @@ -1,51 +1,67 @@ // ============================================================================ // Module: App Service Plan -// Description: AVM wrapper for an Azure App Service Plan (Linux). +// Description: AVM wrapper for Azure App Service Plan // AVM Module: avm/res/web/serverfarm:0.7.0 // ============================================================================ -@description('Required. Name of the App Service Plan.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Name of the App Service Plan.') +param name string = 'asp-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} +@description('SKU name for the App Service Plan.') +@allowed(['F1', 'D1', 'B1', 'B2', 'B3', 'S1', 'S2', 'S3', 'P1', 'P2', 'P3', 'P4', 'P0v3', 'P0v4', 'P1v3', 'P1v4', 'P2v3', 'P3v3']) +param skuName string = 'B2' + +@description('Whether the plan is Linux-based.') +param reserved bool = true + +@description('Kind of the App Service Plan.') +param kind string = 'linux' + @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Optional. SKU name of the App Service Plan.') -param skuName string = 'B1' - -@description('Optional. Number of instances.') +@description('Number of instances (workers).') param skuCapacity int = 1 -@description('Optional. Enable zone redundancy.') -param zoneRedundant bool = false - -@description('Optional. Diagnostic settings for monitoring.') +@description('Diagnostic settings for monitoring.') param diagnosticSettings array = [] -module serverFarm 'br/public:avm/res/web/serverfarm:0.7.0' = { +@description('Enable zone redundancy. Requires Premium SKU (P1v3+).') +param zoneRedundant bool = false + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module appServicePlan 'br/public:avm/res/web/serverfarm:0.7.0' = { name: take('avm.res.web.serverfarm.${name}', 64) params: { name: name + location: location tags: tags enableTelemetry: enableTelemetry - location: location - reserved: true - kind: 'linux' - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : null skuName: skuName skuCapacity: skuCapacity + reserved: reserved + kind: kind + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] zoneRedundant: zoneRedundant } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the App Service Plan.') -output resourceId string = serverFarm.outputs.resourceId +output resourceId string = appServicePlan.outputs.resourceId @description('Name of the App Service Plan.') -output name string = serverFarm.outputs.name +output name string = appServicePlan.outputs.name diff --git a/infra/avm/modules/compute/app-service.bicep b/infra/avm/modules/compute/app-service.bicep index b9c2ac36b..bbbcbf68b 100644 --- a/infra/avm/modules/compute/app-service.bicep +++ b/infra/avm/modules/compute/app-service.bicep @@ -1,374 +1,169 @@ -@description('Required. Name of the site.') -param name string +// ============================================================================ +// Module: App Service +// Description: AVM wrapper for Azure App Service (Web App) +// AVM Module: avm/res/web/site:0.23.1 +// ============================================================================ -@description('Optional. Location for all Resources.') -param location string = resourceGroup().location +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Type of site to deploy.') -@allowed([ - 'functionapp' - 'functionapp,linux' - 'functionapp,workflowapp' - 'functionapp,workflowapp,linux' - 'functionapp,linux,container' - 'functionapp,linux,container,azurecontainerapps' - 'app,linux' - 'app' - 'linux,api' - 'api' - 'app,linux,container' - 'app,container,windows' -]) -param kind string +@description('Name of the App Service.') +param name string = solutionName + +@description('Azure region for the resource.') +param location string -@description('Required. The resource ID of the app service plan to use for the site.') +@description('Tags to apply to the resource.') +param tags object = {} + +@description('Resource ID of the App Service Plan.') param serverFarmResourceId string -@description('Optional. Azure Resource Manager ID of the customers selected Managed Environment on which to host this app.') -param managedEnvironmentId string? +@description('Docker image name (e.g., DOCKER|registry.azurecr.io/image:tag).') +param linuxFxVersion string -@description('Optional. Configures a site to accept only HTTPS requests.') -param httpsOnly bool = true +@description('Application settings key-value pairs.') +param appSettings object = {} -@description('Optional. If client affinity is enabled.') -param clientAffinityEnabled bool = true +@description('Optional. Resource ID of Application Insights for monitoring integration.') +param applicationInsightResourceId string = '' -@description('Optional. The resource ID of the app service environment to use for this resource.') -param appServiceEnvironmentResourceId string? +@description('Whether to enable Always On.') +param alwaysOn bool = true -import { managedIdentityAllType } from 'br/public:avm/utl/types/avm-common-types:0.7.0' -@description('Optional. The managed identity definition for this resource.') -param managedIdentities managedIdentityAllType? +@description('Optional. Health check path for the app.') +param healthCheckPath string = '' -@description('Optional. The resource ID of the assigned identity to be used to access a key vault with.') -param keyVaultAccessIdentityResourceId string? +@description('Optional. Whether to enable WebSockets.') +param webSocketsEnabled bool = false -@description('Optional. Checks if Customer provided storage account is required.') -param storageAccountRequired bool = false +@description('Optional. Command line for the application.') +param appCommandLine string = '' -@description('Optional. Enable monitoring and logging configuration.') -param enableMonitoring bool = false +@description('Required. Type of site to deploy.') +@allowed([ + 'functionapp' // function app windows os + 'functionapp,linux' // function app linux os + 'functionapp,workflowapp' // logic app workflow + 'functionapp,workflowapp,linux' // logic app docker container + 'functionapp,linux,container' // function app linux container + 'functionapp,linux,container,azurecontainerapps' // function app linux container azure container apps + 'app,linux' // linux web app + 'app' // windows web app + 'linux,api' // linux api app + 'api' // windows api app + 'app,linux,container' // linux container app + 'app,container,windows' // windows container app +]) +param kind string = 'app,linux' @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Optional. Azure Resource Manager ID of the Virtual network and subnet to be joined by Regional VNET Integration.') -param virtualNetworkSubnetId string? +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] -@description('Optional. To enable accessing content over virtual network.') -param vnetContentShareEnabled bool = false +@description('Subnet resource ID for VNet integration.') +param virtualNetworkSubnetId string = '' -@description('Optional. To enable pulling image over Virtual Network.') -param vnetImagePullEnabled bool = false +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' -@description('Optional. Virtual Network Route All enabled.') +@description('Optional. Whether to route all outbound traffic through the virtual network.') param vnetRouteAllEnabled bool = false -@description('Optional. Stop SCM (KUDU) site when the app is stopped.') -param scmSiteAlsoStopped bool = false +@description('Optional. Whether to route image pull traffic through the virtual network.') +param imagePullTraffic bool = false -@description('Optional. The site config object.') -param siteConfig resourceInput<'Microsoft.Web/sites@2025-03-01'>.properties.siteConfig = { - alwaysOn: true - minTlsVersion: '1.2' - ftpsState: 'FtpsOnly' -} - -@description('Optional. The web site config.') -param configs appSettingsConfigType[]? +@description('Optional. Whether to route content share traffic through the virtual network.') +param contentShareTraffic bool = false -@description('Optional. The Function App configuration object.') -param functionAppConfig resourceInput<'Microsoft.Web/sites@2025-03-01'>.properties.functionAppConfig? - -import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:0.7.0' -@description('Optional. Configuration details for private endpoints.') +import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.') param privateEndpoints privateEndpointSingleServiceType[]? -@description('Optional. Tags of the resource.') -param tags object? - -import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:0.7.0' -@description('Optional. The diagnostic settings of the service.') -param diagnosticSettings diagnosticSettingFullType[]? - -@description('Optional. To enable client certificate authentication (TLS mutual authentication).') -param clientCertEnabled bool = false - -@description('Optional. Client certificate authentication comma-separated exclusion paths.') -param clientCertExclusionPaths string? - -@description('Optional. Client certificate mode.') -@allowed([ - 'Optional' - 'OptionalInteractiveUser' - 'Required' -]) -param clientCertMode string = 'Optional' - -@description('Optional. If specified during app creation, the app is cloned from a source app.') -param cloningInfo resourceInput<'Microsoft.Web/sites@2025-03-01'>.properties.cloningInfo? - -@description('Optional. Size of the function container.') -param containerSize int? - -@description('Optional. Maximum allowed daily memory-time quota (applicable on dynamic apps only).') -param dailyMemoryTimeQuota int? - -@description('Optional. Setting this value to false disables the app (takes the app offline).') -param enabled bool = true - -@description('Optional. Hostname SSL states are used to manage the SSL bindings for app\'s hostnames.') -param hostNameSslStates resourceInput<'Microsoft.Web/sites@2025-03-01'>.properties.hostNameSslStates? - -@description('Optional. Hyper-V sandbox.') -param hyperV bool = false - -@description('Optional. Site redundancy mode.') -@allowed([ - 'ActiveActive' - 'Failover' - 'GeoRedundant' - 'Manual' - 'None' -]) -param redundancyMode string = 'None' - -@description('Optional. Whether or not public network access is allowed for this resource.') -@allowed([ - 'Enabled' - 'Disabled' -]) -param publicNetworkAccess string? - -@description('Optional. End to End Encryption Setting.') -param e2eEncryptionEnabled bool? - -@description('Optional. Property to configure various DNS related settings for a site.') -param dnsConfiguration resourceInput<'Microsoft.Web/sites@2025-03-01'>.properties.dnsConfiguration? - -@description('Optional. Specifies the scope of uniqueness for the default hostname during resource creation.') -@allowed([ - 'NoReuse' - 'ResourceGroupReuse' - 'SubscriptionReuse' - 'TenantReuse' -]) -param autoGeneratedDomainNameLabelScope string? - -var formattedUserAssignedIdentities = reduce( - map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }), - {}, - (cur, next) => union(cur, next) -) - -var identity = !empty(managedIdentities) - ? { - type: (managedIdentities.?systemAssigned ?? false) - ? (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned') - : (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : 'None') - userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module appService 'br/public:avm/res/web/site:0.23.1' = { + name: take('avm.res.web.site.${name}', 64) + params: { + name: name + location: location + tags: tags + kind: kind + enableTelemetry: enableTelemetry + serverFarmResourceId: serverFarmResourceId + managedIdentities: { + systemAssigned: true } - : null - -// Merge vnet properties into siteConfig (these properties moved from top-level to siteConfig in newer API versions) -var mergedSiteConfig = union(siteConfig, { - vnetRouteAllEnabled: vnetRouteAllEnabled - vnetImagePullEnabled: vnetImagePullEnabled - vnetContentShareEnabled: vnetContentShareEnabled -}) - -resource app 'Microsoft.Web/sites@2025-03-01' = { - name: name - location: location - kind: kind - tags: tags - identity: identity - properties: { - managedEnvironmentId: !empty(managedEnvironmentId) ? managedEnvironmentId : null - serverFarmId: serverFarmResourceId - clientAffinityEnabled: clientAffinityEnabled - httpsOnly: httpsOnly - hostingEnvironmentProfile: !empty(appServiceEnvironmentResourceId) - ? { - id: appServiceEnvironmentResourceId - } - : null - storageAccountRequired: storageAccountRequired - keyVaultReferenceIdentity: keyVaultAccessIdentityResourceId - virtualNetworkSubnetId: virtualNetworkSubnetId - siteConfig: mergedSiteConfig - functionAppConfig: functionAppConfig - clientCertEnabled: clientCertEnabled - clientCertExclusionPaths: clientCertExclusionPaths - clientCertMode: clientCertMode - cloningInfo: cloningInfo - containerSize: containerSize - dailyMemoryTimeQuota: dailyMemoryTimeQuota - enabled: enabled - hostNameSslStates: hostNameSslStates - hyperV: hyperV - redundancyMode: redundancyMode - publicNetworkAccess: !empty(publicNetworkAccess) - ? any(publicNetworkAccess) - : (!empty(privateEndpoints) ? 'Disabled' : 'Enabled') - scmSiteAlsoStopped: scmSiteAlsoStopped - endToEndEncryptionEnabled: e2eEncryptionEnabled - dnsConfiguration: dnsConfiguration - autoGeneratedDomainNameLabelScope: autoGeneratedDomainNameLabelScope - } -} - -module app_config 'app-service.config.bicep' = [ - for (config, index) in (configs ?? []): { - name: '${uniqueString(deployment().name, location)}-Site-Config-${index}' - params: { - appName: app.name - name: config.name - applicationInsightResourceId: config.?applicationInsightResourceId - storageAccountResourceId: config.?storageAccountResourceId - storageAccountUseIdentityAuthentication: config.?storageAccountUseIdentityAuthentication - properties: config.?properties - currentAppSettings: config.?retainCurrentAppSettings ?? true && config.name == 'appsettings' - ? list('${app.id}/config/appsettings', '2023-12-01').properties - : {} - enableMonitoring: enableMonitoring + siteConfig: { + alwaysOn: alwaysOn + ftpsState: 'Disabled' + linuxFxVersion: linuxFxVersion + minTlsVersion: '1.2' + healthCheckPath: !empty(healthCheckPath) ? healthCheckPath : null + webSocketsEnabled: webSocketsEnabled + appCommandLine: appCommandLine } - } -] - -#disable-next-line use-recent-api-versions -resource app_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = [ - for (diagnosticSetting, index) in (diagnosticSettings ?? []): { - name: diagnosticSetting.?name ?? '${name}-diagnosticSettings' - properties: { - storageAccountId: diagnosticSetting.?storageAccountResourceId - workspaceId: diagnosticSetting.?workspaceResourceId - eventHubAuthorizationRuleId: diagnosticSetting.?eventHubAuthorizationRuleResourceId - eventHubName: diagnosticSetting.?eventHubName - metrics: [ - for group in (diagnosticSetting.?metricCategories ?? [{ category: 'AllMetrics' }]): { - category: group.category - enabled: group.?enabled ?? true - timeGrain: null - } - ] - logs: [ - for group in (diagnosticSetting.?logCategoriesAndGroups ?? [{ categoryGroup: 'allLogs' }]): { - categoryGroup: group.?categoryGroup - category: group.?category - enabled: group.?enabled ?? true + e2eEncryptionEnabled: true + configs: [ + { + name: 'appsettings' + properties: appSettings + applicationInsightResourceId: !empty(applicationInsightResourceId) ? applicationInsightResourceId : null + } + { + name: 'logs' + properties: { + applicationLogs: { fileSystem: { level: 'Verbose' } } + detailedErrorMessages: { enabled: true } + failedRequestsTracing: { enabled: true } + httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } } - ] - marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId - logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType + } + { + name:'web' + properties: { + vnetRouteAllEnabled: vnetRouteAllEnabled + } + } + ] + outboundVnetRouting: { + contentShareTraffic: contentShareTraffic + imagePullTraffic: imagePullTraffic } - scope: app + publicNetworkAccess: publicNetworkAccess + privateEndpoints: privateEndpoints + virtualNetworkSubnetResourceId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null + basicPublishingCredentialsPolicies: [ + { + name: 'ftp' + allow: false + } + { + name: 'scm' + allow: false + } + ] + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] } -] - -module app_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.12.0' = [ - for (privateEndpoint, index) in (privateEndpoints ?? []): { - name: '${uniqueString(deployment().name, location)}-app-PrivateEndpoint-${index}' - scope: resourceGroup( - split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[2], - split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[4] - ) - params: { - name: privateEndpoint.?name ?? 'pep-${last(split(app.id, '/'))}-${privateEndpoint.?service ?? 'sites'}-${index}' - privateLinkServiceConnections: privateEndpoint.?isManualConnection != true - ? [ - { - name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(app.id, '/'))}-${privateEndpoint.?service ?? 'sites'}-${index}' - properties: { - privateLinkServiceId: app.id - groupIds: [ - privateEndpoint.?service ?? 'sites' - ] - } - } - ] - : null - manualPrivateLinkServiceConnections: privateEndpoint.?isManualConnection == true - ? [ - { - name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(app.id, '/'))}-${privateEndpoint.?service ?? 'sites'}-${index}' - properties: { - privateLinkServiceId: app.id - groupIds: [ - privateEndpoint.?service ?? 'sites' - ] - requestMessage: privateEndpoint.?manualConnectionRequestMessage ?? 'Manual approval required.' - } - } - ] - : null - subnetResourceId: privateEndpoint.subnetResourceId - enableTelemetry: enableTelemetry - location: privateEndpoint.?location ?? reference( - split(privateEndpoint.subnetResourceId, '/subnets/')[0], - '2020-06-01', - 'Full' - ).location - lock: privateEndpoint.?lock ?? null - privateDnsZoneGroup: privateEndpoint.?privateDnsZoneGroup - roleAssignments: privateEndpoint.?roleAssignments - tags: privateEndpoint.?tags ?? tags - customDnsConfigs: privateEndpoint.?customDnsConfigs - ipConfigurations: privateEndpoint.?ipConfigurations - applicationSecurityGroupResourceIds: privateEndpoint.?applicationSecurityGroupResourceIds - customNetworkInterfaceName: privateEndpoint.?customNetworkInterfaceName - } - } -] - -@description('The name of the site.') -output name string = app.name - -@description('The resource ID of the site.') -output resourceId string = app.id - -@description('The resource group the site was deployed into.') -output resourceGroupName string = resourceGroup().name - -@description('The principal ID of the system assigned identity.') -output systemAssignedMIPrincipalId string? = app.?identity.?principalId - -@description('The location the resource was deployed into.') -output location string = app.location - -@description('Default hostname of the app.') -output defaultHostname string = 'https://${name}.azurewebsites.net' - -@description('Unique identifier that verifies the custom domains assigned to the app.') -output customDomainVerificationId string = app.properties.customDomainVerificationId - -@description('The outbound IP addresses of the app.') -output outboundIpAddresses string = app.properties.outboundIpAddresses - -// ================ // -// Definitions // -// ================ // -@export() -@description('The type of an app settings configuration.') -type appSettingsConfigType = { - @description('Required. The type of config.') - name: 'appsettings' | 'logs' +} - @description('Optional. If the provided storage account requires Identity based authentication.') - storageAccountUseIdentityAuthentication: bool? +// ============================================================================ +// Outputs +// ============================================================================ +@description('Resource ID of the App Service.') +output resourceId string = appService.outputs.resourceId - @description('Optional. Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions.') - storageAccountResourceId: string? +@description('Name of the App Service.') +output name string = appService.outputs.name - @description('Optional. Resource ID of the application insight to leverage for this resource.') - applicationInsightResourceId: string? +@description('Default hostname of the App Service.') +output defaultHostname string = appService.outputs.defaultHostname - @description('Optional. The retain the current app settings. Defaults to true.') - retainCurrentAppSettings: bool? +@description('URL of the App Service.') +output appUrl string = 'https://${appService.outputs.defaultHostname}' - @description('Optional. The app settings key-value pairs.') - properties: { - @description('Required. An app settings key-value pair.') - *: string - }? -} +@description('System-assigned identity principal ID.') +output identityPrincipalId string = appService.outputs.?systemAssignedMIPrincipalId ?? '' diff --git a/infra/avm/modules/compute/container-instance.bicep b/infra/avm/modules/compute/container-instance.bicep index 9a6767a76..c840be20d 100644 --- a/infra/avm/modules/compute/container-instance.bicep +++ b/infra/avm/modules/compute/container-instance.bicep @@ -1,97 +1,96 @@ -// ========== container-instance.bicep ========== // -// Azure Container Instance module for backend API deployment +// ============================================================================ +// Module: Azure Container Instance (AVM) +// AVM Module: avm/res/container-instance/container-group:0.7.0 +// ============================================================================ -@description('Required. Name of the container group.') +@description('Name of the container group.') param name string -@description('Required. Location for the container instance.') +@description('Azure region for deployment.') param location string -@description('Optional. Tags for all resources.') +@description('Resource tags.') param tags object = {} -@description('Required. Container image to deploy.') +@description('Container image to deploy.') param containerImage string -@description('Optional. CPU cores for the container.') +@description('CPU cores for the container.') param cpu int = 2 -@description('Optional. Memory in GB for the container.') +@description('Memory in GB for the container.') param memoryInGB int = 4 -@description('Optional. Port to expose.') +@description('Port to expose.') param port int = 8000 -@description('Optional. Subnet resource ID for VNet integration. If empty, public IP will be used.') +@description('Environment variables for the container.') +param environmentVariables array = [] + +@description('Operating system type.') +@allowed(['Linux', 'Windows']) +param osType string = 'Linux' + +@description('Restart policy.') +@allowed(['Always', 'OnFailure', 'Never']) +param restartPolicy string = 'Always' + +@description('Managed identity configuration.') +param managedIdentities object = {} + +@description('Image registry credentials.') +param imageRegistryCredentials array = [] + +@description('Subnet resource ID for VNet integration. If empty, public IP is used.') param subnetResourceId string = '' -@description('Required. Environment variables for the container.') -param environmentVariables array +@description('Availability zone for the container group. Use -1 for no zone.') +param availabilityZone int = -1 -@description('Optional. Enable telemetry.') +@description('Enable Azure telemetry collection.') param enableTelemetry bool = true -@description('Required. User-assigned managed identity resource ID for ACR pull.') -param userAssignedIdentityResourceId string - +// ============================================================================ +// Variables +// ============================================================================ var isPrivateNetworking = !empty(subnetResourceId) -// ============== // -// Resources // -// ============== // - -#disable-next-line no-deployments-resources -resource avmTelemetry 'Microsoft.Resources/deployments@2025-04-01' = if (enableTelemetry) { - name: '46d3xbcp.res.containerinstance.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}' - properties: { - mode: 'Incremental' - template: { - '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' - contentVersion: '1.0.0.0' - resources: [] - } - } -} - -resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2025-09-01' = { - name: name - location: location - tags: tags - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${userAssignedIdentityResourceId}': {} - } - } - properties: { - containers: [ - { - name: name - properties: { - image: containerImage - resources: { - requests: { - cpu: cpu - memoryInGB: memoryInGB - } - } - ports: [ - { - port: port - protocol: 'TCP' - } - ] - environmentVariables: environmentVariables +var containers = [ + { + name: name + properties: { + image: containerImage + resources: { + requests: { + cpu: cpu + memoryInGB: string(memoryInGB) } } - ] - osType: 'Linux' - restartPolicy: 'Always' - subnetIds: isPrivateNetworking ? [ - { - id: subnetResourceId - } - ] : null + ports: [ + { + port: port + protocol: 'TCP' + } + ] + environmentVariables: environmentVariables + } + } +] + +// ============================================================================ +// Container Instance (AVM) +// ============================================================================ +module containerGroup 'br/public:avm/res/container-instance/container-group:0.7.0' = { + name: take('avm.res.containerinstance.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + containers: containers + osType: osType + restartPolicy: restartPolicy + managedIdentities: !empty(managedIdentities) ? managedIdentities : {} ipAddress: { type: isPrivateNetworking ? 'Private' : 'Public' ports: [ @@ -102,23 +101,20 @@ resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2025-09-01' ] dnsNameLabel: isPrivateNetworking ? null : name } - // Removed imageRegistryCredentials - ACR is public with anonymous pull enabled - // If you need managed identity auth, add AcrPull role to the managed identity on the ACR + imageRegistryCredentials: !empty(imageRegistryCredentials) ? imageRegistryCredentials : [] + subnets: isPrivateNetworking ? [{ subnetResourceId: subnetResourceId }] : [] + availabilityZone: availabilityZone } } -// ============== // -// Outputs // -// ============== // - +// ============================================================================ +// Outputs +// ============================================================================ @description('The name of the container group.') -output name string = containerGroup.name +output name string = containerGroup.outputs.name @description('The resource ID of the container group.') -output resourceId string = containerGroup.id - -@description('The IP address of the container (private or public depending on mode).') -output ipAddress string = containerGroup.properties.ipAddress.ip +output resourceId string = containerGroup.outputs.resourceId -@description('The FQDN of the container (only available for public mode).') -output fqdn string = isPrivateNetworking ? '' : containerGroup.properties.ipAddress.fqdn +@description('The IP address of the container group.') +output ipAddress string = containerGroup.outputs.?iPv4Address ?? '' diff --git a/infra/avm/modules/compute/container-registry.bicep b/infra/avm/modules/compute/container-registry.bicep index f2485e2ca..9e1783be4 100644 --- a/infra/avm/modules/compute/container-registry.bicep +++ b/infra/avm/modules/compute/container-registry.bicep @@ -1,53 +1,109 @@ // ============================================================================ -// Module: Container Registry -// Description: AVM wrapper for an Azure Container Registry used by the Docker -// build (bicep) deployment flavor. AZD builds and pushes the -// application images here. -// AVM Module: avm/res/container-registry/registry:0.9.0 +// Module: Azure Container Registry (AVM) +// AVM Module: avm/res/container-registry/registry:0.12.1 // ============================================================================ -@description('Required. Name of the container registry.') -param name string +@description('Solution name used for naming convention.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Name of the container registry.') +param name string = replace('cr${solutionName}', '-', '') + +@description('Azure region for deployment.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Resource tags.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') +@description('SKU for the container registry.') +@allowed(['Basic', 'Standard', 'Premium']) +param sku string = 'Premium' + +@description('Enable admin user for the registry.') +param adminUserEnabled bool = false + +@description('Public network access setting.') +@allowed(['Enabled', 'Disabled']) +param publicNetworkAccess string = 'Enabled' + +@description('Export policy status. Must be "enabled" when publicNetworkAccess is "Enabled".') +param exportPolicyStatus string = 'enabled' + +@description('Principal IDs to assign AcrPull role.') +param acrPullPrincipalIds array = [] + +@description('Enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs for private endpoint.') +param privateDnsZoneResourceIds array = [] + +@description('Default action for the network rule set. Use Allow when no private endpoint is in place; Deny for private-only.') +@allowed(['Allow', 'Deny']) +param networkRuleSetDefaultAction string = 'Allow' + +@description('Enable Azure telemetry collection.') param enableTelemetry bool = true -@description('Required. Principal ID of the managed identity to grant AcrPull.') -param principalId string +// ============================================================================ +// Role Assignments +// ============================================================================ +var acrPullRoleId = '7f951dda-4ed3-4680-a7ca-43fe172d538d' + +var roleAssignments = [for principalId in acrPullPrincipalIds: { + principalId: principalId + roleDefinitionIdOrName: acrPullRoleId + principalType: 'ServicePrincipal' +}] + +// ============================================================================ +// Private Endpoint Config +// ============================================================================ +var dnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'config${i}' + privateDnsZoneResourceId: zoneId +}] + +var privateEndpointConfig = enablePrivateNetworking && !empty(privateEndpointSubnetId) ? [ + { + subnetResourceId: privateEndpointSubnetId + privateDnsZoneGroup: !empty(privateDnsZoneResourceIds) ? { + privateDnsZoneGroupConfigs: dnsZoneConfigs + } : null + } +] : [] -module registry 'br/public:avm/res/container-registry/registry:0.9.0' = { - name: take('avm.res.container-registry.registry.${name}', 64) +// ============================================================================ +// Container Registry (AVM) +// ============================================================================ +module containerRegistry 'br/public:avm/res/container-registry/registry:0.12.1' = { + name: take('avm.res.containerregistry.${name}', 64) params: { name: name location: location tags: tags enableTelemetry: enableTelemetry - acrSku: 'Standard' - acrAdminUserEnabled: false - anonymousPullEnabled: false - publicNetworkAccess: 'Enabled' - networkRuleBypassOptions: 'AzureServices' - roleAssignments: [ - { - principalId: principalId - roleDefinitionIdOrName: '7f951dda-4ed3-4680-a7ca-43fe172d538d' // AcrPull - principalType: 'ServicePrincipal' - } - ] + acrSku: sku + acrAdminUserEnabled: adminUserEnabled + publicNetworkAccess: publicNetworkAccess + exportPolicyStatus: exportPolicyStatus + roleAssignments: !empty(acrPullPrincipalIds) ? roleAssignments : [] + privateEndpoints: privateEndpointConfig + networkRuleSetDefaultAction: networkRuleSetDefaultAction } } -@description('Resource ID of the container registry.') -output resourceId string = registry.outputs.resourceId +// ============================================================================ +// Outputs +// ============================================================================ +@description('The name of the container registry.') +output name string = containerRegistry.outputs.name -@description('Name of the container registry.') -output name string = registry.outputs.name +@description('The login server URL.') +output loginServer string = containerRegistry.outputs.loginServer -@description('Login server of the container registry.') -output loginServer string = registry.outputs.loginServer +@description('The resource ID of the container registry.') +output resourceId string = containerRegistry.outputs.resourceId diff --git a/infra/avm/modules/compute/kubernetes.bicep b/infra/avm/modules/compute/kubernetes.bicep index 54053dd4a..a15a362ef 100644 --- a/infra/avm/modules/compute/kubernetes.bicep +++ b/infra/avm/modules/compute/kubernetes.bicep @@ -156,3 +156,9 @@ output resourceId string = aksCluster.outputs.resourceId @description('FQDN of the AKS cluster.') output fqdn string = aksCluster.outputs.?fqdn ?? '' + +@description('Object ID of the AKS kubelet system-assigned managed identity (used by pods at runtime via IMDS).') +output kubeletIdentityObjectId string = aksCluster.outputs.?kubeletIdentityObjectId ?? '' + +@description('Principal ID of the AKS control-plane system-assigned managed identity.') +output systemAssignedMIPrincipalId string = aksCluster.outputs.?systemAssignedMIPrincipalId ?? '' diff --git a/infra/avm/modules/compute/virtual-machine.bicep b/infra/avm/modules/compute/virtual-machine.bicep index 3f13b1fd4..8b9c2a7e0 100644 --- a/infra/avm/modules/compute/virtual-machine.bicep +++ b/infra/avm/modules/compute/virtual-machine.bicep @@ -1,108 +1,157 @@ // ============================================================================ // Module: Virtual Machine (Jumpbox) -// Description: AVM wrapper for a Windows jumpbox VM used for private network -// administration via Azure Bastion. -// AVM Module: avm/res/compute/virtual-machine:0.21.0 +// Description: AVM wrapper for Azure Virtual Machine with Entra ID authentication +// AVM Module: avm/res/compute/virtual-machine +// Ref: https://learn.microsoft.com/azure/bastion/bastion-entra-id-authentication // ============================================================================ -@description('Required. Name of the virtual machine (max 15 chars).') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Name of the virtual machine.') +param name string = 'vm-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -@description('Optional. VM size.') +@description('VM size.') param vmSize string = 'Standard_D2s_v5' -@description('Required. Admin username for the VM.') +@secure() +@description('Local admin username. Required by Azure at provisioning time but not used for login when Entra ID is enabled.') param adminUsername string @secure() -@description('Required. Admin password for the VM.') +@description('Local admin password. Required by Azure at provisioning time but not used for login when Entra ID is enabled.') param adminPassword string -@description('Required. Resource ID of the user assigned managed identity.') -param userAssignedIdentityResourceId string - -@description('Required. Resource ID of the subnet to attach the NIC to.') +@description('Resource ID of the subnet for the VM NIC.') param subnetResourceId string -@description('Optional. Availability zone (-1 to disable).') -param availabilityZone int = -1 +@description('OS type for the VM.') +param osType string = 'Windows' + +@description('Availability zone for the VM.') +param availabilityZone int = 1 + +@description('Image reference for the VM.') +param imageReference object = { + publisher: 'microsoft-dsvm' + offer: 'dsvm-win-2022' + sku: 'winserver-2022' + version: 'latest' +} -@description('Optional. Enable monitoring agent and DCR association.') -param enableMonitoring bool = false +@description('OS disk size in GB.') +param osDiskSizeGB int = 128 -@description('Optional. Resource ID of the Data Collection Rule to associate.') -param dataCollectionRuleResourceId string = '' +@description('Resource ID of the maintenance configuration.') +param maintenanceConfigurationResourceId string? -module vm 'br/public:avm/res/compute/virtual-machine:0.21.0' = { +@description('Resource ID of the proximity placement group.') +param proximityPlacementGroupResourceId string? + +@description('Monitoring agent extension configuration (data collection rule associations).') +param extensionMonitoringAgentConfig object? + +@description('Diagnostic settings for the resource.') +param diagnosticSettings array? + +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + +@description('Deploying user principal ID. Used for default role assignment to grant the deploying user login access to the VM. This is required because with Entra ID authentication enabled, local accounts cannot be used to access the VM, including the local admin account created at provisioning.') +param deployingUserPrincipalId string + +@description('Deploying user principal type. Used for default role assignment to grant the deploying user login access to the VM. This is required because with Entra ID authentication enabled, local accounts cannot be used to access the VM, including the local admin account created at provisioning.') +param deployingUserPrincipalType string = 'User' + +@description('Role assignments to apply to the virtual machine.') +param roleAssignments array = [ + { + roleDefinitionIdOrName: '1c0163c0-47e6-4577-8991-ea5c82e286e4' // Virtual Machine Administrator Login + principalId: deployingUserPrincipalId + principalType: deployingUserPrincipalType + } +] + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.22.0' = { name: take('avm.res.compute.virtual-machine.${name}', 64) params: { - name: take(name, 15) + name: name + location: location + tags: tags enableTelemetry: enableTelemetry computerName: take(name, 15) - osType: 'Windows' + osType: osType vmSize: vmSize adminUsername: adminUsername adminPassword: adminPassword - managedIdentities: { - userAssignedResourceIds: [ - userAssignedIdentityResourceId - ] - } + managedIdentities: { systemAssigned: true } + patchMode: 'AutomaticByPlatform' + bypassPlatformSafetyChecksOnUserSchedule: true + maintenanceConfigurationResourceId: maintenanceConfigurationResourceId + enableAutomaticUpdates: true + encryptionAtHost: true availabilityZone: availabilityZone - imageReference: { - publisher: 'microsoft-dsvm' - offer: 'dsvm-win-2022' - sku: 'winserver-2022' - version: 'latest' + proximityPlacementGroupResourceId: proximityPlacementGroupResourceId + imageReference: imageReference + osDisk: { + name: 'osdisk-${name}' + caching: 'ReadWrite' + createOption: 'FromImage' + deleteOption: 'Delete' + diskSizeGB: osDiskSizeGB + managedDisk: { storageAccountType: 'Premium_LRS' } } nicConfigurations: [ { name: 'nic-${name}' - enableAcceleratedNetworking: true + tags: tags + deleteOption: 'Delete' + diagnosticSettings: diagnosticSettings ipConfigurations: [ { - name: 'ipconfig01' + name: '${name}-nic01-ipconfig01' subnetResourceId: subnetResourceId + diagnosticSettings: diagnosticSettings } ] } ] - osDisk: { - caching: 'ReadWrite' - diskSizeGB: 128 - managedDisk: { - storageAccountType: 'Premium_LRS' - } + roleAssignments: roleAssignments + extensionAadJoinConfig: { + enabled: true + tags: tags + typeHandlerVersion: '2.0' + settings: { mdmId: '' } } - encryptionAtHost: false // Some Azure subscriptions do not support encryption at host - extensionMonitoringAgentConfig: { - enabled: enableMonitoring - dataCollectionRuleAssociations: enableMonitoring && !empty(dataCollectionRuleResourceId) - ? [ - { - name: 'dcra-${name}' - dataCollectionRuleResourceId: dataCollectionRuleResourceId - description: 'Associates the Windows security event DCR with the jumpbox VM.' - } - ] - : [] + extensionAntiMalwareConfig: { + enabled: true + settings: { + AntimalwareEnabled: 'true' + Exclusions: {} + RealtimeProtectionEnabled: 'true' + ScheduledScanSettings: { day: '7', isEnabled: 'true', scanType: 'Quick', time: '120' } + } + tags: tags } - location: location - tags: tags + extensionMonitoringAgentConfig: extensionMonitoringAgentConfig + extensionNetworkWatcherAgentConfig: { enabled: true, tags: tags } } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the virtual machine.') -output resourceId string = vm.outputs.resourceId +output resourceId string = virtualMachine.outputs.resourceId @description('Name of the virtual machine.') -output name string = vm.outputs.name +output name string = virtualMachine.outputs.name diff --git a/infra/avm/modules/data/cosmos-db-nosql.bicep b/infra/avm/modules/data/cosmos-db-nosql.bicep index bccef9194..49c39f760 100644 --- a/infra/avm/modules/data/cosmos-db-nosql.bicep +++ b/infra/avm/modules/data/cosmos-db-nosql.bicep @@ -1,94 +1,110 @@ // ============================================================================ -// Module: Cosmos DB (NoSQL) -// Description: AVM wrapper for an Azure Cosmos DB account (SQL/NoSQL API) -// used for conversation history and product metadata. +// Module: Cosmos DB +// Description: AVM wrapper for Azure Cosmos DB (NoSQL) with WAF alignment // AVM Module: avm/res/document-db/database-account:0.19.0 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/cosmos-db // ============================================================================ -@description('Required. Name of the Cosmos DB account.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Name of the Cosmos DB account.') +param name string = 'cosmos-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} +@description('Database name.') +param databaseName string = 'db_conversation_history' + +@description('Container definitions.') +param containers array = [ + { + name: 'conversations' + partitionKeyPath: '/userId' + } +] + @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Required. Name of the SQL database.') -param databaseName string +// --- WAF: Monitoring --- +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] -@description('Required. Containers to create in the SQL database.') -param containers array +// --- WAF: Private Networking --- +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' -@description('Required. Principal ID of the managed identity to grant data contributor.') -param principalId string +@description('Whether to enable private networking.') +param enablePrivateNetworking bool = false -@description('Required. Principal ID of the deploying user/service principal.') -param deployerPrincipalId string +@description('Subnet resource ID for the private endpoint.') +param privateEndpointSubnetId string = '' -@description('Optional. Enable redundancy (zone redundancy + automatic failover).') -param enableRedundancy bool = false +@description('Private DNS zone resource IDs for Cosmos DB.') +param privateDnsZoneResourceIds array = [] -@description('Optional. High-availability failover region used when redundancy is enabled.') -param haLocation string = '' +var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'dns-zone-${i}' + privateDnsZoneResourceId: zoneId +}] -@description('Optional. Enable private networking (disables public access, adds private endpoint).') -param enablePrivateNetworking bool = false +// --- WAF: Redundancy --- +@description('Enable zone redundancy.') +param zoneRedundant bool = false -@description('Optional. Subnet resource ID for the private endpoint.') -param subnetResourceId string = '' +@description('Enable automatic failover.') +param enableAutomaticFailover bool = false -@description('Optional. Resource ID of the Cosmos DB private DNS zone.') -param cosmosDnsZoneResourceId string = '' - -@description('Optional. Diagnostic settings for monitoring.') -param diagnosticSettings array = [] +@description('Optional. HA paired region for multi-region failover when redundancy is enabled.') +param haLocation string = '' -module cosmos 'br/public:avm/res/document-db/database-account:0.19.0' = { +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module cosmosAccount 'br/public:avm/res/document-db/database-account:0.19.0' = { name: take('avm.res.document-db.database-account.${name}', 64) params: { name: name location: location tags: tags enableTelemetry: enableTelemetry + capabilitiesToAdd: zoneRedundant ? [] : ['EnableServerless'] sqlDatabases: [ { name: databaseName - containers: containers - } - ] - sqlRoleDefinitions: [ - { - roleName: 'contentgen-data-contributor' - dataActions: [ - 'Microsoft.DocumentDB/databaseAccounts/readMetadata' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' - ] + containers: [for container in containers: { + name: container.name + paths: [container.partitionKeyPath] + kind: 'Hash' + version: 2 + }] } ] - sqlRoleAssignments: [ - { - principalId: principalId - roleDefinitionId: '00000000-0000-0000-0000-000000000002' // Built-in Cosmos DB Data Contributor - } - { - principalId: deployerPrincipalId - roleDefinitionId: '00000000-0000-0000-0000-000000000002' // Built-in Cosmos DB Data Contributor to the deployer - } - ] - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : null + sqlRoleAssignments: [] + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] networkRestrictions: { - networkAclBypass: 'AzureServices' - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + networkAclBypass: 'None' + publicNetworkAccess: publicNetworkAccess } - zoneRedundant: enableRedundancy - capabilitiesToAdd: enableRedundancy ? null : ['EnableServerless'] - enableAutomaticFailover: enableRedundancy - failoverLocations: enableRedundancy + privateEndpoints: enablePrivateNetworking ? [ + { + name: 'pep-${name}' + customNetworkInterfaceName: 'nic-${name}' + subnetResourceId: privateEndpointSubnetId + service: 'Sql' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: privateDnsZoneConfigs + } + } + ] : [] + zoneRedundant: zoneRedundant + enableAutomaticFailover: enableAutomaticFailover + failoverLocations: zoneRedundant ? [ { failoverPriority: 0 @@ -108,24 +124,23 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.19.0' = { isZoneRedundant: false } ] - privateEndpoints: enablePrivateNetworking - ? [ - { - service: 'Sql' - subnetResourceId: subnetResourceId - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { privateDnsZoneResourceId: cosmosDnsZoneResourceId } - ] - } - } - ] - : null } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the Cosmos DB account.') -output resourceId string = cosmos.outputs.resourceId +output resourceId string = cosmosAccount.outputs.resourceId @description('Name of the Cosmos DB account.') -output name string = cosmos.outputs.name +output name string = cosmosAccount.outputs.name + +@description('Endpoint of the Cosmos DB account.') +output endpoint string = 'https://${name}.documents.azure.com:443/' + +@description('Database name.') +output databaseName string = databaseName + +@description('Container name (first container).') +output containerName string = containers[0].name diff --git a/infra/avm/modules/data/storage-account.bicep b/infra/avm/modules/data/storage-account.bicep index 8e0a11daf..329a532be 100644 --- a/infra/avm/modules/data/storage-account.bicep +++ b/infra/avm/modules/data/storage-account.bicep @@ -1,95 +1,139 @@ // ============================================================================ // Module: Storage Account -// Description: AVM wrapper for an Azure Storage Account (blob) used for -// product and generated image storage. +// Description: AVM wrapper for Azure Storage Account with WAF alignment // AVM Module: avm/res/storage/storage-account:0.32.0 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/storage-accounts // ============================================================================ -@description('Required. Name of the storage account.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Name of the storage account.') +param name string = take('st${toLower(replace(solutionName, '-', ''))}', 24) + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} +@description('Storage account SKU.') +param skuName string = 'Standard_LRS' + +@description('Storage account kind.') +param kind string = 'StorageV2' + +@description('Access tier.') +@allowed(['Hot', 'Cool']) +param accessTier string = 'Hot' + +@description('Allow blob public access.') +param allowBlobPublicAccess bool = false + +@description('Allow shared key access.') +param allowSharedKeyAccess bool = true + +@description('Enable hierarchical namespace (Data Lake Storage Gen2).') +param enableHierarchicalNamespace bool = false + @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Optional. Storage account SKU.') -param skuName string = 'Standard_LRS' +@description('Blob containers to create.') +param containers array = [ + { + name: 'default' + publicAccess: 'None' + } +] -@description('Required. Blob containers to create.') -param containers array +// --- WAF: Monitoring --- +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] -@description('Required. Principal ID of the managed identity to grant Storage Blob Data Contributor.') -param principalId string +// --- WAF: Private Networking --- +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' -@description('Optional. Enable private networking (disables public access, adds private endpoint).') +@description('Network ACLs for the storage account.') +param networkAcls object = { + defaultAction: 'Allow' + bypass: 'AzureServices' +} + +@description('Whether to enable private networking.') param enablePrivateNetworking bool = false -@description('Optional. Subnet resource ID for the private endpoint.') -param subnetResourceId string = '' +@description('Subnet resource ID for the private endpoint.') +param privateEndpointSubnetId string = '' -@description('Optional. Resource ID of the blob private DNS zone.') -param blobDnsZoneResourceId string = '' +@description('Private DNS zone resource IDs for Storage (blob).') +param privateDnsZoneResourceIds array = [] -@description('Optional. Diagnostic settings for monitoring.') -param diagnosticSettings array = [] +var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'dns-zone-${i}' + privateDnsZoneResourceId: zoneId +}] -module storageAccount 'br/public:avm/res/storage/storage-account:0.32.0' = { +// --- Role Assignments --- +@description('Optional. Array of role assignments to create on the Storage Account.') +param roleAssignments array = [] + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module storage 'br/public:avm/res/storage/storage-account:0.32.0' = { name: take('avm.res.storage.storage-account.${name}', 64) params: { name: name location: location + tags: tags + enableTelemetry: enableTelemetry skuName: skuName - managedIdentities: { systemAssigned: true } + kind: kind + accessTier: accessTier + allowBlobPublicAccess: allowBlobPublicAccess + allowSharedKeyAccess: allowSharedKeyAccess + enableHierarchicalNamespace: enableHierarchicalNamespace minimumTlsVersion: 'TLS1_2' - requireInfrastructureEncryption: true - enableTelemetry: enableTelemetry - tags: tags - accessTier: 'Hot' supportsHttpsTrafficOnly: true + requireInfrastructureEncryption: true + publicNetworkAccess: publicNetworkAccess + networkAcls: networkAcls blobServices: { - containerDeleteRetentionPolicyEnabled: true - containerDeleteRetentionPolicyDays: 7 - deleteRetentionPolicyEnabled: true - deleteRetentionPolicyDays: 7 - containers: containers + containers: [for container in containers: { + name: container.name + publicAccess: container.publicAccess + }] + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] } - roleAssignments: [ + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + privateEndpoints: enablePrivateNetworking ? [ { - principalId: principalId - roleDefinitionIdOrName: 'Storage Blob Data Contributor' - principalType: 'ServicePrincipal' + name: 'pep-${name}' + customNetworkInterfaceName: 'nic-${name}' + subnetResourceId: privateEndpointSubnetId + service: 'blob' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: privateDnsZoneConfigs + } } - ] - networkAcls: { - bypass: 'AzureServices' - defaultAction: enablePrivateNetworking ? 'Deny' : 'Allow' - } - allowBlobPublicAccess: false - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' - privateEndpoints: enablePrivateNetworking - ? [ - { - service: 'blob' - subnetResourceId: subnetResourceId - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: [ - { privateDnsZoneResourceId: blobDnsZoneResourceId } - ] - } - } - ] - : null - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : null + ] : [] + roleAssignments: !empty(roleAssignments) ? roleAssignments : [] } } -@description('Resource ID of the storage account.') -output resourceId string = storageAccount.outputs.resourceId +// ============================================================================ +// Outputs +// ============================================================================ +@description('Resource ID of the Storage Account.') +output resourceId string = storage.outputs.resourceId -@description('Name of the storage account.') -output name string = storageAccount.outputs.name +@description('Name of the Storage Account.') +output name string = storage.outputs.name + +@description('Primary blob endpoint.') +output blobEndpoint string = storage.outputs.primaryBlobEndpoint + +@description('Service endpoints.') +output serviceEndpoints object = storage.outputs.serviceEndpoints diff --git a/infra/avm/modules/monitoring/app-insights.bicep b/infra/avm/modules/monitoring/app-insights.bicep index aac01169b..b726ae81d 100644 --- a/infra/avm/modules/monitoring/app-insights.bicep +++ b/infra/avm/modules/monitoring/app-insights.bicep @@ -1,44 +1,76 @@ // ============================================================================ // Module: Application Insights -// Description: AVM wrapper for Azure Application Insights component. +// Description: AVM wrapper for Application Insights with WAF alignment // AVM Module: avm/res/insights/component:0.7.1 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/application-insights // ============================================================================ -@description('Required. Name of the Application Insights component.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Optional. Override name for the Application Insights instance. Defaults to appi-{solutionName}.') +param name string = 'appi-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} +@description('Resource ID of the Log Analytics workspace to link to.') +param workspaceResourceId string + +@description('Application type.') +param applicationType string = 'web' + +@description('Retention period in days. WAF recommends 365.') +param retentionInDays int = 365 + +@description('Disable IP masking for security. WAF recommends false.') +param disableIpMasking bool = false + +@description('Flow type for Application Insights.') +param flowType string = 'Bluefield' + @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Required. Resource ID of the Log Analytics workspace to link.') -param workspaceResourceId string +@description('Kind of Application Insights resource.') +param kind string = 'web' -module component 'br/public:avm/res/insights/component:0.7.1' = { +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module appInsights 'br/public:avm/res/insights/component:0.7.1' = { name: take('avm.res.insights.component.${name}', 64) params: { name: name - tags: tags location: location - enableTelemetry: enableTelemetry - retentionInDays: 365 - kind: 'web' - disableIpMasking: false - flowType: 'Bluefield' + tags: tags workspaceResourceId: workspaceResourceId + kind: kind + applicationType: applicationType + enableTelemetry: enableTelemetry + retentionInDays: retentionInDays + disableIpMasking: disableIpMasking + flowType: flowType } } -@description('Resource ID of the Application Insights component.') -output resourceId string = component.outputs.resourceId +// ============================================================================ +// Outputs +// ============================================================================ +@description('Resource ID of the Application Insights instance.') +output resourceId string = appInsights.outputs.resourceId + +@description('Name of the Application Insights instance.') +output name string = appInsights.outputs.name + +@description('Instrumentation key for the Application Insights instance.') +output instrumentationKey string = appInsights.outputs.instrumentationKey -@description('Name of the Application Insights component.') -output name string = component.outputs.name +@description('Connection string for the Application Insights instance.') +output connectionString string = appInsights.outputs.connectionString -@description('Connection string of the Application Insights component.') -output connectionString string = component.outputs.connectionString +@description('Application ID of the Application Insights instance.') +output applicationId string = appInsights.outputs.applicationId diff --git a/infra/avm/modules/monitoring/data-collection-rule.bicep b/infra/avm/modules/monitoring/data-collection-rule.bicep index ea2bba495..c1fd7606c 100644 --- a/infra/avm/modules/monitoring/data-collection-rule.bicep +++ b/infra/avm/modules/monitoring/data-collection-rule.bicep @@ -1,46 +1,112 @@ // ============================================================================ -// Module: Data Collection Rule (Windows Security Events) -// Description: AVM wrapper for an Azure Monitor Data Collection Rule that -// collects Windows Security audit events from the jumpbox VM -// (SFI-AzTBv17 compliance). -// AVM Module: avm/res/insights/data-collection-rule:0.11.0 +// Module: Data Collection Rule +// Description: AVM wrapper for Azure Monitor Data Collection Rule +// AVM Module: avm/res/insights/data-collection-rule +// WAF: Monitoring for VM observability // ============================================================================ -@description('Required. Name of the data collection rule.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Optional. Override name for the data collection rule. Defaults to dcr-{solutionName}.') +param name string = 'dcr-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} +@description('Resource ID of the Log Analytics workspace destination.') +param logAnalyticsWorkspaceResourceId string + +@description('Name of the Log Analytics workspace (used for destination naming).') +param logAnalyticsWorkspaceName string = '' + @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Required. Resource ID of the Log Analytics workspace destination.') -param workspaceResourceId string +var dcrLogAnalyticsDestinationName = !empty(logAnalyticsWorkspaceName) ? 'la-${logAnalyticsWorkspaceName}-destination' : 'la-${name}-destination' -@description('Optional. Name of the Log Analytics destination.') -param destinationName string = 'la-destination' - -module dcr 'br/public:avm/res/insights/data-collection-rule:0.11.0' = { +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module dataCollectionRule 'br/public:avm/res/insights/data-collection-rule:0.11.0' = { name: take('avm.res.insights.data-collection-rule.${name}', 64) params: { name: name - location: location tags: tags enableTelemetry: enableTelemetry + location: location dataCollectionRuleProperties: { kind: 'Windows' - description: 'Collects Windows Security audit success/failure events from jumpbox VM (SFI-AzTBv17 compliance).' dataSources: { + performanceCounters: [ + { + streams: ['Microsoft-Perf'] + samplingFrequencyInSeconds: 60 + counterSpecifiers: [ + '\\Processor Information(_Total)\\% Processor Time' + '\\Processor Information(_Total)\\% Privileged Time' + '\\Processor Information(_Total)\\% User Time' + '\\Processor Information(_Total)\\Processor Frequency' + '\\System\\Processes' + '\\Process(_Total)\\Thread Count' + '\\Process(_Total)\\Handle Count' + '\\System\\System Up Time' + '\\System\\Context Switches/sec' + '\\System\\Processor Queue Length' + '\\Memory\\% Committed Bytes In Use' + '\\Memory\\Available Bytes' + '\\Memory\\Committed Bytes' + '\\Memory\\Cache Bytes' + '\\Memory\\Pool Paged Bytes' + '\\Memory\\Pool Nonpaged Bytes' + '\\Memory\\Pages/sec' + '\\Memory\\Page Faults/sec' + '\\Process(_Total)\\Working Set' + '\\Process(_Total)\\Working Set - Private' + '\\LogicalDisk(_Total)\\% Disk Time' + '\\LogicalDisk(_Total)\\% Disk Read Time' + '\\LogicalDisk(_Total)\\% Disk Write Time' + '\\LogicalDisk(_Total)\\% Idle Time' + '\\LogicalDisk(_Total)\\Disk Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Read Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Write Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Transfers/sec' + '\\LogicalDisk(_Total)\\Disk Reads/sec' + '\\LogicalDisk(_Total)\\Disk Writes/sec' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Transfer' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Read' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Write' + '\\LogicalDisk(_Total)\\Avg. Disk Queue Length' + '\\LogicalDisk(_Total)\\Avg. Disk Read Queue Length' + '\\LogicalDisk(_Total)\\Avg. Disk Write Queue Length' + '\\LogicalDisk(_Total)\\% Free Space' + '\\LogicalDisk(_Total)\\Free Megabytes' + '\\Network Interface(*)\\Bytes Total/sec' + '\\Network Interface(*)\\Bytes Sent/sec' + '\\Network Interface(*)\\Bytes Received/sec' + '\\Network Interface(*)\\Packets/sec' + '\\Network Interface(*)\\Packets Sent/sec' + '\\Network Interface(*)\\Packets Received/sec' + '\\Network Interface(*)\\Packets Outbound Errors' + '\\Network Interface(*)\\Packets Received Errors' + ] + name: 'perfCounterDataSource60' + } + ] windowsEventLogs: [ { - name: 'securityEventLogsDataSource' - streams: [ - 'Microsoft-SecurityEvent' + name: 'SecurityAuditEvents' + streams: ['Microsoft-WindowsEvent'] + xPathQueries: [ + 'Security!*[System[(EventID=4624 or EventID=4625)]]' ] + } + { + name: 'AuditSuccessFailure' + streams: ['Microsoft-Event'] xPathQueries: [ 'Security!*[System[(band(Keywords,13510798882111488)) and (EventID != 4624)]]' ] @@ -50,27 +116,34 @@ module dcr 'br/public:avm/res/insights/data-collection-rule:0.11.0' = { destinations: { logAnalytics: [ { - name: destinationName - workspaceResourceId: workspaceResourceId + workspaceResourceId: logAnalyticsWorkspaceResourceId + name: dcrLogAnalyticsDestinationName } ] } dataFlows: [ { - streams: [ - 'Microsoft-SecurityEvent' - ] - destinations: [ - destinationName - ] + streams: ['Microsoft-Perf'] + destinations: [dcrLogAnalyticsDestinationName] + transformKql: 'source' + outputStream: 'Microsoft-Perf' + } + { + streams: ['Microsoft-Event'] + destinations: [dcrLogAnalyticsDestinationName] + transformKql: 'source' + outputStream: 'Microsoft-Event' } ] } } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the data collection rule.') -output resourceId string = dcr.outputs.resourceId +output resourceId string = dataCollectionRule.outputs.resourceId @description('Name of the data collection rule.') -output name string = dcr.outputs.name +output name string = dataCollectionRule.outputs.name diff --git a/infra/avm/modules/monitoring/log-analytics.bicep b/infra/avm/modules/monitoring/log-analytics.bicep index b93a4f342..3b231240c 100644 --- a/infra/avm/modules/monitoring/log-analytics.bicep +++ b/infra/avm/modules/monitoring/log-analytics.bicep @@ -1,55 +1,90 @@ // ============================================================================ // Module: Log Analytics Workspace -// Description: AVM wrapper for Azure Log Analytics Workspace. +// Description: AVM wrapper for Log Analytics Workspace with WAF alignment // AVM Module: avm/res/operational-insights/workspace:0.15.0 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-log-analytics +// Note: This module only handles NEW workspace creation. +// Existing workspace logic is handled in main.bicep. // ============================================================================ -@description('Required. Name of the Log Analytics workspace.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Optional. Override name for the Log Analytics workspace. Defaults to log-{solutionName}.') +param name string = 'log-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} +@description('Retention period in days. WAF recommends 365.') +param retentionInDays int = 365 + +@description('SKU name for the workspace.') +param skuName string = 'PerGB2018' + @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Optional. Enable redundancy (cross-region replication and daily quota).') -param enableRedundancy bool = false +// --- WAF: Private Networking --- +@description('Public network access for ingestion.') +param publicNetworkAccessForIngestion string = 'Enabled' + +@description('Public network access for query.') +param publicNetworkAccessForQuery string = 'Enabled' -@description('Optional. Replica region used when redundancy is enabled.') -param replicaLocation string = '' +// --- WAF: Redundancy --- +@description('Enable workspace replication for redundancy.') +param enableReplication bool = false -@description('Optional. Disable public network access (private networking).') -param enablePrivateNetworking bool = false +@description('Replication location (paired region).') +param replicationLocation string = '' +@description('Daily quota in GB. WAF recommends 150 GB/day as starting point.') +param dailyQuotaGb string = '' + +// --- WAF: Monitoring (VM data sources for private networking) --- +@description('Data sources for VM monitoring (Windows events, perf counters).') +param dataSources array = [] + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ module workspace 'br/public:avm/res/operational-insights/workspace:0.15.0' = { name: take('avm.res.operational-insights.workspace.${name}', 64) params: { name: name - tags: tags location: location + tags: tags + dataRetention: retentionInDays + skuName: skuName enableTelemetry: enableTelemetry - skuName: 'PerGB2018' - dataRetention: 365 features: { enableLogAccessUsingOnlyResourcePermissions: true } diagnosticSettings: [{ useThisWorkspace: true }] - dailyQuotaGb: enableRedundancy ? '10' : null - replication: enableRedundancy - ? { - enabled: true - location: replicaLocation - } - : null - publicNetworkAccessForIngestion: enablePrivateNetworking ? 'Disabled' : 'Enabled' - publicNetworkAccessForQuery: enablePrivateNetworking ? 'Disabled' : 'Enabled' + publicNetworkAccessForIngestion: publicNetworkAccessForIngestion + publicNetworkAccessForQuery: publicNetworkAccessForQuery + dailyQuotaGb: !empty(dailyQuotaGb) ? dailyQuotaGb : null + replication: enableReplication ? { + enabled: true + location: replicationLocation + } : null + dataSources: !empty(dataSources) ? dataSources : null } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the Log Analytics workspace.') output resourceId string = workspace.outputs.resourceId @description('Name of the Log Analytics workspace.') output name string = workspace.outputs.name + +@description('Location of the workspace.') +output location string = location + +@description('Log Analytics workspace customer ID.') +output logAnalyticsWorkspaceId string = workspace.outputs.logAnalyticsWorkspaceId diff --git a/infra/avm/modules/networking/bastion-host.bicep b/infra/avm/modules/networking/bastion-host.bicep index d1c804987..bf524087e 100644 --- a/infra/avm/modules/networking/bastion-host.bicep +++ b/infra/avm/modules/networking/bastion-host.bicep @@ -1,58 +1,85 @@ // ============================================================================ // Module: Bastion Host -// Description: AVM wrapper for an Azure Bastion host (Standard SKU). -// AVM Module: avm/res/network/bastion-host:0.8.2 +// Description: AVM wrapper for Azure Bastion Host +// AVM Module: avm/res/network/bastion-host // ============================================================================ -@description('Required. Name of the Bastion host.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +var name = 'bas-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Required. Resource ID of the virtual network to attach the Bastion host to.') +@description('Resource ID of the virtual network.') param virtualNetworkResourceId string -@description('Optional. Resource ID of the Log Analytics workspace for diagnostics. Empty disables diagnostics.') -param logAnalyticsWorkspaceResourceId string = '' +@description('Optional. Diagnostic settings for the resource.') +param diagnosticSettings array? + +@description('SKU name for the Bastion Host.') +param skuName string = 'Standard' + +@description('Number of scale units.') +param scaleUnits int = 4 + +@description('Disable copy/paste functionality.') +param disableCopyPaste bool = true + +@description('Enable file copy functionality.') +param enableFileCopy bool = false + +@description('Enable IP Connect functionality.') +param enableIpConnect bool = false + +@description('Enable shareable link functionality.') +param enableShareableLink bool = false + +@description('Availability zones for the Bastion Host public IP. Pass empty array to disable zone redundancy.') +param availabilityZones array = [] -module bastion 'br/public:avm/res/network/bastion-host:0.8.2' = { +@description('Optional. Diagnostic settings for the public IP address.') +param publicIPDiagnosticSettings array? + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = { name: take('avm.res.network.bastion-host.${name}', 64) params: { name: name - skuName: 'Standard' location: location - virtualNetworkResourceId: virtualNetworkResourceId - diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId) - ? [ - { - name: 'bastionDiagnostics' - workspaceResourceId: logAnalyticsWorkspaceResourceId - logCategoriesAndGroups: [ - { - categoryGroup: 'allLogs' - enabled: true - } - ] - } - ] - : [] tags: tags enableTelemetry: enableTelemetry + skuName: skuName + virtualNetworkResourceId: virtualNetworkResourceId + availabilityZones: availabilityZones publicIPAddressObject: { name: 'pip-${name}' + diagnosticSettings: publicIPDiagnosticSettings + tags: tags } + disableCopyPaste: disableCopyPaste + enableFileCopy: enableFileCopy + enableIpConnect: enableIpConnect + enableShareableLink: enableShareableLink + scaleUnits: scaleUnits + diagnosticSettings: diagnosticSettings } } -@description('Resource ID of the Bastion host.') -output resourceId string = bastion.outputs.resourceId +// ============================================================================ +// Outputs +// ============================================================================ +@description('Resource ID of the Bastion Host.') +output resourceId string = bastionHost.outputs.resourceId -@description('Name of the Bastion host.') -output name string = bastion.outputs.name +@description('Name of the Bastion Host.') +output name string = bastionHost.outputs.name diff --git a/infra/avm/modules/networking/private-dns-zone.bicep b/infra/avm/modules/networking/private-dns-zone.bicep index 319afb17c..be1f69733 100644 --- a/infra/avm/modules/networking/private-dns-zone.bicep +++ b/infra/avm/modules/networking/private-dns-zone.bicep @@ -1,38 +1,44 @@ // ============================================================================ // Module: Private DNS Zone -// Description: AVM wrapper for a single Private DNS Zone linked to a VNet. -// AVM Module: avm/res/network/private-dns-zone:0.8.1 +// Description: AVM wrapper for Azure Private DNS Zone +// AVM Module: avm/res/network/private-dns-zone +// Usage: Call once per DNS zone from main.bicep // ============================================================================ -@description('Required. Name of the private DNS zone (e.g. privatelink.blob.core.windows.net).') +@description('Name of the private DNS zone (e.g., privatelink.cognitiveservices.azure.com).') param name string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Required. Resource ID of the virtual network to link.') -param virtualNetworkResourceId string +@description('Virtual network links to associate with the DNS zone.') +param virtualNetworkLinks array = [] -module zone 'br/public:avm/res/network/private-dns-zone:0.8.1' = { - name: take('avm.res.network.private-dns-zone.${replace(name, '.', '-')}', 64) +@description('Optional. Array of A records.') +param a array = [] + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module privateDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.1' = { + name: take('avm.res.network.private-dns-zone.${split(name, '.')[1]}', 64) params: { name: name tags: tags enableTelemetry: enableTelemetry - virtualNetworkLinks: [ - { - virtualNetworkResourceId: virtualNetworkResourceId - registrationEnabled: false - } - ] + virtualNetworkLinks: virtualNetworkLinks + a: a } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the private DNS zone.') -output resourceId string = zone.outputs.resourceId +output resourceId string = privateDnsZone.outputs.resourceId @description('Name of the private DNS zone.') -output name string = zone.outputs.name +output name string = privateDnsZone.outputs.name diff --git a/infra/avm/modules/networking/private-endpoint.bicep b/infra/avm/modules/networking/private-endpoint.bicep index 72196b4e0..04bfff07c 100644 --- a/infra/avm/modules/networking/private-endpoint.bicep +++ b/infra/avm/modules/networking/private-endpoint.bicep @@ -1,58 +1,50 @@ // ============================================================================ // Module: Private Endpoint -// Description: AVM wrapper for an Azure Private Endpoint with DNS zone group. -// AVM Module: avm/res/network/private-endpoint:0.12.0 +// Description: AVM wrapper for Azure Private Endpoint +// AVM Module: avm/res/network/private-endpoint +// Usage: Call once per private endpoint from main.bicep // ============================================================================ -@description('Required. Name of the private endpoint.') +@description('Name of the private endpoint.') param name string -@description('Required. Azure region for the resource.') +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true +@description('Optional. Custom NIC name for the private endpoint.') +param customNetworkInterfaceName string = '' -@description('Required. Resource ID of the subnet to deploy the private endpoint into.') +@description('Resource ID of the subnet for the private endpoint.') param subnetResourceId string -@description('Required. Resource ID of the target resource (private link service).') -param targetResourceId string - -@description('Required. Group IDs (sub-resources) the private endpoint connects to.') -param groupIds array +@description('Private link service connections configuration.') +param privateLinkServiceConnections array -@description('Optional. Private DNS zone group configurations.') -param privateDnsZoneConfigs array = [] +@description('Optional. Private DNS zone group configuration.') +param privateDnsZoneGroup object? +// ============================================================================ +// AVM Module Deployment +// ============================================================================ module privateEndpoint 'br/public:avm/res/network/private-endpoint:0.12.0' = { name: take('avm.res.network.private-endpoint.${name}', 64) params: { name: name location: location tags: tags - enableTelemetry: enableTelemetry + customNetworkInterfaceName: !empty(customNetworkInterfaceName) ? customNetworkInterfaceName : 'nic-${name}' subnetResourceId: subnetResourceId - privateLinkServiceConnections: [ - { - name: name - properties: { - privateLinkServiceId: targetResourceId - groupIds: groupIds - } - } - ] - privateDnsZoneGroup: !empty(privateDnsZoneConfigs) - ? { - privateDnsZoneGroupConfigs: privateDnsZoneConfigs - } - : null + privateLinkServiceConnections: privateLinkServiceConnections + privateDnsZoneGroup: privateDnsZoneGroup } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the private endpoint.') output resourceId string = privateEndpoint.outputs.resourceId diff --git a/infra/avm/modules/networking/virtual-network.bicep b/infra/avm/modules/networking/virtual-network.bicep index 008056bc9..ca6f68946 100644 --- a/infra/avm/modules/networking/virtual-network.bicep +++ b/infra/avm/modules/networking/virtual-network.bicep @@ -1,119 +1,122 @@ -/****************************************************************************************************************************/ -// Networking - NSGs, VNET and Subnets for Content Generation Solution -/****************************************************************************************************************************/ -@description('Name of the virtual network.') -param vnetName string +// ============================================================================ +// Module: Virtual Network +// Description: VNet, Subnets, and NSGs using AVM modules. +// Each subnet gets its own NSG. Subnet config is passed as param. +// AVM Modules: +// - avm/res/network/network-security-group:0.5.3 +// - avm/res/network/virtual-network:0.8.0 +// ============================================================================ -@description('Azure region to deploy resources.') -param location string = resourceGroup().location +@description('Solution name suffix used to derive the resource name.') +param solutionName string + +var name = 'vnet-${solutionName}' -@description('Required. An Array of 1 or more IP Address Prefixes for the Virtual Network.') -param addressPrefixes array = ['10.0.0.0/20'] +@description('Azure region for the resource.') +param location string = resourceGroup().location -@description('Optional. Deploy Azure Bastion and Jumpbox subnets for VM-based administration.') -param deployBastionAndJumpbox bool = false +@description('Address prefixes for the virtual network.') +param addressPrefixes array -@description('An array of subnets to be created within the virtual network.') -// Core subnets: web (App Service), peps (Private Endpoints), aci (Container Instance) -// Optional: AzureBastionSubnet and jumpbox (only when deployBastionAndJumpbox is true) -var coreSubnets = [ +@description('Subnet configurations.') +param subnets subnetType[] = [ { - name: 'web' - addressPrefixes: ['10.0.0.0/23'] - delegation: 'Microsoft.Web/serverFarms' + name: 'backend' + addressPrefixes: ['10.0.0.0/27'] networkSecurityGroup: { - name: 'nsg-web' + name: 'nsg-backend' securityRules: [ { - name: 'AllowHttpsInbound' + name: 'deny-hop-outbound' properties: { - access: 'Allow' - direction: 'Inbound' - priority: 100 + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: ['22', '3389'] + direction: 'Outbound' + priority: 200 protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefixes: ['0.0.0.0/0'] - destinationAddressPrefixes: ['10.0.0.0/23'] } } + ] + } + } + { + name: 'containers' + addressPrefixes: ['10.0.2.0/23'] + delegation: 'Microsoft.App/environments' + privateEndpointNetworkPolicies: 'Enabled' + privateLinkServiceNetworkPolicies: 'Enabled' + networkSecurityGroup: { + name: 'nsg-containers' + securityRules: [ { - name: 'AllowIntraSubnetTraffic' + name: 'deny-hop-outbound' properties: { - access: 'Allow' - direction: 'Inbound' + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: ['22', '3389'] + direction: 'Outbound' priority: 200 - protocol: '*' + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefixes: ['10.0.0.0/23'] - destinationAddressPrefixes: ['10.0.0.0/23'] } } + ] + } + } + { + name: 'webserverfarm' + addressPrefixes: ['10.0.4.0/27'] + delegation: 'Microsoft.Web/serverfarms' + privateEndpointNetworkPolicies: 'Enabled' + privateLinkServiceNetworkPolicies: 'Enabled' + networkSecurityGroup: { + name: 'nsg-webserverfarm' + securityRules: [ { - name: 'AllowAzureLoadBalancer' + name: 'deny-hop-outbound' properties: { - access: 'Allow' - direction: 'Inbound' - priority: 300 - protocol: '*' + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: ['22', '3389'] + direction: 'Outbound' + priority: 200 + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: 'AzureLoadBalancer' - destinationAddressPrefix: '10.0.0.0/23' } } ] } } { - name: 'peps' - addressPrefixes: ['10.0.2.0/23'] - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Disabled' + name: 'administration' + addressPrefixes: ['10.0.0.32/27'] networkSecurityGroup: { - name: 'nsg-peps' - securityRules: [] - } - } - { - name: 'aci' - addressPrefixes: ['10.0.4.0/24'] - delegation: 'Microsoft.ContainerInstance/containerGroups' - networkSecurityGroup: { - name: 'nsg-aci' + name: 'nsg-administration' securityRules: [ { - name: 'AllowHttpsInbound' + name: 'deny-hop-outbound' properties: { - access: 'Allow' - direction: 'Inbound' - priority: 100 + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: ['22', '3389'] + direction: 'Outbound' + priority: 200 protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' sourcePortRange: '*' - destinationPortRange: '8000' - sourceAddressPrefixes: ['10.0.0.0/20'] - destinationAddressPrefixes: ['10.0.4.0/24'] } } ] } } -] - -// Bastion and Jumpbox subnets (only deployed when deployBastionAndJumpbox is true) -// VM Size Notes: -// 1 B-series VMs (like Standard_B2ms) do not support accelerated networking. -// 2 Pick a VM size that supports accelerated networking + Premium SSD (the usual jump-box candidates): -// Standard_D2s_v5 (2 vCPU, 8 GiB RAM, Premium SSD/v2/Ultra) // DEFAULT - current-gen Intel, broad regional availability. -// Standard_D2as_v5 (2 vCPU, 8 GiB RAM, Premium SSD/Ultra) // AMD alternative, typically ~15% cheaper. -// Standard_D2s_v4 (2 vCPU, 8 GiB RAM, Premium SSD) // Previous gen, also broadly available. -// Standard_DS2_v2 (2 vCPU, 7 GiB RAM, Premium SSD) // Legacy SKU, being retired from some regions - avoid for new deployments. -// 3 A-series (Av2) is NOT suitable: no Premium SSD support, no accelerated networking. -var bastionSubnets = deployBastionAndJumpbox ? [ { name: 'AzureBastionSubnet' - addressPrefixes: ['10.0.10.0/26'] + addressPrefixes: ['10.0.0.64/26'] networkSecurityGroup: { name: 'nsg-bastion' securityRules: [ @@ -172,49 +175,27 @@ var bastionSubnets = deployBastionAndJumpbox ? [ ] } } - { - name: 'jumpbox' - addressPrefixes: ['10.0.12.0/23'] - networkSecurityGroup: { - name: 'nsg-jumpbox' - securityRules: [ - { - name: 'AllowRdpFromBastion' - properties: { - access: 'Allow' - direction: 'Inbound' - priority: 100 - protocol: 'Tcp' - sourcePortRange: '*' - destinationPortRange: '3389' - sourceAddressPrefixes: ['10.0.10.0/26'] - destinationAddressPrefixes: ['10.0.12.0/23'] - } - } - ] - } - } -] : [] - -var vnetSubnets = concat(coreSubnets, bastionSubnets) +] -@description('Optional. Tags to be applied to the resources.') +@description('Tags to apply to the resources.') param tags object = {} -@description('Optional. The resource ID of the Log Analytics Workspace to send diagnostic logs to.') -param logAnalyticsWorkspaceId string = '' +@description('Resource ID of the Log Analytics Workspace for diagnostics.') +param logAnalyticsWorkspaceId string @description('Optional. Enable/Disable usage telemetry for module.') param enableTelemetry bool = true -@description('Required. Suffix for resource naming.') +@description('Suffix for resource naming.') param resourceSuffix string -// Create NSGs for subnets using AVM Network Security Group module +// ============================================================================ +// NSGs — one per subnet +// ============================================================================ @batchSize(1) module nsgs 'br/public:avm/res/network/network-security-group:0.5.3' = [ - for (subnet, i) in vnetSubnets: if (!empty(subnet.?networkSecurityGroup)) { - name: take('avm.res.network.network-security-group.${subnet.?networkSecurityGroup.name}.${resourceSuffix}', 64) + for (subnet, i) in subnets: if (!empty(subnet.?networkSecurityGroup)) { + name: take('avm.res.network.nsg.${subnet.?networkSecurityGroup.name}.${resourceSuffix}', 64) params: { name: '${subnet.?networkSecurityGroup.name}-${resourceSuffix}' location: location @@ -225,15 +206,17 @@ module nsgs 'br/public:avm/res/network/network-security-group:0.5.3' = [ } ] -// Create VNet and subnets using AVM Virtual Network module +// ============================================================================ +// Virtual Network + Subnets +// ============================================================================ module virtualNetwork 'br/public:avm/res/network/virtual-network:0.8.0' = { - name: take('avm.res.network.virtual-network.${vnetName}', 64) + name: take('avm.res.network.virtual-network.${name}', 64) params: { - name: vnetName + name: name location: location addressPrefixes: addressPrefixes subnets: [ - for (subnet, i) in vnetSubnets: { + for (subnet, i) in subnets: { name: subnet.name addressPrefixes: subnet.?addressPrefixes networkSecurityGroupResourceId: !empty(subnet.?networkSecurityGroup) ? nsgs[i]!.outputs.resourceId : null @@ -242,7 +225,7 @@ module virtualNetwork 'br/public:avm/res/network/virtual-network:0.8.0' = { delegation: subnet.?delegation } ] - diagnosticSettings: !empty(logAnalyticsWorkspaceId) ? [ + diagnosticSettings: [ { name: 'vnetDiagnostics' workspaceResourceId: logAnalyticsWorkspaceId @@ -259,20 +242,90 @@ module virtualNetwork 'br/public:avm/res/network/virtual-network:0.8.0' = { } ] } - ] : [] + ] tags: tags enableTelemetry: enableTelemetry } } +// ============================================================================ +// Outputs +// ============================================================================ output name string = virtualNetwork.outputs.name output resourceId string = virtualNetwork.outputs.resourceId -// Core subnet outputs (always present) -output webSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'web') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'web')] : '' -output pepsSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'peps') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'peps')] : '' -output aciSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'aci') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'aci')] : '' +output subnets subnetOutputType[] = [ + for (subnet, i) in subnets: { + name: subnet.name + resourceId: virtualNetwork.outputs.subnetResourceIds[i] + nsgName: !empty(subnet.?networkSecurityGroup) ? subnet.?networkSecurityGroup.name : null + nsgResourceId: !empty(subnet.?networkSecurityGroup) ? nsgs[i]!.outputs.resourceId : null + } +] -// Bastion/jumpbox subnet outputs (always declared; will be empty when those subnets are not deployed) -output bastionSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'AzureBastionSubnet') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'AzureBastionSubnet')] : '' -output jumpboxSubnetResourceId string = contains(map(vnetSubnets, subnet => subnet.name), 'jumpbox') ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(vnetSubnets, subnet => subnet.name), 'jumpbox')] : '' +// Individual subnet outputs for backward compatibility +output backendSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'backend') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'backend')] + : '' +output containerSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'containers') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'containers')] + : '' +output webserverfarmSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'webserverfarm') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'webserverfarm')] + : '' +output administrationSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'administration') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'administration')] + : '' +output bastionSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'AzureBastionSubnet') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')] + : '' + +// ============================================================================ +// Custom Types +// ============================================================================ +@export() +@description('Subnet output type') +type subnetOutputType = { + @description('The name of the subnet.') + name: string + @description('The resource ID of the subnet.') + resourceId: string + @description('The name of the associated NSG, if any.') + nsgName: string? + @description('The resource ID of the associated NSG, if any.') + nsgResourceId: string? +} + +@export() +@description('Subnet configuration type') +type subnetType = { + @description('Required. The name of the subnet.') + name: string + @description('Required. Address prefixes for the subnet.') + addressPrefixes: string[] + @description('Optional. Delegation for the subnet.') + delegation: string? + @description('Optional. Private endpoint network policies.') + privateEndpointNetworkPolicies: ('Disabled' | 'Enabled' | 'NetworkSecurityGroupEnabled' | 'RouteTableEnabled')? + @description('Optional. Private link service network policies.') + privateLinkServiceNetworkPolicies: ('Disabled' | 'Enabled')? + @description('Optional. NSG configuration for the subnet.') + networkSecurityGroup: networkSecurityGroupType? + @description('Optional. Route table resource ID.') + routeTableResourceId: string? + @description('Optional. Service endpoint policies.') + serviceEndpointPolicies: object[]? + @description('Optional. Service endpoints to enable.') + serviceEndpoints: string[]? + @description('Optional. Disable default outbound connectivity.') + defaultOutboundAccess: bool? +} + +@export() +@description('NSG configuration type') +type networkSecurityGroupType = { + @description('Required. The name of the NSG.') + name: string + @description('Required. Security rules for the NSG.') + securityRules: object[] +} diff --git a/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep b/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep index 96595e093..4ed69a72c 100644 --- a/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep +++ b/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep @@ -1,35 +1,66 @@ -@description('Required. Name of the AI Services account.') -param aiServicesName string +// ============================================================================ +// Module: Model Deployment — Vanilla Bicep +// Description: Deploys a single AI model to an existing AI Services account. +// Called repetitively from main.bicep for each model in the array. +// Generic, reusable across GSAs. +// ============================================================================ -@description('Required. Array of model deployments to create.') -param deployments array = [] +targetScope = 'resourceGroup' -// Reference AI Services account (module is scoped to the correct resource group) -resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { - name: aiServicesName +@description('Required. Name of the parent AI Services account.') +param aiServicesAccountName string + +@description('Required. Name for this model deployment.') +param deploymentName string + +@description('Optional. Model format (e.g., OpenAI).') +param modelFormat string = 'OpenAI' + +@description('Required. Model name (e.g., gpt-4o, text-embedding-ada-002).') +param modelName string + +@description('Optional. Model version. Empty string means latest.') +param modelVersion string = '' + +@description('Optional. RAI policy name.') +param raiPolicyName string = 'Microsoft.Default' + +@description('Required. SKU name (e.g., Standard, GlobalStandard).') +param skuName string + +@description('Required. SKU capacity (tokens per minute in thousands).') +param skuCapacity int + +// ============================================================================ +// Model Deployment +// ============================================================================ +resource aiServicesAccount 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { + name: aiServicesAccountName } -// Deploy models to AI Services account -// Using batchSize(1) to avoid concurrent deployment issues -@batchSize(1) -resource modelDeployments 'Microsoft.CognitiveServices/accounts/deployments@2025-12-01' = [ - for (deployment, index) in deployments: { - parent: aiServices - name: deployment.name - properties: { - model: { - format: deployment.format - name: deployment.model - version: deployment.version - } - raiPolicyName: deployment.raiPolicyName - } - sku: { - name: deployment.sku.name - capacity: deployment.sku.capacity +resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-12-01' = { + parent: aiServicesAccount + name: deploymentName + properties: { + model: { + format: modelFormat + name: modelName + version: !empty(modelVersion) ? modelVersion : null } + raiPolicyName: raiPolicyName + } + sku: { + name: skuName + capacity: skuCapacity } -] +} + +// ============================================================================ +// Outputs +// ============================================================================ + +@description('Name of the deployed model.') +output name string = modelDeployment.name -@description('The names of the deployed models.') -output deployedModelNames array = [for (deployment, i) in deployments: modelDeployments[i].name] +@description('Resource ID of the model deployment.') +output resourceId string = modelDeployment.id diff --git a/infra/bicep/modules/ai/ai-foundry-project.bicep b/infra/bicep/modules/ai/ai-foundry-project.bicep index 4c425e99e..362dbbad9 100644 --- a/infra/bicep/modules/ai/ai-foundry-project.bicep +++ b/infra/bicep/modules/ai/ai-foundry-project.bicep @@ -1,58 +1,117 @@ -@description('Required. Name of the AI Services project.') -param name string +// ============================================================================ +// Module: AI Foundry Project (Account + Project) — Vanilla Bicep +// Description: Creates an Azure AI Services account and AI Foundry project. +// Generic, reusable across GSAs — no app-specific parameters. +// ============================================================================ -@description('Required. The location of the Project resource.') -param location string = resourceGroup().location +targetScope = 'resourceGroup' -@description('Optional. The description of the AI Foundry project to create. Defaults to the project name.') -param desc string = name +@description('Required. Solution name suffix used to generate resource names.') +param solutionName string -@description('Required. Name of the existing Cognitive Services resource to create the AI Foundry project in.') -param aiServicesName string +@description('Optional. Override name for the AI Services account. Defaults to aif-{solutionName}.') +param name string = 'aif-${solutionName}' -@description('Required. Azure Existing AI Project ResourceID.') -param azureExistingAIProjectResourceId string = '' +@description('Optional. Override name for the AI Foundry project. Defaults to proj-{solutionName}.') +param projectName string = 'proj-${solutionName}' -@description('Optional. Tags to be applied to the resources.') +@description('Required. Azure region for the resources.') +param location string + +@description('Optional. Tags to apply to resources.') param tags object = {} -var useExistingAiFoundryAiProject = !empty(azureExistingAIProjectResourceId) -var existingOpenAIEndpoint = useExistingAiFoundryAiProject - ? format('https://{0}.openai.azure.com/', split(azureExistingAIProjectResourceId, '/')[8]) - : '' +@description('Optional. SKU name for the AI Services account.') +param skuName string = 'S0' -// Reference to cognitive service in current resource group for new projects -resource cogServiceReference 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { - name: aiServicesName -} +@description('Optional. Whether to disable local (key-based) authentication.') +param disableLocalAuth bool = true -resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-12-01' = { - parent: cogServiceReference +@description('Optional. Whether to allow project management (AI Foundry hub).') +param allowProjectManagement bool = true + +@description('Optional. Public network access setting.') +param publicNetworkAccess string = 'Enabled' + +@description('Optional. Managed identity type for the resources.') +@allowed(['SystemAssigned', 'UserAssigned', 'SystemAssigned, UserAssigned', 'None']) +param identityType string = 'SystemAssigned' + +@description('Optional. Network ACLs default action.') +@allowed(['Allow', 'Deny']) +param networkAclsDefaultAction string = 'Allow' + +// ============================================================================ +// AI Services Account +// ============================================================================ +resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' = { name: name - tags: tags location: location + tags: tags + sku: { + name: skuName + } + kind: 'AIServices' identity: { - type: 'SystemAssigned' + type: identityType } properties: { - description: desc - displayName: name + allowProjectManagement: allowProjectManagement + customSubDomainName: name + networkAcls: { + defaultAction: networkAclsDefaultAction + virtualNetworkRules: [] + ipRules: [] + } + publicNetworkAccess: publicNetworkAccess + disableLocalAuth: disableLocalAuth + } +} + +// ============================================================================ +// AI Foundry Project +// ============================================================================ +resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-12-01' = { + parent: aiServices + name: projectName + location: location + kind: 'AIServices' + identity: { + type: identityType } + properties: {} } -@description('Required. Name of the AI project.') -output name string = aiProject.name +// ============================================================================ +// Outputs +// ============================================================================ + +@description('Resource ID of the AI Services account.') +output resourceId string = aiServices.id + +@description('Name of the AI Services account.') +output name string = aiServices.name + +@description('Endpoint of the AI Services account (OpenAI Language Model Instance API).') +output endpoint string = aiServices.properties.endpoints['OpenAI Language Model Instance API'] + +@description('Endpoint of the AI Services account (Cognitive Services).') +output cognitiveServicesEndpoint string = aiServices.properties.endpoint + +@description('Azure OpenAI Content Understanding endpoint URL.') +output azureOpenAiCuEndpoint string = aiServices.properties.endpoints['Content Understanding'] + +@description('System-assigned identity principal ID of the AI Services account.') +output principalId string = aiServices.identity.principalId -@description('Required. Resource ID of the AI project.') -output resourceId string = aiProject.id +@description('Resource ID of the AI Foundry project.') +output projectResourceId string = aiProject.id -@description('Required. API endpoint for the AI project.') -output apiEndpoint string = aiProject!.properties.endpoints['AI Foundry API'] +@description('Name of the AI Foundry project.') +output projectName string = aiProject.name -@description('Contains AI Endpoint.') -output aoaiEndpoint string = !empty(existingOpenAIEndpoint) - ? existingOpenAIEndpoint - : cogServiceReference.properties.endpoints['OpenAI Language Model Instance API'] +@description('AI Foundry project endpoint.') +output projectEndpoint string = aiProject.properties.endpoints['AI Foundry API'] -@description('Required. Principal ID of the AI project system-assigned managed identity.') -output systemAssignedMIPrincipalId string = aiProject.identity.principalId +@description('System-assigned identity principal ID of the project.') +output projectIdentityPrincipalId string = aiProject.identity.principalId diff --git a/infra/bicep/modules/ai/ai-search-identity.bicep b/infra/bicep/modules/ai/ai-search-identity.bicep index da2309b81..ceb2dbaa5 100644 --- a/infra/bicep/modules/ai/ai-search-identity.bicep +++ b/infra/bicep/modules/ai/ai-search-identity.bicep @@ -35,6 +35,12 @@ param semanticSearch string = 'free' @description('Whether to disable local authentication.') param disableLocalAuth bool = true +@description('Optional. Authentication options for the search service.') +param authOptions object = {} + +@description('Optional. Network rule set for the search service.') +param networkRuleSet object = {} + @description('Managed identity type for the search service.') param managedIdentityType string = 'SystemAssigned' @@ -58,6 +64,8 @@ resource searchServiceUpdate 'Microsoft.Search/searchServices@2025-05-01' = { semanticSearch: semanticSearch disableLocalAuth: disableLocalAuth publicNetworkAccess: publicNetworkAccess + authOptions: !empty(authOptions) ? authOptions : null + networkRuleSet: !empty(networkRuleSet) ? networkRuleSet : null } } diff --git a/infra/bicep/modules/ai/ai-search.bicep b/infra/bicep/modules/ai/ai-search.bicep index 816bfce8b..798a0f74c 100644 --- a/infra/bicep/modules/ai/ai-search.bicep +++ b/infra/bicep/modules/ai/ai-search.bicep @@ -1,74 +1,105 @@ // ============================================================================ // Module: AI Search -// Description: Vanilla Bicep module for an Azure AI Search service. -// Resource: Microsoft.Search/searchServices@2024-06-01-preview +// Description: Deploys Azure AI Search with a two-step pattern: +// Step 1: Plain Bicep resource for fast initial creation (name, location, SKU) +// Step 2: Separate module deployment to enable managed identity & full config +// This reduces deployment time by making the resource available immediately +// while identity enablement proceeds as a separate ARM deployment. // ============================================================================ -@description('Required. Name of the AI Search service.') -param name string +targetScope = 'resourceGroup' -@description('Required. Azure region for the resource.') +@description('Solution name suffix used to derive the resource name.') +@minLength(3) +param solutionName string + +@description('Optional. Override name for the search service. Defaults to srch-{solutionName}.') +param name string = 'srch-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true - -@description('Optional. SKU of the AI Search service.') -param sku string = 'basic' +@description('SKU name for the search service.') +@allowed(['free', 'basic', 'standard', 'standard2', 'standard3', 'storage_optimized_l1', 'storage_optimized_l2']) +param skuName string = 'basic' -@description('Optional. Number of replicas.') +@description('Number of replicas.') param replicaCount int = 1 -@description('Required. Principal ID of the managed identity to grant search data/service roles.') -param principalId string +@description('Number of partitions.') +param partitionCount int = 1 + +@description('Hosting mode.') +@allowed(['Default', 'HighDensity']) +param hostingMode string = 'Default' + +@description('Semantic search tier.') +@allowed(['disabled', 'free', 'standard']) +param semanticSearch string = 'free' + +@description('Whether to disable local authentication.') +param disableLocalAuth bool = true -resource search 'Microsoft.Search/searchServices@2024-06-01-preview' = { +@description('Optional. Authentication options for the search service.') +param authOptions object = {} + +@description('Optional. Network rule set for the search service.') +param networkRuleSet object = {} + +@description('Managed identity type for the search service.') +param managedIdentityType string = 'SystemAssigned' + +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' + +// ============================================================================ +// Step 1: Initial resource creation (fast — no identity) +// ============================================================================ +resource aiSearch 'Microsoft.Search/searchServices@2025-05-01' = { name: name location: location - tags: tags sku: { - name: sku - } - identity: { - type: 'SystemAssigned' - } - properties: { - replicaCount: replicaCount - partitionCount: 1 - hostingMode: 'default' - semanticSearch: 'free' - disableLocalAuth: true - // AI Search remains publicly accessible - accessed from ACI via managed identity - publicNetworkAccess: 'enabled' + name: skuName } } -resource searchIndexDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(search.id, principalId, '8ebe5a00-799e-43f5-93ac-243d3dce84a7') - scope: search - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7') // Search Index Data Contributor - principalId: principalId - principalType: 'ServicePrincipal' +// ============================================================================ +// Step 2: Separate deployment — enables identity & full configuration +// ============================================================================ +module searchServiceUpdate 'ai-search-identity.bicep' = { + name: 'searchServiceUpdate' + params: { + name: aiSearch.name + location: location + tags: tags + skuName: skuName + replicaCount: replicaCount + partitionCount: partitionCount + hostingMode: hostingMode + semanticSearch: semanticSearch + disableLocalAuth: disableLocalAuth + authOptions: authOptions + networkRuleSet: networkRuleSet + managedIdentityType: managedIdentityType + publicNetworkAccess: publicNetworkAccess } } -resource searchServiceContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(search.id, principalId, '7ca78c08-252a-4471-8644-bb5ff32d4ba0') - scope: search - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0') // Search Service Contributor - principalId: principalId - principalType: 'ServicePrincipal' - } -} +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the AI Search service.') -output resourceId string = search.id +output resourceId string = aiSearch.id @description('Name of the AI Search service.') -output name string = search.name +output name string = aiSearch.name + +@description('Endpoint URL of the AI Search service.') +output endpoint string = 'https://${aiSearch.name}.search.windows.net' + +@description('System-assigned identity principal ID.') +output identityPrincipalId string = searchServiceUpdate.outputs.systemAssignedMIPrincipalId diff --git a/infra/bicep/modules/ai/existing-project-setup.bicep b/infra/bicep/modules/ai/existing-project-setup.bicep index 60b44539e..df0acdc5e 100644 --- a/infra/bicep/modules/ai/existing-project-setup.bicep +++ b/infra/bicep/modules/ai/existing-project-setup.bicep @@ -38,6 +38,9 @@ output name string = aiServices.name @description('Endpoint of the AI Services account (OpenAI Language Model Instance API).') output endpoint string = aiServices.properties.endpoints['OpenAI Language Model Instance API'] +@description('Endpoint of the AI Services account (Cognitive Services).') +output cognitiveServicesEndpoint string = aiServices.properties.endpoint + @description('Azure OpenAI Content Understanding endpoint URL.') output azureOpenAiCuEndpoint string = aiServices.properties.endpoints['Content Understanding'] diff --git a/infra/bicep/modules/compute/app-service-plan.bicep b/infra/bicep/modules/compute/app-service-plan.bicep index cbd7abd67..f9409f0cf 100644 --- a/infra/bicep/modules/compute/app-service-plan.bicep +++ b/infra/bicep/modules/compute/app-service-plan.bicep @@ -1,44 +1,60 @@ // ============================================================================ // Module: App Service Plan -// Description: Vanilla Bicep module for an Azure App Service Plan (Linux). -// Resource: Microsoft.Web/serverfarms@2024-04-01 +// Description: Creates an Azure App Service Plan +// API: Microsoft.Web/serverfarms@2025-05-01 // ============================================================================ -@description('Required. Name of the App Service Plan.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Name of the App Service Plan.') +param name string = 'asp-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true +@description('SKU name for the App Service Plan.') +@allowed(['F1', 'D1', 'B1', 'B2', 'B3', 'S1', 'S2', 'S3', 'P1', 'P2', 'P3', 'P4', 'P0v3', 'P0v4', 'P1v3', 'P1v4', 'P2v3', 'P3v3']) +param skuName string = 'B2' -@description('Optional. SKU name of the App Service Plan.') -param skuName string = 'B1' +@description('Whether the plan is Linux-based.') +param reserved bool = true -@description('Optional. Number of instances.') +@description('Kind of the App Service Plan.') +param kind string = 'linux' + +@description('Number of instances (workers).') param skuCapacity int = 1 -resource serverFarm 'Microsoft.Web/serverfarms@2024-04-01' = { +@description('Enable zone redundancy. Requires Premium SKU (P1v3+).') +param zoneRedundant bool = false + +// ============================================================================ +// Resource Deployment +// ============================================================================ +resource appServicePlan 'Microsoft.Web/serverfarms@2025-05-01' = { name: name location: location tags: tags - kind: 'linux' + kind: kind sku: { name: skuName capacity: skuCapacity } properties: { - reserved: true + reserved: reserved + zoneRedundant: zoneRedundant } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the App Service Plan.') -output resourceId string = serverFarm.id +output resourceId string = appServicePlan.id @description('Name of the App Service Plan.') -output name string = serverFarm.name +output name string = appServicePlan.name diff --git a/infra/bicep/modules/compute/app-service.bicep b/infra/bicep/modules/compute/app-service.bicep index 0fae4797c..39cd9565c 100644 --- a/infra/bicep/modules/compute/app-service.bicep +++ b/infra/bicep/modules/compute/app-service.bicep @@ -1,175 +1,134 @@ -@description('Required. Name of the site.') -param name string +// ============================================================================ +// Module: App Service +// Description: Creates an Azure App Service (Web App) +// API: Microsoft.Web/sites@2025-05-01 +// ============================================================================ -@description('Optional. Location for all Resources.') -param location string = resourceGroup().location +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Type of site to deploy.') -@allowed([ - 'functionapp' - 'functionapp,linux' - 'functionapp,workflowapp' - 'functionapp,workflowapp,linux' - 'functionapp,linux,container' - 'functionapp,linux,container,azurecontainerapps' - 'app,linux' - 'app' - 'linux,api' - 'api' - 'app,linux,container' - 'app,container,windows' -]) -param kind string - -@description('Required. The resource ID of the app service plan to use for the site.') -param serverFarmResourceId string - -@description('Optional. Configures a site to accept only HTTPS requests.') -param httpsOnly bool = true +@description('Name of the App Service.') +param name string = solutionName -@description('Optional. If client affinity is enabled.') -param clientAffinityEnabled bool = true +@description('Azure region for the resource.') +param location string -@description('Optional. The managed identity definition for this resource.') -param managedIdentities { - @description('Optional. Enables system assigned managed identity on the resource.') - systemAssigned: bool? +@description('Tags to apply to the resource.') +param tags object = {} - @description('Optional. The resource ID(s) to assign to the resource.') - userAssignedResourceIds: string[]? -}? +@description('Resource ID of the App Service Plan.') +param serverFarmResourceId string -@description('Optional. The resource ID of the assigned identity to be used to access a key vault with.') -param keyVaultAccessIdentityResourceId string? +@description('Docker image name (e.g., DOCKER|registry.azurecr.io/image:tag).') +param linuxFxVersion string -@description('Optional. Checks if Customer provided storage account is required.') -param storageAccountRequired bool = false +@description('Application settings key-value pairs.') +param appSettings object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true +@description('Whether to enable Always On.') +param alwaysOn bool = true -@description('Optional. The site config object.') -param siteConfig resourceInput<'Microsoft.Web/sites@2025-03-01'>.properties.siteConfig = { - alwaysOn: true - minTlsVersion: '1.2' - ftpsState: 'FtpsOnly' -} +@description('Optional. Health check path for the app.') +param healthCheckPath string = '' -@description('Optional. The web site config.') -param configs appSettingsConfigType[]? +@description('Optional. Whether to enable WebSockets.') +param webSocketsEnabled bool = false -@description('Optional. Tags of the resource.') -param tags object? +@description('Optional. Command line for the application.') +param appCommandLine string = '' -@description('Optional. Whether or not public network access is allowed for this resource.') +@description('Required. Type of site to deploy.') @allowed([ - 'Enabled' - 'Disabled' + 'functionapp' // function app windows os + 'functionapp,linux' // function app linux os + 'functionapp,workflowapp' // logic app workflow + 'functionapp,workflowapp,linux' // logic app docker container + 'functionapp,linux,container' // function app linux container + 'functionapp,linux,container,azurecontainerapps' // function app linux container azure container apps + 'app,linux' // linux web app + 'app' // windows web app + 'linux,api' // linux api app + 'api' // windows api app + 'app,linux,container' // linux container app + 'app,container,windows' // windows container app ]) -param publicNetworkAccess string? - -@description('Optional. End to End Encryption Setting.') -param e2eEncryptionEnabled bool? - -var formattedUserAssignedIdentities = reduce( - map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }), - {}, - (cur, next) => union(cur, next) -) - -var identity = !empty(managedIdentities) - ? { - type: (managedIdentities.?systemAssigned ?? false) - ? (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned') - : (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : 'None') - userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null - } - : null +param kind string = 'app,linux' -resource app 'Microsoft.Web/sites@2025-03-01' = { +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' + +// ============================================================================ +// Resource Deployment +// ============================================================================ +resource appService 'Microsoft.Web/sites@2025-05-01' = { name: name location: location - kind: kind tags: tags - identity: identity + kind: kind + identity: { + type: 'SystemAssigned' + } properties: { serverFarmId: serverFarmResourceId - clientAffinityEnabled: clientAffinityEnabled - httpsOnly: httpsOnly - storageAccountRequired: storageAccountRequired - keyVaultReferenceIdentity: keyVaultAccessIdentityResourceId - siteConfig: siteConfig - publicNetworkAccess: !empty(publicNetworkAccess) ? any(publicNetworkAccess) : 'Enabled' - endToEndEncryptionEnabled: e2eEncryptionEnabled + publicNetworkAccess: publicNetworkAccess + siteConfig: { + alwaysOn: alwaysOn + ftpsState: 'Disabled' + linuxFxVersion: linuxFxVersion + minTlsVersion: '1.2' + healthCheckPath: !empty(healthCheckPath) ? healthCheckPath : null + webSocketsEnabled: webSocketsEnabled + appCommandLine: appCommandLine + } + endToEndEncryptionEnabled: true } -} -module app_config 'app-service.config.bicep' = [ - for (config, index) in (configs ?? []): { - name: '${uniqueString(deployment().name, location)}-Site-Config-${index}' - params: { - appName: app.name - name: config.name - applicationInsightResourceId: config.?applicationInsightResourceId - storageAccountResourceId: config.?storageAccountResourceId - storageAccountUseIdentityAuthentication: config.?storageAccountUseIdentityAuthentication - properties: config.?properties - currentAppSettings: config.?retainCurrentAppSettings ?? true && config.name == 'appsettings' - ? list('${app.id}/config/appsettings', '2023-12-01').properties - : {} + resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { + name: 'ftp' + properties: { + allow: false } } -] - -@description('The name of the site.') -output name string = app.name - -@description('The resource ID of the site.') -output resourceId string = app.id - -@description('The resource group the site was deployed into.') -output resourceGroupName string = resourceGroup().name - -@description('The principal ID of the system assigned identity.') -output systemAssignedMIPrincipalId string? = app.?identity.?principalId - -@description('The location the resource was deployed into.') -output location string = app.location - -@description('Default hostname of the app.') -output defaultHostname string = 'https://${name}.azurewebsites.net' - -@description('Unique identifier that verifies the custom domains assigned to the app.') -output customDomainVerificationId string = app.properties.customDomainVerificationId + resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = { + name: 'scm' + properties: { + allow: false + } + } +} -@description('The outbound IP addresses of the app.') -output outboundIpAddresses string = app.properties.outboundIpAddresses +resource configAppSettings 'Microsoft.Web/sites/config@2025-05-01' = { + name: 'appsettings' + parent: appService + properties: appSettings +} -// ================ // -// Definitions // -// ================ // -@export() -@description('The type of an app settings configuration.') -type appSettingsConfigType = { - @description('Required. The type of config.') - name: 'appsettings' | 'logs' +resource configLogs 'Microsoft.Web/sites/config@2025-05-01' = { + name: 'logs' + parent: appService + properties: { + applicationLogs: { fileSystem: { level: 'Verbose' } } + detailedErrorMessages: { enabled: true } + failedRequestsTracing: { enabled: true } + httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } + } + dependsOn: [configAppSettings] +} - @description('Optional. If the provided storage account requires Identity based authentication.') - storageAccountUseIdentityAuthentication: bool? +// ============================================================================ +// Outputs +// ============================================================================ +@description('Resource ID of the App Service.') +output resourceId string = appService.id - @description('Optional. Required if app of kind functionapp. Resource ID of the storage account to manage triggers and logging function executions.') - storageAccountResourceId: string? +@description('Name of the App Service.') +output name string = appService.name - @description('Optional. Resource ID of the application insight to leverage for this resource.') - applicationInsightResourceId: string? +@description('Default hostname of the App Service.') +output defaultHostname string = appService.properties.defaultHostName - @description('Optional. The retain the current app settings. Defaults to true.') - retainCurrentAppSettings: bool? +@description('URL of the App Service.') +output appUrl string = 'https://${appService.properties.defaultHostName}' - @description('Optional. The app settings key-value pairs.') - properties: { - @description('Required. An app settings key-value pair.') - *: string - }? -} +@description('System-assigned identity principal ID.') +output identityPrincipalId string = appService.identity.principalId diff --git a/infra/bicep/modules/compute/container-instance.bicep b/infra/bicep/modules/compute/container-instance.bicep index 02ccf94c2..e3d690ff6 100644 --- a/infra/bicep/modules/compute/container-instance.bicep +++ b/infra/bicep/modules/compute/container-instance.bicep @@ -1,71 +1,75 @@ -// ========== container-instance.bicep ========== // -// Azure Container Instance module for backend API deployment. -// NOTE: Not used by the lean main.bicep (which inlines the ACI resource); retained -// for module parity with the AVM flavor across GSA. +// ============================================================================ +// Module: Azure Container Instance +// Description: Creates an Azure Container Instance group +// API: Microsoft.ContainerInstance/containerGroups@2025-09-01 +// ============================================================================ -@description('Required. Name of the container group.') +@description('Name of the container group.') param name string -@description('Required. Location for the container instance.') +@description('Azure region for deployment.') param location string -@description('Optional. Tags for all resources.') +@description('Resource tags.') param tags object = {} -@description('Required. Container image to deploy.') +@description('Container image to deploy.') param containerImage string -@description('Optional. CPU cores for the container.') +@description('CPU cores for the container.') param cpu int = 2 -@description('Optional. Memory in GB for the container.') +@description('Memory in GB for the container.') param memoryInGB int = 4 -@description('Optional. Port to expose.') +@description('Port to expose.') param port int = 8000 -@description('Optional. Subnet resource ID for VNet integration. If empty, public IP will be used.') -param subnetResourceId string = '' +@description('Environment variables for the container.') +param environmentVariables array = [] -@description('Required. Environment variables for the container.') -param environmentVariables array +@description('Operating system type.') +@allowed(['Linux', 'Windows']) +param osType string = 'Linux' -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true +@description('Restart policy.') +@allowed(['Always', 'OnFailure', 'Never']) +param restartPolicy string = 'Always' -@description('Required. User-assigned managed identity resource ID for ACR pull.') -param userAssignedIdentityResourceId string +@description('Managed identity configuration.') +param managedIdentities object = {} -var isPrivateNetworking = !empty(subnetResourceId) +@description('Image registry credentials.') +param imageRegistryCredentials array = [] -// ============== // -// Resources // -// ============== // +@description('Subnet resource ID for VNet integration. If empty, public IP is used.') +param subnetResourceId string = '' -#disable-next-line no-deployments-resources -resource avmTelemetry 'Microsoft.Resources/deployments@2025-04-01' = if (enableTelemetry) { - name: '46d3xbcp.res.containerinstance.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}' - properties: { - mode: 'Incremental' - template: { - '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' - contentVersion: '1.0.0.0' - resources: [] - } - } +@description('Availability zone for the container group. Use -1 for no zone.') +param availabilityZone int = -1 + +// ============================================================================ +// Variables +// ============================================================================ +var isPrivateNetworking = !empty(subnetResourceId) + +var identityConfig = empty(managedIdentities) ? { type: 'None' } : { + type: contains(managedIdentities, 'userAssignedResourceIds') ? 'UserAssigned' : 'SystemAssigned' + userAssignedIdentities: contains(managedIdentities, 'userAssignedResourceIds') ? reduce(managedIdentities.userAssignedResourceIds, {}, (cur, id) => union(cur, { '${id}': {} })) : null } +// ============================================================================ +// Resource Deployment +// ============================================================================ resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2025-09-01' = { name: name location: location tags: tags - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${userAssignedIdentityResourceId}': {} - } - } + identity: identityConfig + zones: availabilityZone != -1 ? [string(availabilityZone)] : null properties: { + osType: osType + restartPolicy: restartPolicy containers: [ { name: name @@ -87,13 +91,8 @@ resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2025-09-01' } } ] - osType: 'Linux' - restartPolicy: 'Always' - subnetIds: isPrivateNetworking ? [ - { - id: subnetResourceId - } - ] : null + imageRegistryCredentials: imageRegistryCredentials + subnetIds: isPrivateNetworking ? [{ id: subnetResourceId }] : null ipAddress: { type: isPrivateNetworking ? 'Private' : 'Public' ports: [ @@ -104,27 +103,17 @@ resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2025-09-01' ] dnsNameLabel: isPrivateNetworking ? null : name } - imageRegistryCredentials: [ - { - server: split(containerImage, '/')[0] - identity: userAssignedIdentityResourceId - } - ] } } -// ============== // -// Outputs // -// ============== // - +// ============================================================================ +// Outputs +// ============================================================================ @description('The name of the container group.') output name string = containerGroup.name @description('The resource ID of the container group.') output resourceId string = containerGroup.id -@description('The IP address of the container (private or public depending on mode).') +@description('The IP address of the container group.') output ipAddress string = containerGroup.properties.ipAddress.ip - -@description('The FQDN of the container (only available for public mode).') -output fqdn string = isPrivateNetworking ? '' : containerGroup.properties.ipAddress.fqdn diff --git a/infra/bicep/modules/compute/container-registry.bicep b/infra/bicep/modules/compute/container-registry.bicep index d78b34829..9566d2182 100644 --- a/infra/bicep/modules/compute/container-registry.bicep +++ b/infra/bicep/modules/compute/container-registry.bicep @@ -1,58 +1,75 @@ // ============================================================================ -// Module: Container Registry -// Description: Vanilla Bicep module for an Azure Container Registry used by the -// Docker build (bicep) deployment flavor. AZD builds and pushes the -// application images here. -// Resource: Microsoft.ContainerRegistry/registries@2023-11-01-preview +// Module: Azure Container Registry +// Description: Creates an Azure Container Registry +// API: Microsoft.ContainerRegistry/registries@2025-04-01 // ============================================================================ -@description('Required. Name of the container registry.') -param name string +@description('Solution name used for naming convention.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Name of the container registry.') +param name string = replace('cr${solutionName}', '-', '') + +@description('Azure region for deployment.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Resource tags.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true +@description('SKU for the container registry.') +@allowed(['Basic', 'Standard', 'Premium']) +param sku string = 'Premium' + +@description('Enable admin user.') +param adminUserEnabled bool = false + +@description('Public network access setting.') +@allowed(['Enabled', 'Disabled']) +param publicNetworkAccess string = 'Enabled' -@description('Required. Principal ID of the managed identity to grant AcrPull.') -param principalId string +@description('Export policy status.') +param exportPolicyStatus string = 'enabled' -resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = { +// ============================================================================ +// Resource Deployment +// ============================================================================ +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2025-04-01' = { name: name location: location tags: tags sku: { - name: 'Standard' + name: sku } properties: { - adminUserEnabled: false - anonymousPullEnabled: false - publicNetworkAccess: 'Enabled' + adminUserEnabled: adminUserEnabled + publicNetworkAccess: publicNetworkAccess + dataEndpointEnabled: false networkRuleBypassOptions: 'AzureServices' + policies: { + exportPolicy: { + status: exportPolicyStatus + } + retentionPolicy: { + status: 'enabled' + days: 7 + } + trustPolicy: { + status: 'disabled' + type: 'Notary' + } + } + zoneRedundancy: 'Disabled' } } -// AcrPull role assignment for the managed identity. -resource acrPullRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(registry.id, principalId, '7f951dda-4ed3-4680-a7ca-43fe172d538d') - scope: registry - properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // AcrPull - principalId: principalId - principalType: 'ServicePrincipal' - } -} - -@description('Resource ID of the container registry.') -output resourceId string = registry.id +// ============================================================================ +// Outputs +// ============================================================================ +@description('The name of the container registry.') +output name string = containerRegistry.name -@description('Name of the container registry.') -output name string = registry.name +@description('The login server URL.') +output loginServer string = containerRegistry.properties.loginServer -@description('Login server of the container registry.') -output loginServer string = registry.properties.loginServer +@description('The resource ID of the container registry.') +output resourceId string = containerRegistry.id \ No newline at end of file diff --git a/infra/bicep/modules/compute/kubernetes.bicep b/infra/bicep/modules/compute/kubernetes.bicep index a12df945e..44e294404 100644 --- a/infra/bicep/modules/compute/kubernetes.bicep +++ b/infra/bicep/modules/compute/kubernetes.bicep @@ -132,3 +132,9 @@ output resourceId string = aksCluster.id @description('FQDN of the AKS cluster.') output fqdn string = aksCluster.properties.fqdn + +@description('Object ID of the AKS kubelet system-assigned managed identity (used by pods at runtime via IMDS).') +output kubeletIdentityObjectId string = aksCluster.properties.?identityProfile.?kubeletidentity.?objectId ?? '' + +@description('Principal ID of the AKS control-plane system-assigned managed identity.') +output systemAssignedMIPrincipalId string = aksCluster.identity.?principalId ?? '' diff --git a/infra/bicep/modules/data/cosmos-db-nosql.bicep b/infra/bicep/modules/data/cosmos-db-nosql.bicep index 6cd311f18..9c758c2ec 100644 --- a/infra/bicep/modules/data/cosmos-db-nosql.bicep +++ b/infra/bicep/modules/data/cosmos-db-nosql.bicep @@ -1,47 +1,42 @@ // ============================================================================ -// Module: Cosmos DB (NoSQL) -// Description: Vanilla Bicep module for an Azure Cosmos DB account (SQL/NoSQL API) -// used for conversation history and product metadata. -// Resource: Microsoft.DocumentDB/databaseAccounts@2024-11-15 +// Module: Cosmos DB +// Description: Creates an Azure Cosmos DB (NoSQL) account with database/container +// API: Microsoft.DocumentDB/databaseAccounts@2025-10-15 // ============================================================================ -@description('Required. Name of the Cosmos DB account.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Name of the Cosmos DB account.') +param name string = 'cosmos-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true - -@description('Required. Name of the SQL database.') -param databaseName string - -@description('Required. Containers to create in the SQL database. Each item: { name, paths }.') -param containers array - -@description('Required. Principal ID of the managed identity to grant data contributor.') -param principalId string - -@description('Required. Principal ID of the deploying user/service principal.') -param deployerPrincipalId string +@description('Database name.') +param databaseName string = 'db_conversation_history' -var cosmosDataContributorRoleId = '00000000-0000-0000-0000-000000000002' // Built-in Cosmos DB Data Contributor +@description('Container definitions.') +param containers array = [ + { + name: 'conversations' + partitionKeyPath: '/userId' + } +] -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' = { +// ============================================================================ +// Resource Deployment +// ============================================================================ +resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2025-10-15' = { name: name location: location tags: tags kind: 'GlobalDocumentDB' properties: { - databaseAccountOfferType: 'Standard' - consistencyPolicy: { - defaultConsistencyLevel: 'Session' - } + consistencyPolicy: { defaultConsistencyLevel: 'Session' } locations: [ { locationName: location @@ -49,86 +44,47 @@ resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-11-15' = { isZoneRedundant: false } ] + databaseAccountOfferType: 'Standard' enableAutomaticFailover: false - capabilities: [ - { - name: 'EnableServerless' - } - ] - publicNetworkAccess: 'Enabled' - networkAclBypass: 'AzureServices' + enableMultipleWriteLocations: false + disableLocalAuth: true + capabilities: [ { name: 'EnableServerless' } ] } } -resource sqlDatabase 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2024-11-15' = { +resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2025-10-15' = { parent: cosmos name: databaseName properties: { - resource: { - id: databaseName - } + resource: { id: databaseName } } -} -resource sqlContainers 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers@2024-11-15' = [ - for container in containers: { - parent: sqlDatabase + resource list 'containers' = [for container in containers: { name: container.name properties: { resource: { id: container.name - partitionKey: { - paths: container.paths - kind: 'Hash' - } + partitionKey: { paths: [ container.partitionKeyPath ] } } + options: {} } - } -] - -resource dataContributorRoleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2024-11-15' = { - parent: cosmos - name: guid(cosmos.id, 'contentgen-data-contributor') - properties: { - roleName: 'contentgen-data-contributor' - type: 'CustomRole' - assignableScopes: [ - cosmos.id - ] - permissions: [ - { - dataActions: [ - 'Microsoft.DocumentDB/databaseAccounts/readMetadata' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' - 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' - ] - } - ] - } -} - -resource identityRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-11-15' = { - parent: cosmos - name: guid(cosmos.id, principalId, cosmosDataContributorRoleId) - properties: { - roleDefinitionId: '${cosmos.id}/sqlRoleDefinitions/${cosmosDataContributorRoleId}' - principalId: principalId - scope: cosmos.id - } -} - -resource deployerRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-11-15' = { - parent: cosmos - name: guid(cosmos.id, deployerPrincipalId, cosmosDataContributorRoleId) - properties: { - roleDefinitionId: '${cosmos.id}/sqlRoleDefinitions/${cosmosDataContributorRoleId}' - principalId: deployerPrincipalId - scope: cosmos.id - } + }] } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the Cosmos DB account.') output resourceId string = cosmos.id @description('Name of the Cosmos DB account.') output name string = cosmos.name + +@description('Endpoint of the Cosmos DB account.') +output endpoint string = 'https://${name}.documents.azure.com:443/' + +@description('Database name.') +output databaseName string = databaseName + +@description('Container name (first container).') +output containerName string = containers[0].name diff --git a/infra/bicep/modules/data/storage-account.bicep b/infra/bicep/modules/data/storage-account.bicep index 3a4a1d46d..dc3e93f18 100644 --- a/infra/bicep/modules/data/storage-account.bicep +++ b/infra/bicep/modules/data/storage-account.bicep @@ -1,52 +1,67 @@ // ============================================================================ // Module: Storage Account -// Description: Vanilla Bicep module for an Azure Storage Account (blob) used for -// product and generated image storage. -// Resource: Microsoft.Storage/storageAccounts@2024-01-01 +// Description: Creates an Azure Storage Account with blob container +// API: Microsoft.Storage/storageAccounts@2025-08-01 // ============================================================================ -@description('Required. Name of the storage account.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Name of the storage account.') +param name string = take('st${toLower(replace(solutionName, '-', ''))}', 24) + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true - -@description('Optional. Storage account SKU.') +@description('Storage account SKU.') param skuName string = 'Standard_LRS' -@description('Required. Blob containers to create. Each item: { name, publicAccess }.') -param containers array +@description('Storage account kind.') +param kind string = 'StorageV2' + +@description('Access tier.') +@allowed(['Hot', 'Cool']) +param accessTier string = 'Hot' -@description('Required. Principal ID of the managed identity to grant Storage Blob Data Contributor.') -param principalId string +@description('Allow blob public access.') +param allowBlobPublicAccess bool = false + +@description('Allow shared key access.') +param allowSharedKeyAccess bool = true + +@description('Enable hierarchical namespace (Data Lake Storage Gen2).') +param enableHierarchicalNamespace bool = false + +@description('Blob containers to create.') +param containers array = [ + { + name: 'default' + publicAccess: 'None' + } +] -resource storageAccount 'Microsoft.Storage/storageAccounts@2024-01-01' = { +// ============================================================================ +// Resource Deployment +// ============================================================================ +resource storageAccount 'Microsoft.Storage/storageAccounts@2025-08-01' = { name: name location: location tags: tags - kind: 'StorageV2' + kind: kind sku: { name: skuName } - identity: { - type: 'SystemAssigned' - } properties: { - accessTier: 'Hot' + accessTier: accessTier + allowBlobPublicAccess: allowBlobPublicAccess + allowSharedKeyAccess: allowSharedKeyAccess minimumTlsVersion: 'TLS1_2' supportsHttpsTrafficOnly: true - allowBlobPublicAccess: false - publicNetworkAccess: 'Enabled' + isHnsEnabled: enableHierarchicalNamespace encryption: { - requireInfrastructureEncryption: true - keySource: 'Microsoft.Storage' services: { blob: { enabled: true @@ -55,52 +70,36 @@ resource storageAccount 'Microsoft.Storage/storageAccounts@2024-01-01' = { enabled: true } } - } - networkAcls: { - bypass: 'AzureServices' - defaultAction: 'Allow' + keySource: 'Microsoft.Storage' + requireInfrastructureEncryption: true } } } -resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { +resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2025-08-01' = { parent: storageAccount name: 'default' - properties: { - containerDeleteRetentionPolicy: { - enabled: true - days: 7 - } - deleteRetentionPolicy: { - enabled: true - days: 7 - } - } } -resource blobContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = [ - for container in containers: { - parent: blobServices - name: container.name - properties: { - publicAccess: container.?publicAccess ?? 'None' - } - } -] - -// Storage Blob Data Contributor role assignment for the managed identity. -resource blobDataContributorRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(storageAccount.id, principalId, 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') - scope: storageAccount +resource blobContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-08-01' = [for container in containers: { + parent: blobService + name: container.name properties: { - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor - principalId: principalId - principalType: 'ServicePrincipal' + publicAccess: container.publicAccess } -} +}] -@description('Resource ID of the storage account.') +// ============================================================================ +// Outputs +// ============================================================================ +@description('Resource ID of the Storage Account.') output resourceId string = storageAccount.id -@description('Name of the storage account.') +@description('Name of the Storage Account.') output name string = storageAccount.name + +@description('Primary blob endpoint.') +output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob + +@description('All service endpoints.') +output serviceEndpoints object = storageAccount.properties.primaryEndpoints diff --git a/infra/bicep/modules/monitoring/app-insights.bicep b/infra/bicep/modules/monitoring/app-insights.bicep index e9c4bbf75..21109d756 100644 --- a/infra/bicep/modules/monitoring/app-insights.bicep +++ b/infra/bicep/modules/monitoring/app-insights.bicep @@ -1,47 +1,75 @@ // ============================================================================ // Module: Application Insights -// Description: Vanilla Bicep module for an Azure Application Insights component. +// Description: Vanilla Bicep module for Application Insights // Resource: Microsoft.Insights/components@2020-02-02 +// Docs: https://learn.microsoft.com/azure/templates/microsoft.insights/components // ============================================================================ -@description('Required. Name of the Application Insights component.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Optional. Override name for the Application Insights instance. Defaults to appi-{solutionName}.') +param name string = 'appi-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true - -@description('Required. Resource ID of the Log Analytics workspace to link.') +@description('Resource ID of the Log Analytics workspace to link to.') param workspaceResourceId string -resource component 'Microsoft.Insights/components@2020-02-02' = { +@description('Application type.') +param applicationType string = 'web' + +@description('Retention period in days.') +param retentionInDays int = 365 + +@description('Disable IP masking for security.') +param disableIpMasking bool = false + +@description('Flow type for Application Insights.') +param flowType string = 'Bluefield' + +@description('Kind of Application Insights resource.') +param kind string = 'web' + +// ============================================================================ +// Resource +// ============================================================================ + +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { name: name location: location tags: tags - kind: 'web' + kind: kind properties: { - Application_Type: 'web' - Flow_Type: 'Bluefield' - RetentionInDays: 365 - DisableIpMasking: false - IngestionMode: 'LogAnalytics' + Application_Type: applicationType + Flow_Type: flowType WorkspaceResourceId: workspaceResourceId + RetentionInDays: retentionInDays + DisableIpMasking: disableIpMasking publicNetworkAccessForIngestion: 'Enabled' publicNetworkAccessForQuery: 'Enabled' } } -@description('Resource ID of the Application Insights component.') -output resourceId string = component.id +// ============================================================================ +// Outputs +// ============================================================================ + +@description('Resource ID of the Application Insights instance.') +output resourceId string = appInsights.id + +@description('Name of the Application Insights instance.') +output name string = appInsights.name + +@description('Instrumentation key for the Application Insights instance.') +output instrumentationKey string = appInsights.properties.InstrumentationKey -@description('Name of the Application Insights component.') -output name string = component.name +@description('Connection string for the Application Insights instance.') +output connectionString string = appInsights.properties.ConnectionString -@description('Connection string of the Application Insights component.') -output connectionString string = component.properties.ConnectionString +@description('Application ID of the Application Insights instance.') +output applicationId string = appInsights.properties.AppId diff --git a/infra/bicep/modules/monitoring/log-analytics.bicep b/infra/bicep/modules/monitoring/log-analytics.bicep index 971e01121..87d79740c 100644 --- a/infra/bicep/modules/monitoring/log-analytics.bicep +++ b/infra/bicep/modules/monitoring/log-analytics.bicep @@ -1,39 +1,58 @@ // ============================================================================ // Module: Log Analytics Workspace -// Description: Vanilla Bicep module for an Azure Log Analytics Workspace. -// Resource: Microsoft.OperationalInsights/workspaces@2025-02-01 +// Description: Vanilla Bicep module for Log Analytics Workspace +// Resource: Microsoft.OperationalInsights/workspaces@2023-09-01 +// Docs: https://learn.microsoft.com/azure/templates/microsoft.operationalinsights/workspaces +// Note: This module only handles NEW workspace creation. +// Existing workspace logic is handled in main.bicep. // ============================================================================ -@description('Required. Name of the Log Analytics workspace.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Optional. Override name for the Log Analytics workspace. Defaults to log-{solutionName}.') +param name string = 'log-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true +@description('Retention period in days.') +param retentionInDays int = 365 + +@description('SKU name for the workspace.') +param skuName string = 'PerGB2018' + +// ============================================================================ +// Resource +// ============================================================================ -resource workspace 'Microsoft.OperationalInsights/workspaces@2025-02-01' = { +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { name: name location: location tags: tags properties: { + retentionInDays: retentionInDays sku: { - name: 'PerGB2018' - } - retentionInDays: 365 - features: { - enableLogAccessUsingOnlyResourcePermissions: true + name: skuName } } } +// ============================================================================ +// Outputs +// ============================================================================ + @description('Resource ID of the Log Analytics workspace.') -output resourceId string = workspace.id +output resourceId string = logAnalytics.id @description('Name of the Log Analytics workspace.') -output name string = workspace.name +output name string = logAnalytics.name + +@description('Location of the workspace.') +output location string = logAnalytics.location + +@description('Log Analytics workspace customer ID.') +output logAnalyticsWorkspaceId string = logAnalytics.properties.customerId From 4da589ba5ae0d7a6f2bf735f7c360079dbe35364 Mon Sep 17 00:00:00 2001 From: nagshetti Date: Tue, 16 Jun 2026 19:13:53 +0530 Subject: [PATCH 3/4] latest changes from repo --- .../modules/ai/ai-foundry-connection.bicep | 2 +- .../ai/ai-foundry-model-deployment.bicep | 4 +- .../bicep/modules/ai/ai-foundry-project.bicep | 62 ++- infra/bicep/modules/ai/ai-search.bicep | 66 ++- infra/bicep/modules/ai/ai-services.bicep | 74 +++- .../modules/ai/existing-project-setup.bicep | 8 +- .../modules/compute/app-service-plan.bicep | 37 +- infra/bicep/modules/compute/app-service.bicep | 135 ++++--- .../compute/container-app-environment.bicep | 77 ++-- .../bicep/modules/compute/container-app.bicep | 74 ++-- .../modules/compute/container-instance.bicep | 83 ++-- .../modules/compute/container-registry.bicep | 100 +++-- .../bicep/modules/compute/function-app.bicep | 77 ++-- infra/bicep/modules/compute/kubernetes.bicep | 110 +++-- .../compute/maintenance-configuration.bicep | 84 ++++ .../compute/proximity-placement-group.bicep | 51 +++ .../modules/compute/virtual-machine.bicep | 236 +++++------ .../modules/data/app-configuration.bicep | 91 +++-- .../bicep/modules/data/cosmos-db-mongo.bicep | 125 +++--- .../bicep/modules/data/cosmos-db-nosql.bicep | 136 +++++-- infra/bicep/modules/data/event-grid.bicep | 62 ++- infra/bicep/modules/data/event-hub.bicep | 94 +++-- .../data/postgresql-flexible-server.bicep | 197 ++++----- infra/bicep/modules/data/sql-database.bicep | 129 +++--- .../bicep/modules/data/storage-account.bicep | 116 ++++-- .../modules/fabric/fabric-capacity.bicep | 38 +- .../cross-scope-role-assignment.bicep | 8 +- .../modules/identity/managed-identity.bicep | 36 +- .../modules/identity/role-assignments.bicep | 70 ---- .../modules/monitoring/app-insights.bicep | 53 +-- .../monitoring/data-collection-rule.bicep | 176 +++++--- .../modules/monitoring/log-analytics.bicep | 72 +++- .../modules/monitoring/portal-dashboard.bicep | 30 +- .../modules/networking/bastion-host.bicep | 125 +++--- .../modules/networking/private-dns-zone.bicep | 51 ++- .../modules/networking/private-endpoint.bicep | 83 ++-- .../modules/networking/virtual-network.bicep | 377 ++++++++++-------- infra/bicep/modules/security/key-vault.bicep | 84 ++-- 38 files changed, 2037 insertions(+), 1396 deletions(-) create mode 100644 infra/bicep/modules/compute/maintenance-configuration.bicep create mode 100644 infra/bicep/modules/compute/proximity-placement-group.bicep diff --git a/infra/bicep/modules/ai/ai-foundry-connection.bicep b/infra/bicep/modules/ai/ai-foundry-connection.bicep index 6649b5f74..443de377c 100644 --- a/infra/bicep/modules/ai/ai-foundry-connection.bicep +++ b/infra/bicep/modules/ai/ai-foundry-connection.bicep @@ -1,5 +1,5 @@ // ============================================================================ -// Module: AI Foundry Project Connection (Single) — Vanilla Bicep +// Module: AI Foundry Project Connection (Single) // Description: Creates a single connection on an AI Foundry project. // Generic, reusable — call once per connection type from main.bicep. // Supports any connection category (CognitiveSearch, AzureBlob, diff --git a/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep b/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep index 4ed69a72c..1c534fd88 100644 --- a/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep +++ b/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep @@ -1,12 +1,10 @@ // ============================================================================ -// Module: Model Deployment — Vanilla Bicep +// Module: Model Deployment // Description: Deploys a single AI model to an existing AI Services account. // Called repetitively from main.bicep for each model in the array. // Generic, reusable across GSAs. // ============================================================================ -targetScope = 'resourceGroup' - @description('Required. Name of the parent AI Services account.') param aiServicesAccountName string diff --git a/infra/bicep/modules/ai/ai-foundry-project.bicep b/infra/bicep/modules/ai/ai-foundry-project.bicep index 362dbbad9..69fc4fa7c 100644 --- a/infra/bicep/modules/ai/ai-foundry-project.bicep +++ b/infra/bicep/modules/ai/ai-foundry-project.bicep @@ -1,11 +1,11 @@ // ============================================================================ -// Module: AI Foundry Project (Account + Project) — Vanilla Bicep -// Description: Creates an Azure AI Services account and AI Foundry project. -// Generic, reusable across GSAs — no app-specific parameters. +// Module: AI Foundry Project (Account + Project) +// Description: AVM wrapper for Azure AI Services account creation and +// AI Foundry project provisioning. Generic, reusable across GSAs. +// AVM Module: avm/res/cognitive-services/account +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-openai // ============================================================================ -targetScope = 'resourceGroup' - @description('Required. Solution name suffix used to generate resource names.') param solutionName string @@ -41,21 +41,31 @@ param identityType string = 'SystemAssigned' @allowed(['Allow', 'Deny']) param networkAclsDefaultAction string = 'Allow' +// --- WAF: Monitoring --- +@description('Optional. Diagnostic settings for the resource.') +param diagnosticSettings array? + +// --- WAF: Telemetry --- +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +// --- Role Assignments --- +@description('Optional. Array of role assignments to create on the AI Services account.') +param roleAssignments array? + // ============================================================================ -// AI Services Account +// AI Services Account (AVM Module) // ============================================================================ -resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' = { - name: name - location: location - tags: tags - sku: { - name: skuName - } - kind: 'AIServices' - identity: { - type: identityType - } - properties: { +module aiServicesAccount 'br/public:avm/res/cognitive-services/account:0.14.2' = { + name: take('avm.res.cognitive-services.account.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + sku: skuName + kind: 'AIServices' + disableLocalAuth: disableLocalAuth allowProjectManagement: allowProjectManagement customSubDomainName: name networkAcls: { @@ -64,22 +74,36 @@ resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' = { ipRules: [] } publicNetworkAccess: publicNetworkAccess - disableLocalAuth: disableLocalAuth + managedIdentities: { + systemAssigned: true + } + diagnosticSettings: diagnosticSettings + deployments: [] + roleAssignments: roleAssignments + // Private endpoints deployed separately to avoid AccountProvisioningStateInvalid + privateEndpoints: [] } } // ============================================================================ // AI Foundry Project // ============================================================================ +resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { + name: name + dependsOn: [aiServicesAccount] +} + resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-12-01' = { parent: aiServices name: projectName location: location + tags: tags kind: 'AIServices' identity: { type: identityType } properties: {} + dependsOn: [aiServicesAccount] } // ============================================================================ diff --git a/infra/bicep/modules/ai/ai-search.bicep b/infra/bicep/modules/ai/ai-search.bicep index 798a0f74c..aa0843542 100644 --- a/infra/bicep/modules/ai/ai-search.bicep +++ b/infra/bicep/modules/ai/ai-search.bicep @@ -2,13 +2,13 @@ // Module: AI Search // Description: Deploys Azure AI Search with a two-step pattern: // Step 1: Plain Bicep resource for fast initial creation (name, location, SKU) -// Step 2: Separate module deployment to enable managed identity & full config +// Step 2: AVM module update to enable managed identity & full configuration // This reduces deployment time by making the resource available immediately -// while identity enablement proceeds as a separate ARM deployment. +// while identity enablement proceeds separately. +// AVM Module: avm/res/search/search-service:0.12.0 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-cognitive-search // ============================================================================ -targetScope = 'resourceGroup' - @description('Solution name suffix used to derive the resource name.') @minLength(3) param solutionName string @@ -43,10 +43,10 @@ param semanticSearch string = 'free' @description('Whether to disable local authentication.') param disableLocalAuth bool = true -@description('Optional. Authentication options for the search service.') +@description('Optional. Authentication options for the search service (e.g., aadOrApiKey).') param authOptions object = {} -@description('Optional. Network rule set for the search service.') +@description('Optional. Network rule set for the search service (e.g., bypass: AzureServices).') param networkRuleSet object = {} @description('Managed identity type for the search service.') @@ -55,10 +55,26 @@ param managedIdentityType string = 'SystemAssigned' @description('Public network access setting.') param publicNetworkAccess string = 'Enabled' +// --- WAF: Telemetry --- +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +// --- WAF: Monitoring --- +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] + +// --- WAF: Private Networking --- +@description('Private endpoint configurations.') +param privateEndpoints array = [] + +// --- Role Assignments --- +@description('Optional. Array of role assignments to create on the AI Search service.') +param roleAssignments array = [] + // ============================================================================ -// Step 1: Initial resource creation (fast — no identity) +// Step 1: Initial resource creation (plain Bicep — fast) // ============================================================================ -resource aiSearch 'Microsoft.Search/searchServices@2025-05-01' = { +resource searchService 'Microsoft.Search/searchServices@2025-05-01' = { name: name location: location sku: { @@ -67,39 +83,47 @@ resource aiSearch 'Microsoft.Search/searchServices@2025-05-01' = { } // ============================================================================ -// Step 2: Separate deployment — enables identity & full configuration +// Step 2: AVM update — enables identity & full configuration // ============================================================================ -module searchServiceUpdate 'ai-search-identity.bicep' = { - name: 'searchServiceUpdate' +module searchServiceUpdate 'br/public:avm/res/search/search-service:0.12.0' = { + name: take('avm.res.search.update.${name}', 64) params: { - name: aiSearch.name + name: name location: location tags: tags - skuName: skuName + enableTelemetry: enableTelemetry + sku: skuName replicaCount: replicaCount partitionCount: partitionCount hostingMode: hostingMode semanticSearch: semanticSearch + authOptions: !empty(authOptions) ? authOptions : null disableLocalAuth: disableLocalAuth - authOptions: authOptions - networkRuleSet: networkRuleSet - managedIdentityType: managedIdentityType + networkRuleSet: !empty(networkRuleSet) ? networkRuleSet : null publicNetworkAccess: publicNetworkAccess + managedIdentities: { + systemAssigned: managedIdentityType == 'SystemAssigned' + } + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + privateEndpoints: privateEndpoints + roleAssignments: !empty(roleAssignments) ? roleAssignments : [] } + dependsOn: [ + searchService + ] } // ============================================================================ // Outputs // ============================================================================ - @description('Resource ID of the AI Search service.') -output resourceId string = aiSearch.id +output resourceId string = searchService.id @description('Name of the AI Search service.') -output name string = aiSearch.name +output name string = searchService.name @description('Endpoint URL of the AI Search service.') -output endpoint string = 'https://${aiSearch.name}.search.windows.net' +output endpoint string = 'https://${searchService.name}.search.windows.net' @description('System-assigned identity principal ID.') -output identityPrincipalId string = searchServiceUpdate.outputs.systemAssignedMIPrincipalId +output identityPrincipalId string = searchServiceUpdate.outputs.?systemAssignedMIPrincipalId ?? '' diff --git a/infra/bicep/modules/ai/ai-services.bicep b/infra/bicep/modules/ai/ai-services.bicep index 4c3d6128b..7e27f49d6 100644 --- a/infra/bicep/modules/ai/ai-services.bicep +++ b/infra/bicep/modules/ai/ai-services.bicep @@ -1,8 +1,8 @@ // ============================================================================ // Module: Azure AI Services (Generic) -// Description: Deploys Cognitive Services — supports Content Safety, +// Description: AVM wrapper for Cognitive Services — supports Content Safety, // Speech, Computer Vision, Document Intelligence, and others. -// API: Microsoft.CognitiveServices/accounts@2025-04-01 +// AVM Module: avm/res/cognitive-services/account:0.14.2 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -34,6 +34,9 @@ param location string @description('Tags to apply to the resource.') param tags object = {} +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = false + @description('SKU for the Cognitive Services account.') @allowed(['F0', 'S0', 'S1']) param sku string = 'S0' @@ -48,26 +51,57 @@ param disableLocalAuth bool = true @allowed(['Enabled', 'Disabled']) param publicNetworkAccess string = 'Enabled' +@description('Whether to enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for the private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs.') +param privateDnsZoneResourceIds array = [] + +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] + +@description('Optional. Role assignments for the resource.') +param roleAssignments array = [] + var effectiveSubDomain = !empty(customSubDomainName) ? customSubDomainName : name +var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'dns-zone-${i}' + privateDnsZoneResourceId: zoneId +}] + // ============================================================================ -// Resource +// AVM Module Deployment // ============================================================================ -resource aiService 'Microsoft.CognitiveServices/accounts@2025-12-01' = { - name: name - location: location - tags: tags - kind: kind - sku: { - name: sku - } - identity: { - type: 'SystemAssigned' - } - properties: { +module aiService 'br/public:avm/res/cognitive-services/account:0.14.2' = { + name: take('avm.res.cognitive-services.${namePrefix}.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + kind: kind + sku: sku customSubDomainName: effectiveSubDomain - publicNetworkAccess: publicNetworkAccess disableLocalAuth: disableLocalAuth + managedIdentities: { systemAssigned: true } + publicNetworkAccess: publicNetworkAccess + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + roleAssignments: !empty(roleAssignments) ? roleAssignments : [] + privateEndpoints: enablePrivateNetworking ? [ + { + name: 'pep-${name}' + customNetworkInterfaceName: 'nic-${name}' + subnetResourceId: privateEndpointSubnetId + service: 'account' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: privateDnsZoneConfigs + } + } + ] : [] } } @@ -75,13 +109,13 @@ resource aiService 'Microsoft.CognitiveServices/accounts@2025-12-01' = { // Outputs // ============================================================================ @description('Name of the AI Services account.') -output name string = aiService.name +output name string = aiService.outputs.name @description('Resource ID of the AI Services account.') -output resourceId string = aiService.id +output resourceId string = aiService.outputs.resourceId @description('Endpoint of the AI Services account.') -output endpoint string = aiService.properties.endpoint +output endpoint string = aiService.outputs.endpoint @description('System-assigned identity principal ID.') -output identityPrincipalId string = aiService.identity.principalId +output identityPrincipalId string = aiService.outputs.?systemAssignedMIPrincipalId ?? '' diff --git a/infra/bicep/modules/ai/existing-project-setup.bicep b/infra/bicep/modules/ai/existing-project-setup.bicep index df0acdc5e..cd0fe1f2c 100644 --- a/infra/bicep/modules/ai/existing-project-setup.bicep +++ b/infra/bicep/modules/ai/existing-project-setup.bicep @@ -1,5 +1,5 @@ // ============================================================================ -// Module: Existing AI Foundry Project Reference — Vanilla Bicep +// Module: Existing AI Foundry Project Reference // Description: References an existing AI Services account and project to // retrieve their identities. No deployments, no connections. // Use generic ai-foundry-connection and ai-foundry-model-deployment @@ -15,7 +15,6 @@ param projectName string // ============================================================================ // Existing Resource References // ============================================================================ - resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { name: name } @@ -45,7 +44,7 @@ output cognitiveServicesEndpoint string = aiServices.properties.endpoint output azureOpenAiCuEndpoint string = aiServices.properties.endpoints['Content Understanding'] @description('System-assigned identity principal ID of the AI Services account (empty if none).') -output principalId string = contains(aiServices, 'identity') && contains(aiServices.identity, 'principalId') ? aiServices.identity.principalId : '' +output principalId string = aiServices.identity.?principalId ?? '' @description('Resource ID of the AI Foundry project.') output projectResourceId string = aiProject.id @@ -57,4 +56,5 @@ output projectName string = aiProject.name output projectEndpoint string = aiProject.properties.endpoints['AI Foundry API'] @description('System-assigned identity principal ID of the project (empty if none).') -output projectIdentityPrincipalId string = contains(aiProject, 'identity') && contains(aiProject.identity, 'principalId') ? aiProject.identity.principalId : '' +output projectIdentityPrincipalId string = aiProject.identity.?principalId ?? '' + diff --git a/infra/bicep/modules/compute/app-service-plan.bicep b/infra/bicep/modules/compute/app-service-plan.bicep index f9409f0cf..6e9e72d0c 100644 --- a/infra/bicep/modules/compute/app-service-plan.bicep +++ b/infra/bicep/modules/compute/app-service-plan.bicep @@ -1,7 +1,7 @@ // ============================================================================ // Module: App Service Plan -// Description: Creates an Azure App Service Plan -// API: Microsoft.Web/serverfarms@2025-05-01 +// Description: AVM wrapper for Azure App Service Plan +// AVM Module: avm/res/web/serverfarm:0.7.0 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -26,26 +26,33 @@ param reserved bool = true @description('Kind of the App Service Plan.') param kind string = 'linux' +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + @description('Number of instances (workers).') param skuCapacity int = 1 +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] + @description('Enable zone redundancy. Requires Premium SKU (P1v3+).') param zoneRedundant bool = false // ============================================================================ -// Resource Deployment +// AVM Module Deployment // ============================================================================ -resource appServicePlan 'Microsoft.Web/serverfarms@2025-05-01' = { - name: name - location: location - tags: tags - kind: kind - sku: { - name: skuName - capacity: skuCapacity - } - properties: { +module appServicePlan 'br/public:avm/res/web/serverfarm:0.7.0' = { + name: take('avm.res.web.serverfarm.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + skuName: skuName + skuCapacity: skuCapacity reserved: reserved + kind: kind + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] zoneRedundant: zoneRedundant } } @@ -54,7 +61,7 @@ resource appServicePlan 'Microsoft.Web/serverfarms@2025-05-01' = { // Outputs // ============================================================================ @description('Resource ID of the App Service Plan.') -output resourceId string = appServicePlan.id +output resourceId string = appServicePlan.outputs.resourceId @description('Name of the App Service Plan.') -output name string = appServicePlan.name +output name string = appServicePlan.outputs.name diff --git a/infra/bicep/modules/compute/app-service.bicep b/infra/bicep/modules/compute/app-service.bicep index 39cd9565c..bbbcbf68b 100644 --- a/infra/bicep/modules/compute/app-service.bicep +++ b/infra/bicep/modules/compute/app-service.bicep @@ -1,7 +1,7 @@ // ============================================================================ // Module: App Service -// Description: Creates an Azure App Service (Web App) -// API: Microsoft.Web/sites@2025-05-01 +// Description: AVM wrapper for Azure App Service (Web App) +// AVM Module: avm/res/web/site:0.23.1 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -25,6 +25,9 @@ param linuxFxVersion string @description('Application settings key-value pairs.') param appSettings object = {} +@description('Optional. Resource ID of Application Insights for monitoring integration.') +param applicationInsightResourceId string = '' + @description('Whether to enable Always On.') param alwaysOn bool = true @@ -54,23 +57,46 @@ param appCommandLine string = '' ]) param kind string = 'app,linux' +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] + +@description('Subnet resource ID for VNet integration.') +param virtualNetworkSubnetId string = '' + @description('Public network access setting.') param publicNetworkAccess string = 'Enabled' +@description('Optional. Whether to route all outbound traffic through the virtual network.') +param vnetRouteAllEnabled bool = false + +@description('Optional. Whether to route image pull traffic through the virtual network.') +param imagePullTraffic bool = false + +@description('Optional. Whether to route content share traffic through the virtual network.') +param contentShareTraffic bool = false + +import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' +@description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.') +param privateEndpoints privateEndpointSingleServiceType[]? + // ============================================================================ -// Resource Deployment +// AVM Module Deployment // ============================================================================ -resource appService 'Microsoft.Web/sites@2025-05-01' = { - name: name - location: location - tags: tags - kind: kind - identity: { - type: 'SystemAssigned' - } - properties: { - serverFarmId: serverFarmResourceId - publicNetworkAccess: publicNetworkAccess +module appService 'br/public:avm/res/web/site:0.23.1' = { + name: take('avm.res.web.site.${name}', 64) + params: { + name: name + location: location + tags: tags + kind: kind + enableTelemetry: enableTelemetry + serverFarmResourceId: serverFarmResourceId + managedIdentities: { + systemAssigned: true + } siteConfig: { alwaysOn: alwaysOn ftpsState: 'Disabled' @@ -80,55 +106,64 @@ resource appService 'Microsoft.Web/sites@2025-05-01' = { webSocketsEnabled: webSocketsEnabled appCommandLine: appCommandLine } - endToEndEncryptionEnabled: true - } - - resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { - name: 'ftp' - properties: { - allow: false + e2eEncryptionEnabled: true + configs: [ + { + name: 'appsettings' + properties: appSettings + applicationInsightResourceId: !empty(applicationInsightResourceId) ? applicationInsightResourceId : null + } + { + name: 'logs' + properties: { + applicationLogs: { fileSystem: { level: 'Verbose' } } + detailedErrorMessages: { enabled: true } + failedRequestsTracing: { enabled: true } + httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } + } + } + { + name:'web' + properties: { + vnetRouteAllEnabled: vnetRouteAllEnabled + } + } + ] + outboundVnetRouting: { + contentShareTraffic: contentShareTraffic + imagePullTraffic: imagePullTraffic } + publicNetworkAccess: publicNetworkAccess + privateEndpoints: privateEndpoints + virtualNetworkSubnetResourceId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null + basicPublishingCredentialsPolicies: [ + { + name: 'ftp' + allow: false + } + { + name: 'scm' + allow: false + } + ] + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] } - resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = { - name: 'scm' - properties: { - allow: false - } - } -} - -resource configAppSettings 'Microsoft.Web/sites/config@2025-05-01' = { - name: 'appsettings' - parent: appService - properties: appSettings -} - -resource configLogs 'Microsoft.Web/sites/config@2025-05-01' = { - name: 'logs' - parent: appService - properties: { - applicationLogs: { fileSystem: { level: 'Verbose' } } - detailedErrorMessages: { enabled: true } - failedRequestsTracing: { enabled: true } - httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } - } - dependsOn: [configAppSettings] } // ============================================================================ // Outputs // ============================================================================ @description('Resource ID of the App Service.') -output resourceId string = appService.id +output resourceId string = appService.outputs.resourceId @description('Name of the App Service.') -output name string = appService.name +output name string = appService.outputs.name @description('Default hostname of the App Service.') -output defaultHostname string = appService.properties.defaultHostName +output defaultHostname string = appService.outputs.defaultHostname @description('URL of the App Service.') -output appUrl string = 'https://${appService.properties.defaultHostName}' +output appUrl string = 'https://${appService.outputs.defaultHostname}' @description('System-assigned identity principal ID.') -output identityPrincipalId string = appService.identity.principalId +output identityPrincipalId string = appService.outputs.?systemAssignedMIPrincipalId ?? '' diff --git a/infra/bicep/modules/compute/container-app-environment.bicep b/infra/bicep/modules/compute/container-app-environment.bicep index af51d7366..1f488eed9 100644 --- a/infra/bicep/modules/compute/container-app-environment.bicep +++ b/infra/bicep/modules/compute/container-app-environment.bicep @@ -1,7 +1,6 @@ // ============================================================================ -// Module: Azure Container Apps Environment -// Description: Creates an Azure Container Apps managed environment -// API: Microsoft.App/managedEnvironments@2024-03-01 +// Module: Azure Container Apps Environment (AVM) +// AVM Module: avm/res/app/managed-environment:0.13.3 // ============================================================================ @description('Solution name used for naming convention.') @@ -16,12 +15,33 @@ param location string @description('Resource tags.') param tags object = {} -@description('Resource ID of the Log Analytics workspace.') -param logAnalyticsWorkspaceResourceId string +@description('Resource ID of the Log Analytics workspace (required when enableMonitoring is true).') +param logAnalyticsWorkspaceResourceId string = '' + +@description('Subnet resource ID for VNet integration (required when enablePrivateNetworking is true).') +param infrastructureSubnetId string = '' @description('Enable zone redundancy.') param zoneRedundant bool = false +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + +@description('Enable private networking (internal environment, public access disabled).') +param enablePrivateNetworking bool = false + +@description('Enable monitoring (Log Analytics + App Insights).') +param enableMonitoring bool = true + +@description('Application Insights connection string (optional, for App Insights integration).') +param appInsightsConnectionString string = '' + +@description('Enable redundancy (dedicated workload profiles + infra resource group).') +param enableRedundancy bool = false + +@description('Infrastructure resource group name (used when zone redundancy is enabled). Defaults to "{resourceGroup}-infra" if empty.') +param infrastructureResourceGroupName string = '${resourceGroup().name}-infra' + @description('Workload profiles configuration (e.g., Consumption or dedicated D4 profiles).') param workloadProfiles array = [ { @@ -31,22 +51,31 @@ param workloadProfiles array = [ ] // ============================================================================ -// Resource Deployment +// Container Apps Environment (AVM) // ============================================================================ -resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' = { - name: name - location: location - tags: tags - properties: { - appLogsConfiguration: { - destination: 'log-analytics' - logAnalyticsConfiguration: { - customerId: reference(logAnalyticsWorkspaceResourceId, '2023-09-01').customerId - sharedKey: listKeys(logAnalyticsWorkspaceResourceId, '2023-09-01').primarySharedKey - } - } +module managedEnvironment 'br/public:avm/res/app/managed-environment:0.13.3' = { + name: take('avm.res.app.managedenvironment.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + // WAF: Private networking + publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' + internal: enablePrivateNetworking + infrastructureSubnetResourceId: !empty(infrastructureSubnetId) ? infrastructureSubnetId : null + // WAF: Monitoring + appLogsConfiguration: enableMonitoring && !empty(logAnalyticsWorkspaceResourceId) + ? { + destination: 'log-analytics' + logAnalyticsWorkspaceResourceId: logAnalyticsWorkspaceResourceId + } + : null + appInsightsConnectionString: !empty(appInsightsConnectionString) ? appInsightsConnectionString : null + // WAF: Redundancy + zoneRedundant: zoneRedundant || enableRedundancy + infrastructureResourceGroupName: !empty(infrastructureResourceGroupName) ? infrastructureResourceGroupName : null workloadProfiles: workloadProfiles - zoneRedundant: zoneRedundant } } @@ -54,13 +83,13 @@ resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' // Outputs // ============================================================================ @description('The name of the Container Apps Environment.') -output name string = containerAppEnvironment.name +output name string = managedEnvironment.outputs.name @description('The resource ID of the Container Apps Environment.') -output resourceId string = containerAppEnvironment.id +output resourceId string = managedEnvironment.outputs.resourceId @description('The default domain of the Container Apps Environment.') -output defaultDomain string = containerAppEnvironment.properties.defaultDomain +output defaultDomain string = managedEnvironment.outputs.defaultDomain -@description('The static IP address of the Container Apps Environment.') -output staticIp string = containerAppEnvironment.properties.staticIp +@description('The static IP of the Container Apps Environment.') +output staticIp string = managedEnvironment.outputs.staticIp diff --git a/infra/bicep/modules/compute/container-app.bicep b/infra/bicep/modules/compute/container-app.bicep index 15596c7d2..07e7b4f9e 100644 --- a/infra/bicep/modules/compute/container-app.bicep +++ b/infra/bicep/modules/compute/container-app.bicep @@ -1,7 +1,6 @@ // ============================================================================ -// Module: Azure Container App -// Description: Creates an Azure Container App -// API: Microsoft.App/containerApps@2024-10-02-preview +// Module: Azure Container App (AVM) +// AVM Module: avm/res/app/container-app:0.22.1 // ============================================================================ @description('Name of the container app.') @@ -51,7 +50,7 @@ param corsPolicy object = {} @allowed(['Single', 'Multiple']) param activeRevisionsMode string = 'Single' -@description('Scale settings (maxReplicas, minReplicas, rules).') +@description('Scale settings (maxReplicas, minReplicas, rules, cooldownPeriod, pollingInterval).') param scaleSettings object = { maxReplicas: 10 minReplicas: 0 @@ -60,44 +59,33 @@ param scaleSettings object = { @description('Workload profile name.') param workloadProfileName string? +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + // ============================================================================ -// Resource Deployment +// Container App (AVM) // ============================================================================ -var identityConfig = empty(managedIdentities) ? { type: 'None' } : { - type: contains(managedIdentities, 'userAssignedResourceIds') ? (contains(managedIdentities, 'systemAssigned') && managedIdentities.systemAssigned ? 'SystemAssigned,UserAssigned' : 'UserAssigned') : 'SystemAssigned' - userAssignedIdentities: contains(managedIdentities, 'userAssignedResourceIds') ? reduce(managedIdentities.userAssignedResourceIds, {}, (cur, id) => union(cur, { '${id}': {} })) : null -} - -var ingressConfig = disableIngress ? null : { - external: ingressExternal - targetPort: ingressTargetPort - transport: ingressTransport - allowInsecure: ingressAllowInsecure - corsPolicy: !empty(corsPolicy) ? corsPolicy : null -} - -resource containerApp 'Microsoft.App/containerApps@2024-10-02-preview' = { - name: name - location: location - tags: tags - identity: identityConfig - properties: { - managedEnvironmentId: environmentResourceId +module containerApp 'br/public:avm/res/app/container-app:0.22.1' = { + name: take('avm.res.app.containerapp.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + environmentResourceId: environmentResourceId + containers: containers + ingressExternal: disableIngress ? false : ingressExternal + ingressTargetPort: ingressTargetPort + ingressTransport: ingressTransport + ingressAllowInsecure: ingressAllowInsecure + disableIngress: disableIngress + registries: registries + secrets: secrets + managedIdentities: !empty(managedIdentities) ? managedIdentities : {} + corsPolicy: !empty(corsPolicy) ? corsPolicy : null + activeRevisionsMode: activeRevisionsMode + scaleSettings: scaleSettings workloadProfileName: workloadProfileName - configuration: { - activeRevisionsMode: activeRevisionsMode - ingress: ingressConfig - registries: registries - secrets: secrets - } - template: { - containers: containers - scale: { - minReplicas: scaleSettings.minReplicas - maxReplicas: scaleSettings.maxReplicas - rules: contains(scaleSettings, 'rules') ? scaleSettings.rules : null - } - } } } @@ -105,13 +93,13 @@ resource containerApp 'Microsoft.App/containerApps@2024-10-02-preview' = { // Outputs // ============================================================================ @description('The name of the container app.') -output name string = containerApp.name +output name string = containerApp.outputs.name @description('The resource ID of the container app.') -output resourceId string = containerApp.id +output resourceId string = containerApp.outputs.resourceId @description('The FQDN of the container app.') -output fqdn string = !disableIngress ? containerApp.properties.configuration.ingress.fqdn : '' +output fqdn string = containerApp.outputs.fqdn @description('System-assigned identity principal ID.') -output principalId string = contains(containerApp.identity.type, 'SystemAssigned') ? containerApp.identity.principalId : '' +output principalId string = containerApp.outputs.?systemAssignedMIPrincipalId ?? '' diff --git a/infra/bicep/modules/compute/container-instance.bicep b/infra/bicep/modules/compute/container-instance.bicep index e3d690ff6..c840be20d 100644 --- a/infra/bicep/modules/compute/container-instance.bicep +++ b/infra/bicep/modules/compute/container-instance.bicep @@ -1,7 +1,6 @@ // ============================================================================ -// Module: Azure Container Instance -// Description: Creates an Azure Container Instance group -// API: Microsoft.ContainerInstance/containerGroups@2025-09-01 +// Module: Azure Container Instance (AVM) +// AVM Module: avm/res/container-instance/container-group:0.7.0 // ============================================================================ @description('Name of the container group.') @@ -48,51 +47,50 @@ param subnetResourceId string = '' @description('Availability zone for the container group. Use -1 for no zone.') param availabilityZone int = -1 +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + // ============================================================================ // Variables // ============================================================================ var isPrivateNetworking = !empty(subnetResourceId) -var identityConfig = empty(managedIdentities) ? { type: 'None' } : { - type: contains(managedIdentities, 'userAssignedResourceIds') ? 'UserAssigned' : 'SystemAssigned' - userAssignedIdentities: contains(managedIdentities, 'userAssignedResourceIds') ? reduce(managedIdentities.userAssignedResourceIds, {}, (cur, id) => union(cur, { '${id}': {} })) : null -} +var containers = [ + { + name: name + properties: { + image: containerImage + resources: { + requests: { + cpu: cpu + memoryInGB: string(memoryInGB) + } + } + ports: [ + { + port: port + protocol: 'TCP' + } + ] + environmentVariables: environmentVariables + } + } +] // ============================================================================ -// Resource Deployment +// Container Instance (AVM) // ============================================================================ -resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2025-09-01' = { - name: name - location: location - tags: tags - identity: identityConfig - zones: availabilityZone != -1 ? [string(availabilityZone)] : null - properties: { +module containerGroup 'br/public:avm/res/container-instance/container-group:0.7.0' = { + name: take('avm.res.containerinstance.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + containers: containers osType: osType restartPolicy: restartPolicy - containers: [ - { - name: name - properties: { - image: containerImage - resources: { - requests: { - cpu: cpu - memoryInGB: memoryInGB - } - } - ports: [ - { - port: port - protocol: 'TCP' - } - ] - environmentVariables: environmentVariables - } - } - ] - imageRegistryCredentials: imageRegistryCredentials - subnetIds: isPrivateNetworking ? [{ id: subnetResourceId }] : null + managedIdentities: !empty(managedIdentities) ? managedIdentities : {} ipAddress: { type: isPrivateNetworking ? 'Private' : 'Public' ports: [ @@ -103,6 +101,9 @@ resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2025-09-01' ] dnsNameLabel: isPrivateNetworking ? null : name } + imageRegistryCredentials: !empty(imageRegistryCredentials) ? imageRegistryCredentials : [] + subnets: isPrivateNetworking ? [{ subnetResourceId: subnetResourceId }] : [] + availabilityZone: availabilityZone } } @@ -110,10 +111,10 @@ resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2025-09-01' // Outputs // ============================================================================ @description('The name of the container group.') -output name string = containerGroup.name +output name string = containerGroup.outputs.name @description('The resource ID of the container group.') -output resourceId string = containerGroup.id +output resourceId string = containerGroup.outputs.resourceId @description('The IP address of the container group.') -output ipAddress string = containerGroup.properties.ipAddress.ip +output ipAddress string = containerGroup.outputs.?iPv4Address ?? '' diff --git a/infra/bicep/modules/compute/container-registry.bicep b/infra/bicep/modules/compute/container-registry.bicep index 9566d2182..9e1783be4 100644 --- a/infra/bicep/modules/compute/container-registry.bicep +++ b/infra/bicep/modules/compute/container-registry.bicep @@ -1,7 +1,6 @@ // ============================================================================ -// Module: Azure Container Registry -// Description: Creates an Azure Container Registry -// API: Microsoft.ContainerRegistry/registries@2025-04-01 +// Module: Azure Container Registry (AVM) +// AVM Module: avm/res/container-registry/registry:0.12.1 // ============================================================================ @description('Solution name used for naming convention.') @@ -20,45 +19,80 @@ param tags object = {} @allowed(['Basic', 'Standard', 'Premium']) param sku string = 'Premium' -@description('Enable admin user.') +@description('Enable admin user for the registry.') param adminUserEnabled bool = false @description('Public network access setting.') @allowed(['Enabled', 'Disabled']) param publicNetworkAccess string = 'Enabled' -@description('Export policy status.') +@description('Export policy status. Must be "enabled" when publicNetworkAccess is "Enabled".') param exportPolicyStatus string = 'enabled' +@description('Principal IDs to assign AcrPull role.') +param acrPullPrincipalIds array = [] + +@description('Enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs for private endpoint.') +param privateDnsZoneResourceIds array = [] + +@description('Default action for the network rule set. Use Allow when no private endpoint is in place; Deny for private-only.') +@allowed(['Allow', 'Deny']) +param networkRuleSetDefaultAction string = 'Allow' + +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + +// ============================================================================ +// Role Assignments // ============================================================================ -// Resource Deployment +var acrPullRoleId = '7f951dda-4ed3-4680-a7ca-43fe172d538d' + +var roleAssignments = [for principalId in acrPullPrincipalIds: { + principalId: principalId + roleDefinitionIdOrName: acrPullRoleId + principalType: 'ServicePrincipal' +}] + // ============================================================================ -resource containerRegistry 'Microsoft.ContainerRegistry/registries@2025-04-01' = { - name: name - location: location - tags: tags - sku: { - name: sku +// Private Endpoint Config +// ============================================================================ +var dnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'config${i}' + privateDnsZoneResourceId: zoneId +}] + +var privateEndpointConfig = enablePrivateNetworking && !empty(privateEndpointSubnetId) ? [ + { + subnetResourceId: privateEndpointSubnetId + privateDnsZoneGroup: !empty(privateDnsZoneResourceIds) ? { + privateDnsZoneGroupConfigs: dnsZoneConfigs + } : null } - properties: { - adminUserEnabled: adminUserEnabled +] : [] + +// ============================================================================ +// Container Registry (AVM) +// ============================================================================ +module containerRegistry 'br/public:avm/res/container-registry/registry:0.12.1' = { + name: take('avm.res.containerregistry.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + acrSku: sku + acrAdminUserEnabled: adminUserEnabled publicNetworkAccess: publicNetworkAccess - dataEndpointEnabled: false - networkRuleBypassOptions: 'AzureServices' - policies: { - exportPolicy: { - status: exportPolicyStatus - } - retentionPolicy: { - status: 'enabled' - days: 7 - } - trustPolicy: { - status: 'disabled' - type: 'Notary' - } - } - zoneRedundancy: 'Disabled' + exportPolicyStatus: exportPolicyStatus + roleAssignments: !empty(acrPullPrincipalIds) ? roleAssignments : [] + privateEndpoints: privateEndpointConfig + networkRuleSetDefaultAction: networkRuleSetDefaultAction } } @@ -66,10 +100,10 @@ resource containerRegistry 'Microsoft.ContainerRegistry/registries@2025-04-01' = // Outputs // ============================================================================ @description('The name of the container registry.') -output name string = containerRegistry.name +output name string = containerRegistry.outputs.name @description('The login server URL.') -output loginServer string = containerRegistry.properties.loginServer +output loginServer string = containerRegistry.outputs.loginServer @description('The resource ID of the container registry.') -output resourceId string = containerRegistry.id \ No newline at end of file +output resourceId string = containerRegistry.outputs.resourceId diff --git a/infra/bicep/modules/compute/function-app.bicep b/infra/bicep/modules/compute/function-app.bicep index 4756b20fe..f6d849458 100644 --- a/infra/bicep/modules/compute/function-app.bicep +++ b/infra/bicep/modules/compute/function-app.bicep @@ -1,7 +1,6 @@ // ============================================================================ -// Module: Azure Function App -// Description: Creates an Azure Function App on Linux -// API: Microsoft.Web/sites@2024-04-01 +// Module: Azure Function App (AVM) +// AVM Module: avm/res/web/site:0.23.1 // ============================================================================ @description('Name of the function app.') @@ -39,48 +38,44 @@ param runtimeStack string = 'python' @description('Runtime version.') param runtimeVersion string = '3.11' +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + // ============================================================================ // Variables // ============================================================================ -var identityConfig = empty(managedIdentities) ? null : { - type: contains(managedIdentities, 'userAssignedResourceIds') ? (contains(managedIdentities, 'systemAssigned') && managedIdentities.systemAssigned ? 'SystemAssigned,UserAssigned' : 'UserAssigned') : 'SystemAssigned' - userAssignedIdentities: contains(managedIdentities, 'userAssignedResourceIds') ? reduce(managedIdentities.userAssignedResourceIds, {}, (cur, id) => union(cur, { '${id}': {} })) : null -} - -var storageConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${listKeys(storageAccountResourceId, '2023-05-01').keys[0].value};EndpointSuffix=${environment().suffixes.storage}' -var linuxFxVersion = '${toUpper(runtimeStack)}|${runtimeVersion}' - -var baseSettings = [ - { name: 'AzureWebJobsStorage', value: storageConnectionString } - { name: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' } - { name: 'FUNCTIONS_WORKER_RUNTIME', value: toLower(runtimeStack) } - { name: 'WEBSITE_RUN_FROM_PACKAGE', value: '1' } -] - -var mergedSettings = concat(baseSettings, appSettings) - -var defaultSiteConfig = { - linuxFxVersion: linuxFxVersion - ftpsState: 'Disabled' - minTlsVersion: '1.2' - appSettings: mergedSettings +var baseAppSettings = { + AzureWebJobsStorage__accountName: storageAccountName + FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_WORKER_RUNTIME: runtimeStack } -var effectiveSiteConfig = union(defaultSiteConfig, siteConfig) +var customAppSettings = reduce(appSettings, {}, (cur, next) => union(cur, { '${next.name}': next.value })) +var mergedAppSettings = union(baseAppSettings, customAppSettings) // ============================================================================ -// Resource Deployment +// Function App (AVM) // ============================================================================ -resource functionApp 'Microsoft.Web/sites@2024-04-01' = { - name: name - location: location - tags: tags - kind: 'functionapp,linux' - identity: identityConfig - properties: { - serverFarmId: serverFarmResourceId - siteConfig: effectiveSiteConfig - httpsOnly: true +module functionApp 'br/public:avm/res/web/site:0.23.1' = { + name: take('avm.res.web.site.func.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + kind: 'functionapp,linux' + serverFarmResourceId: serverFarmResourceId + storageAccountRequired: false + managedIdentities: managedIdentities + configs: [ + { + name: 'appsettings' + properties: mergedAppSettings + } + ] + siteConfig: union({ + linuxFxVersion: '${toUpper(runtimeStack)}|${runtimeVersion}' + }, siteConfig) } } @@ -88,13 +83,13 @@ resource functionApp 'Microsoft.Web/sites@2024-04-01' = { // Outputs // ============================================================================ @description('The name of the function app.') -output name string = functionApp.name +output name string = functionApp.outputs.name @description('The resource ID of the function app.') -output resourceId string = functionApp.id +output resourceId string = functionApp.outputs.resourceId @description('The default hostname of the function app.') -output defaultHostName string = functionApp.properties.defaultHostName +output defaultHostName string = functionApp.outputs.defaultHostname @description('The principal ID of the system-assigned managed identity.') -output principalId string = contains(functionApp.identity, 'principalId') ? functionApp.identity.principalId : '' +output principalId string = functionApp.outputs.?systemAssignedMIPrincipalId ?? '' diff --git a/infra/bicep/modules/compute/kubernetes.bicep b/infra/bicep/modules/compute/kubernetes.bicep index 44e294404..a15a362ef 100644 --- a/infra/bicep/modules/compute/kubernetes.bicep +++ b/infra/bicep/modules/compute/kubernetes.bicep @@ -1,7 +1,7 @@ // ============================================================================ // Module: Azure Kubernetes Service (AKS) -// Description: Deploys Azure Kubernetes Service Managed Cluster -// API: Microsoft.ContainerService/managedClusters@2025-03-01 +// Description: AVM wrapper for Azure Kubernetes Service Managed Cluster +// AVM Module: avm/res/container-service/managed-cluster:0.13.1 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -22,14 +22,17 @@ param kubernetesVersion string = '1.34' @description('Agent pool configurations. Each entry requires name, vmSize, count, mode (System/User).') param agentPools array = [ { - name: 'systempool' + name: 'agentpool' vmSize: 'Standard_D4ds_v5' count: 2 minCount: 1 - maxCount: 3 + maxCount: 2 enableAutoScaling: true osType: 'Linux' mode: 'System' + type: 'VirtualMachineScaleSets' + scaleSetEvictionPolicy: 'Delete' + scaleSetPriority: 'Regular' } ] @@ -67,57 +70,78 @@ param autoUpgradeChannel string = 'stable' @description('Log Analytics workspace resource ID for monitoring.') param logAnalyticsWorkspaceResourceId string = '' +// --- WAF: Networking --- +@description('Public network access setting.') +@allowed(['Enabled', 'Disabled']) +param publicNetworkAccess string = 'Enabled' + +@description('Enable private cluster (API server not publicly accessible).') +param enablePrivateCluster bool = false + +@description('Subnet resource ID for the agent pool (for VNet integration).') +param agentPoolSubnetId string = '' + +@description('Enable Microsoft Defender for Containers.') +param enableDefender bool = false + +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] + +@description('Role assignments for the cluster.') +param roleAssignments array = [] + +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + // ============================================================================ // Variables // ============================================================================ var effectiveDnsPrefix = !empty(dnsPrefix) ? dnsPrefix : name +var enableMonitoring = !empty(logAnalyticsWorkspaceResourceId) + +var effectiveAgentPools = [for pool in agentPools: union(pool, !empty(agentPoolSubnetId) ? { vnetSubnetResourceId: agentPoolSubnetId } : {})] // ============================================================================ -// Resource Deployment +// AVM Module Deployment // ============================================================================ -resource aksCluster 'Microsoft.ContainerService/managedClusters@2025-03-01' = { - name: name - location: location - tags: tags - identity: { - type: 'SystemAssigned' - } - sku: { - name: 'Base' - tier: skuTier - } - properties: { +module aksCluster 'br/public:avm/res/container-service/managed-cluster:0.13.1' = { + name: take('avm.res.container-service.managed-cluster.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry kubernetesVersion: kubernetesVersion - dnsPrefix: effectiveDnsPrefix + primaryAgentPoolProfiles: effectiveAgentPools enableRBAC: enableRBAC disableLocalAccounts: disableLocalAccounts - agentPoolProfiles: [for pool in agentPools: { - name: pool.name - vmSize: pool.vmSize - count: pool.count - minCount: pool.?enableAutoScaling == true ? pool.?minCount : null - maxCount: pool.?enableAutoScaling == true ? pool.?maxCount : null - enableAutoScaling: pool.?enableAutoScaling ?? false - osType: pool.?osType ?? 'Linux' - mode: pool.mode - }] - networkProfile: { - networkPlugin: networkPlugin - networkPolicy: !empty(networkPolicy) ? networkPolicy : null - serviceCidr: serviceCidr - dnsServiceIP: dnsServiceIP + networkPlugin: networkPlugin + networkPolicy: networkPolicy + dnsPrefix: effectiveDnsPrefix + skuTier: skuTier + serviceCidr: serviceCidr + dnsServiceIP: dnsServiceIP + publicNetworkAccess: publicNetworkAccess + apiServerAccessProfile: { + enablePrivateCluster: enablePrivateCluster } autoUpgradeProfile: { upgradeChannel: autoUpgradeChannel + nodeOSUpgradeChannel: 'Unmanaged' } - addonProfiles: !empty(logAnalyticsWorkspaceResourceId) ? { - omsagent: { - enabled: true - config: { - logAnalyticsWorkspaceResourceID: logAnalyticsWorkspaceResourceId + managedIdentities: { systemAssigned: true } + omsAgentEnabled: enableMonitoring + monitoringWorkspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : null + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + securityProfile: enableDefender && enableMonitoring ? { + defender: { + logAnalyticsWorkspaceResourceId: logAnalyticsWorkspaceResourceId + securityMonitoring: { + enabled: true } } } : {} + roleAssignments: roleAssignments } } @@ -125,16 +149,16 @@ resource aksCluster 'Microsoft.ContainerService/managedClusters@2025-03-01' = { // Outputs // ============================================================================ @description('Name of the AKS cluster.') -output name string = aksCluster.name +output name string = aksCluster.outputs.name @description('Resource ID of the AKS cluster.') -output resourceId string = aksCluster.id +output resourceId string = aksCluster.outputs.resourceId @description('FQDN of the AKS cluster.') -output fqdn string = aksCluster.properties.fqdn +output fqdn string = aksCluster.outputs.?fqdn ?? '' @description('Object ID of the AKS kubelet system-assigned managed identity (used by pods at runtime via IMDS).') -output kubeletIdentityObjectId string = aksCluster.properties.?identityProfile.?kubeletidentity.?objectId ?? '' +output kubeletIdentityObjectId string = aksCluster.outputs.?kubeletIdentityObjectId ?? '' @description('Principal ID of the AKS control-plane system-assigned managed identity.') -output systemAssignedMIPrincipalId string = aksCluster.identity.?principalId ?? '' +output systemAssignedMIPrincipalId string = aksCluster.outputs.?systemAssignedMIPrincipalId ?? '' diff --git a/infra/bicep/modules/compute/maintenance-configuration.bicep b/infra/bicep/modules/compute/maintenance-configuration.bicep new file mode 100644 index 000000000..2683939d1 --- /dev/null +++ b/infra/bicep/modules/compute/maintenance-configuration.bicep @@ -0,0 +1,84 @@ +// ============================================================================ +// Module: Maintenance Configuration +// Description: AVM wrapper for Azure Maintenance Configuration +// AVM Module: avm/res/maintenance/maintenance-configuration +// WAF: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/virtual-machines +// ============================================================================ + +@description('Solution name suffix used to derive the resource name.') +param solutionName string + +@description('Name of the maintenance configuration.') +param name string = 'mc-${solutionName}' + +@description('Azure region for the resource.') +param location string + +@description('Tags to apply to the resource.') +param tags object = {} + +@description('Maintenance scope.') +param maintenanceScope string = 'InGuestPatch' + +@description('Visibility of the configuration.') +param visibility string = 'Custom' + +@description('Extension properties.') +param extensionProperties object = { + InGuestPatchMode: 'User' +} + +@description('Maintenance window configuration.') +param maintenanceWindow object = { + startDateTime: '2024-06-16 00:00' + duration: '03:55' + timeZone: 'W. Europe Standard Time' + recurEvery: '1Day' +} + +@description('Install patches configuration.') +param installPatches object = { + rebootSetting: 'IfRequired' + windowsParameters: { + classificationsToInclude: [ + 'Critical' + 'Security' + ] + } + linuxParameters: { + classificationsToInclude: [ + 'Critical' + 'Security' + ] + } +} + +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module maintenanceConfiguration 'br/public:avm/res/maintenance/maintenance-configuration:0.4.0' = { + name: take('avm.res.maintenance.maintenance-configuration.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + extensionProperties: extensionProperties + maintenanceScope: maintenanceScope + maintenanceWindow: maintenanceWindow + visibility: visibility + installPatches: installPatches + } +} + +// ============================================================================ +// Outputs +// ============================================================================ +@description('Resource ID of the maintenance configuration.') +output resourceId string = maintenanceConfiguration.outputs.resourceId + +@description('Name of the maintenance configuration.') +output name string = maintenanceConfiguration.outputs.name diff --git a/infra/bicep/modules/compute/proximity-placement-group.bicep b/infra/bicep/modules/compute/proximity-placement-group.bicep new file mode 100644 index 000000000..f1a3e2796 --- /dev/null +++ b/infra/bicep/modules/compute/proximity-placement-group.bicep @@ -0,0 +1,51 @@ +// ============================================================================ +// Module: Proximity Placement Group +// Description: AVM wrapper for Azure Proximity Placement Group +// AVM Module: avm/res/compute/proximity-placement-group +// WAF: https://learn.microsoft.com/en-us/azure/well-architected/service-guides/virtual-machines +// ============================================================================ + +@description('Solution name suffix used to derive the resource name.') +param solutionName string + +@description('Name of the proximity placement group.') +param name string = 'ppg-${solutionName}' + +@description('Azure region for the resource.') +param location string + +@description('Tags to apply to the resource.') +param tags object = {} + +@description('Availability zone for the proximity placement group.') +param availabilityZone int = 1 + +@description('VM sizes intent for the proximity placement group.') +param vmSizes array = [] + +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module proximityPlacementGroup 'br/public:avm/res/compute/proximity-placement-group:0.4.1' = { + name: take('avm.res.compute.proximity-placement-group.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + availabilityZone: availabilityZone + intent: !empty(vmSizes) ? { vmSizes: vmSizes } : null + } +} + +// ============================================================================ +// Outputs +// ============================================================================ +@description('Resource ID of the proximity placement group.') +output resourceId string = proximityPlacementGroup.outputs.resourceId + +@description('Name of the proximity placement group.') +output name string = proximityPlacementGroup.outputs.name diff --git a/infra/bicep/modules/compute/virtual-machine.bicep b/infra/bicep/modules/compute/virtual-machine.bicep index a0e8c1dac..8b9c2a7e0 100644 --- a/infra/bicep/modules/compute/virtual-machine.bicep +++ b/infra/bicep/modules/compute/virtual-machine.bicep @@ -1,149 +1,157 @@ // ============================================================================ // Module: Virtual Machine (Jumpbox) -// Description: Vanilla Bicep module for a Windows jumpbox VM used for private -// network administration via Azure Bastion. -// Resources: Microsoft.Compute/virtualMachines, Microsoft.Network/networkInterfaces -// NOTE: Not used by the lean main.bicep; retained for module parity with the -// AVM flavor across GSA. +// Description: AVM wrapper for Azure Virtual Machine with Entra ID authentication +// AVM Module: avm/res/compute/virtual-machine +// Ref: https://learn.microsoft.com/azure/bastion/bastion-entra-id-authentication // ============================================================================ -@description('Required. Name of the virtual machine (max 15 chars).') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Name of the virtual machine.') +param name string = 'vm-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true - -@description('Optional. VM size.') +@description('VM size.') param vmSize string = 'Standard_D2s_v5' -@description('Required. Admin username for the VM.') +@secure() +@description('Local admin username. Required by Azure at provisioning time but not used for login when Entra ID is enabled.') param adminUsername string @secure() -@description('Required. Admin password for the VM.') +@description('Local admin password. Required by Azure at provisioning time but not used for login when Entra ID is enabled.') param adminPassword string -@description('Required. Resource ID of the user assigned managed identity.') -param userAssignedIdentityResourceId string - -@description('Required. Resource ID of the subnet to attach the NIC to.') +@description('Resource ID of the subnet for the VM NIC.') param subnetResourceId string -@description('Optional. Availability zone (-1 to disable).') -param availabilityZone int = -1 +@description('OS type for the VM.') +param osType string = 'Windows' + +@description('Availability zone for the VM.') +param availabilityZone int = 1 + +@description('Image reference for the VM.') +param imageReference object = { + publisher: 'microsoft-dsvm' + offer: 'dsvm-win-2022' + sku: 'winserver-2022' + version: 'latest' +} + +@description('OS disk size in GB.') +param osDiskSizeGB int = 128 -@description('Optional. Enable monitoring agent and DCR association.') -param enableMonitoring bool = false +@description('Resource ID of the maintenance configuration.') +param maintenanceConfigurationResourceId string? -@description('Optional. Resource ID of the Data Collection Rule to associate.') -param dataCollectionRuleResourceId string = '' +@description('Resource ID of the proximity placement group.') +param proximityPlacementGroupResourceId string? -var vmName = take(name, 15) +@description('Monitoring agent extension configuration (data collection rule associations).') +param extensionMonitoringAgentConfig object? -resource nic 'Microsoft.Network/networkInterfaces@2024-05-01' = { - name: 'nic-${vmName}' - location: location - tags: tags - properties: { - enableAcceleratedNetworking: true - ipConfigurations: [ +@description('Diagnostic settings for the resource.') +param diagnosticSettings array? + +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + +@description('Deploying user principal ID. Used for default role assignment to grant the deploying user login access to the VM. This is required because with Entra ID authentication enabled, local accounts cannot be used to access the VM, including the local admin account created at provisioning.') +param deployingUserPrincipalId string + +@description('Deploying user principal type. Used for default role assignment to grant the deploying user login access to the VM. This is required because with Entra ID authentication enabled, local accounts cannot be used to access the VM, including the local admin account created at provisioning.') +param deployingUserPrincipalType string = 'User' + +@description('Role assignments to apply to the virtual machine.') +param roleAssignments array = [ + { + roleDefinitionIdOrName: '1c0163c0-47e6-4577-8991-ea5c82e286e4' // Virtual Machine Administrator Login + principalId: deployingUserPrincipalId + principalType: deployingUserPrincipalType + } +] + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module virtualMachine 'br/public:avm/res/compute/virtual-machine:0.22.0' = { + name: take('avm.res.compute.virtual-machine.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + computerName: take(name, 15) + osType: osType + vmSize: vmSize + adminUsername: adminUsername + adminPassword: adminPassword + managedIdentities: { systemAssigned: true } + patchMode: 'AutomaticByPlatform' + bypassPlatformSafetyChecksOnUserSchedule: true + maintenanceConfigurationResourceId: maintenanceConfigurationResourceId + enableAutomaticUpdates: true + encryptionAtHost: true + availabilityZone: availabilityZone + proximityPlacementGroupResourceId: proximityPlacementGroupResourceId + imageReference: imageReference + osDisk: { + name: 'osdisk-${name}' + caching: 'ReadWrite' + createOption: 'FromImage' + deleteOption: 'Delete' + diskSizeGB: osDiskSizeGB + managedDisk: { storageAccountType: 'Premium_LRS' } + } + nicConfigurations: [ { - name: 'ipconfig01' - properties: { - privateIPAllocationMethod: 'Dynamic' - subnet: { - id: subnetResourceId + name: 'nic-${name}' + tags: tags + deleteOption: 'Delete' + diagnosticSettings: diagnosticSettings + ipConfigurations: [ + { + name: '${name}-nic01-ipconfig01' + subnetResourceId: subnetResourceId + diagnosticSettings: diagnosticSettings } - } + ] } ] - } -} - -resource vm 'Microsoft.Compute/virtualMachines@2024-07-01' = { - name: vmName - location: location - tags: tags - zones: availabilityZone == -1 ? null : [string(availabilityZone)] - identity: { - type: 'UserAssigned' - userAssignedIdentities: { - '${userAssignedIdentityResourceId}': {} + roleAssignments: roleAssignments + extensionAadJoinConfig: { + enabled: true + tags: tags + typeHandlerVersion: '2.0' + settings: { mdmId: '' } } - } - properties: { - hardwareProfile: { - vmSize: vmSize - } - osProfile: { - computerName: vmName - adminUsername: adminUsername - adminPassword: adminPassword - } - storageProfile: { - imageReference: { - publisher: 'microsoft-dsvm' - offer: 'dsvm-win-2022' - sku: 'winserver-2022' - version: 'latest' + extensionAntiMalwareConfig: { + enabled: true + settings: { + AntimalwareEnabled: 'true' + Exclusions: {} + RealtimeProtectionEnabled: 'true' + ScheduledScanSettings: { day: '7', isEnabled: 'true', scanType: 'Quick', time: '120' } } - osDisk: { - createOption: 'FromImage' - caching: 'ReadWrite' - diskSizeGB: 128 - managedDisk: { - storageAccountType: 'Premium_LRS' - } - } - } - networkProfile: { - networkInterfaces: [ - { - id: nic.id - } - ] - } - securityProfile: { - encryptionAtHost: false // Some Azure subscriptions do not support encryption at host + tags: tags } + extensionMonitoringAgentConfig: extensionMonitoringAgentConfig + extensionNetworkWatcherAgentConfig: { enabled: true, tags: tags } } } -resource monitoringAgent 'Microsoft.Compute/virtualMachines/extensions@2024-07-01' = if (enableMonitoring) { - parent: vm - name: 'AzureMonitorWindowsAgent' - location: location - tags: tags - properties: { - publisher: 'Microsoft.Azure.Monitor' - type: 'AzureMonitorWindowsAgent' - typeHandlerVersion: '1.0' - autoUpgradeMinorVersion: true - enableAutomaticUpgrade: true - } -} - -resource dcrAssociation 'Microsoft.Insights/dataCollectionRuleAssociations@2023-03-11' = if (enableMonitoring && !empty(dataCollectionRuleResourceId)) { - name: 'dcra-${vmName}' - scope: vm - properties: { - description: 'Associates the Windows security event DCR with the jumpbox VM.' - dataCollectionRuleId: dataCollectionRuleResourceId - } - dependsOn: [ - monitoringAgent - ] -} - +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the virtual machine.') -output resourceId string = vm.id +output resourceId string = virtualMachine.outputs.resourceId @description('Name of the virtual machine.') -output name string = vm.name +output name string = virtualMachine.outputs.name diff --git a/infra/bicep/modules/data/app-configuration.bicep b/infra/bicep/modules/data/app-configuration.bicep index 6df7aea16..ee3fb3187 100644 --- a/infra/bicep/modules/data/app-configuration.bicep +++ b/infra/bicep/modules/data/app-configuration.bicep @@ -1,7 +1,5 @@ // ============================================================================ -// Module: Azure App Configuration -// Description: Creates an Azure App Configuration store -// API: Microsoft.AppConfiguration/configurationStores@2023-03-01 +// Module: Azure App Configuration (AVM) // ============================================================================ @description('Solution name used for naming convention.') @@ -10,12 +8,15 @@ param solutionName string @description('Name of the App Configuration store.') param name string = 'appcs-${solutionName}' -@description('Azure region for the resource.') +@description('Azure region for deployment.') param location string -@description('Tags to apply to the resource.') +@description('Resource tags.') param tags object = {} +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + @description('SKU for the configuration store.') @allowed(['Free', 'Standard']) param sku string = 'Standard' @@ -23,41 +24,75 @@ param sku string = 'Standard' @description('Disable local (key-based) authentication.') param disableLocalAuth bool = true +@description('Enable purge protection.') +param enablePurgeProtection bool = false + +@description('Soft delete retention in days.') +param softDeleteRetentionInDays int = 7 + +@description('Managed identity configuration.') +param managedIdentities object = {} + +@description('Role assignments.') +param roleAssignments array = [] + @description('Key-value pairs to store in the configuration.') param keyValues array = [] +@description('Enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs.') +param privateDnsZoneResourceIds array = [] + // ============================================================================ -// Resource Deployment +// App Configuration (AVM) // ============================================================================ -resource appConfiguration 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = { - name: name - location: location - tags: tags - sku: { - name: sku + +var dnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'config${i}' + privateDnsZoneResourceId: zoneId +}] + +var privateEndpointConfig = enablePrivateNetworking && !empty(privateEndpointSubnetId) ? [ + { + subnetResourceId: privateEndpointSubnetId + privateDnsZoneGroup: !empty(privateDnsZoneResourceIds) ? { + privateDnsZoneGroupConfigs: dnsZoneConfigs + } : null } - properties: { +] : [] + +module configStore 'br/public:avm/res/app-configuration/configuration-store:0.9.2' = { + name: take('avm.res.appconfiguration.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + sku: sku disableLocalAuth: disableLocalAuth - publicNetworkAccess: 'Enabled' + enablePurgeProtection: enablePurgeProtection + softDeleteRetentionInDays: softDeleteRetentionInDays + managedIdentities: !empty(managedIdentities) ? managedIdentities : {} + roleAssignments: !empty(roleAssignments) ? roleAssignments : [] + keyValues: !empty(keyValues) ? keyValues : [] + privateEndpoints: privateEndpointConfig } } -resource configurationKeyValues 'Microsoft.AppConfiguration/configurationStores/keyValues@2023-03-01' = [for keyValue in keyValues: { - name: keyValue.name - parent: appConfiguration - properties: { - value: keyValue.value - } -}] - // ============================================================================ // Outputs // ============================================================================ -@description('The name of the App Configuration store.') -output name string = appConfiguration.name -@description('The endpoint of the App Configuration store.') -output endpoint string = appConfiguration.properties.endpoint +@description('The name of the configuration store.') +output name string = configStore.outputs.name + +@description('The endpoint of the configuration store.') +output endpoint string = configStore.outputs.endpoint -@description('The resource ID of the App Configuration store.') -output resourceId string = appConfiguration.id +@description('The resource ID of the configuration store.') +output resourceId string = configStore.outputs.resourceId diff --git a/infra/bicep/modules/data/cosmos-db-mongo.bicep b/infra/bicep/modules/data/cosmos-db-mongo.bicep index 620c2f632..36baef6c6 100644 --- a/infra/bicep/modules/data/cosmos-db-mongo.bicep +++ b/infra/bicep/modules/data/cosmos-db-mongo.bicep @@ -1,7 +1,8 @@ // ============================================================================ // Module: Cosmos DB (MongoDB) -// Description: Creates an Azure Cosmos DB account with MongoDB API -// API: Microsoft.DocumentDB/databaseAccounts@2025-10-15 +// Description: AVM wrapper for Azure Cosmos DB with MongoDB API +// AVM Module: avm/res/document-db/database-account:0.19.0 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/cosmos-db // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -26,83 +27,105 @@ param collections array = [] @allowed(['4.2', '5.0', '6.0', '7.0']) param serverVersion string = '7.0' +@description('Enable analytical storage (Synapse Link).') +param enableAnalyticalStorage bool = false + @description('Default consistency level.') @allowed(['Eventual', 'ConsistentPrefix', 'Session', 'BoundedStaleness', 'Strong']) param consistencyLevel string = 'Session' -@description('Enable analytical storage (Synapse Link).') -param enableAnalyticalStorage bool = false +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +// --- WAF: Monitoring --- +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] +// --- WAF: Private Networking --- +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' + +@description('Whether to enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for the private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs for Cosmos DB (MongoDB).') +param privateDnsZoneResourceIds array = [] + +var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'dns-zone-${i}' + privateDnsZoneResourceId: zoneId +}] + +// --- WAF: Redundancy --- @description('Enable zone redundancy.') param zoneRedundant bool = false @description('Enable automatic failover.') param enableAutomaticFailover bool = false -@description('HA paired region for multi-region failover.') +@description('Optional. HA paired region for multi-region failover when redundancy is enabled.') param haLocation string = '' -@description('Public network access setting.') -param publicNetworkAccess string = 'Enabled' - // ============================================================================ -// Resource Deployment +// AVM Module Deployment // ============================================================================ -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2025-10-15' = { - name: name - location: location - tags: tags - kind: 'MongoDB' - properties: { - consistencyPolicy: { defaultConsistencyLevel: consistencyLevel } - locations: zoneRedundant && !empty(haLocation) +module cosmosAccount 'br/public:avm/res/document-db/database-account:0.19.0' = { + name: take('avm.res.document-db.database-account.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + capabilitiesToAdd: ['EnableMongo'] + serverVersion: serverVersion + enableAnalyticalStorage: enableAnalyticalStorage + defaultConsistencyLevel: consistencyLevel + mongodbDatabases: [ + { + name: databaseName + collections: collections + } + ] + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + networkRestrictions: { + networkAclBypass: 'None' + publicNetworkAccess: publicNetworkAccess + } + privateEndpoints: enablePrivateNetworking ? [ + { + name: 'pep-${name}' + customNetworkInterfaceName: 'nic-${name}' + subnetResourceId: privateEndpointSubnetId + service: 'MongoDB' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: privateDnsZoneConfigs + } + } + ] : [] + zoneRedundant: zoneRedundant + enableAutomaticFailover: enableAutomaticFailover + failoverLocations: zoneRedundant && !empty(haLocation) ? [ - { locationName: location, failoverPriority: 0, isZoneRedundant: true } - { locationName: haLocation, failoverPriority: 1, isZoneRedundant: true } + { failoverPriority: 0, isZoneRedundant: true, locationName: location } + { failoverPriority: 1, isZoneRedundant: true, locationName: haLocation } ] : [ - { locationName: location, failoverPriority: 0, isZoneRedundant: zoneRedundant } + { locationName: location, failoverPriority: 0, isZoneRedundant: false } ] - databaseAccountOfferType: 'Standard' - enableAutomaticFailover: enableAutomaticFailover - enableMultipleWriteLocations: false - apiProperties: { serverVersion: serverVersion } - enableAnalyticalStorage: enableAnalyticalStorage - capabilities: [{ name: 'EnableMongo' }] - publicNetworkAccess: publicNetworkAccess } } -resource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-10-15' = { - parent: cosmos - name: databaseName - properties: { - resource: { id: databaseName } - } -} - -resource mongoCollections 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-10-15' = [for collection in collections: { - parent: database - name: collection.name - properties: { - resource: { - id: collection.name - shardKey: collection.?shardKey ?? {} - indexes: collection.?indexes ?? [ - { key: { keys: ['_id'] } } - ] - } - } -}] - // ============================================================================ // Outputs // ============================================================================ @description('Resource ID of the Cosmos DB account.') -output resourceId string = cosmos.id +output resourceId string = cosmosAccount.outputs.resourceId @description('Name of the Cosmos DB account.') -output name string = cosmos.name +output name string = cosmosAccount.outputs.name @description('MongoDB connection string (without credentials — use Key Vault for secrets).') output connectionString string = 'mongodb+srv://${name}.mongo.cosmos.azure.com:443/?ssl=true&retrywrites=false&maxIdleTimeMS=120000' diff --git a/infra/bicep/modules/data/cosmos-db-nosql.bicep b/infra/bicep/modules/data/cosmos-db-nosql.bicep index 9c758c2ec..49c39f760 100644 --- a/infra/bicep/modules/data/cosmos-db-nosql.bicep +++ b/infra/bicep/modules/data/cosmos-db-nosql.bicep @@ -1,7 +1,8 @@ // ============================================================================ // Module: Cosmos DB -// Description: Creates an Azure Cosmos DB (NoSQL) account with database/container -// API: Microsoft.DocumentDB/databaseAccounts@2025-10-15 +// Description: AVM wrapper for Azure Cosmos DB (NoSQL) with WAF alignment +// AVM Module: avm/res/document-db/database-account:0.19.0 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/cosmos-db // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -27,58 +28,113 @@ param containers array = [ } ] +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +// --- WAF: Monitoring --- +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] + +// --- WAF: Private Networking --- +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' + +@description('Whether to enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for the private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs for Cosmos DB.') +param privateDnsZoneResourceIds array = [] + +var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'dns-zone-${i}' + privateDnsZoneResourceId: zoneId +}] + +// --- WAF: Redundancy --- +@description('Enable zone redundancy.') +param zoneRedundant bool = false + +@description('Enable automatic failover.') +param enableAutomaticFailover bool = false + +@description('Optional. HA paired region for multi-region failover when redundancy is enabled.') +param haLocation string = '' + // ============================================================================ -// Resource Deployment +// AVM Module Deployment // ============================================================================ -resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2025-10-15' = { - name: name - location: location - tags: tags - kind: 'GlobalDocumentDB' - properties: { - consistencyPolicy: { defaultConsistencyLevel: 'Session' } - locations: [ +module cosmosAccount 'br/public:avm/res/document-db/database-account:0.19.0' = { + name: take('avm.res.document-db.database-account.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + capabilitiesToAdd: zoneRedundant ? [] : ['EnableServerless'] + sqlDatabases: [ { - locationName: location - failoverPriority: 0 - isZoneRedundant: false + name: databaseName + containers: [for container in containers: { + name: container.name + paths: [container.partitionKeyPath] + kind: 'Hash' + version: 2 + }] } ] - databaseAccountOfferType: 'Standard' - enableAutomaticFailover: false - enableMultipleWriteLocations: false - disableLocalAuth: true - capabilities: [ { name: 'EnableServerless' } ] - } -} - -resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2025-10-15' = { - parent: cosmos - name: databaseName - properties: { - resource: { id: databaseName } - } - - resource list 'containers' = [for container in containers: { - name: container.name - properties: { - resource: { - id: container.name - partitionKey: { paths: [ container.partitionKeyPath ] } - } - options: {} + sqlRoleAssignments: [] + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + networkRestrictions: { + networkAclBypass: 'None' + publicNetworkAccess: publicNetworkAccess } - }] + privateEndpoints: enablePrivateNetworking ? [ + { + name: 'pep-${name}' + customNetworkInterfaceName: 'nic-${name}' + subnetResourceId: privateEndpointSubnetId + service: 'Sql' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: privateDnsZoneConfigs + } + } + ] : [] + zoneRedundant: zoneRedundant + enableAutomaticFailover: enableAutomaticFailover + failoverLocations: zoneRedundant + ? [ + { + failoverPriority: 0 + isZoneRedundant: true + locationName: location + } + { + failoverPriority: 1 + isZoneRedundant: true + locationName: haLocation + } + ] + : [ + { + locationName: location + failoverPriority: 0 + isZoneRedundant: false + } + ] + } } // ============================================================================ // Outputs // ============================================================================ @description('Resource ID of the Cosmos DB account.') -output resourceId string = cosmos.id +output resourceId string = cosmosAccount.outputs.resourceId @description('Name of the Cosmos DB account.') -output name string = cosmos.name +output name string = cosmosAccount.outputs.name @description('Endpoint of the Cosmos DB account.') output endpoint string = 'https://${name}.documents.azure.com:443/' diff --git a/infra/bicep/modules/data/event-grid.bicep b/infra/bicep/modules/data/event-grid.bicep index 724481e12..16c675167 100644 --- a/infra/bicep/modules/data/event-grid.bicep +++ b/infra/bicep/modules/data/event-grid.bicep @@ -1,7 +1,7 @@ // ============================================================================ // Module: Azure Event Grid System Topic -// Description: Deploys Azure Event Grid System Topic -// API: Microsoft.EventGrid/systemTopics@2025-07-15-preview +// Description: AVM wrapper for Azure Event Grid System Topic +// AVM Module: avm/res/event-grid/system-topic:0.6.5 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -16,6 +16,9 @@ param location string @description('Tags to apply to the resource.') param tags object = {} +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + @description('Resource ID of the source that publishes events (e.g., Storage Account resource ID).') param source string @@ -25,57 +28,38 @@ param topicType string @description('Event subscriptions to create on the system topic.') param eventSubscriptions array = [] -@description('Managed identities configuration. E.g., { systemAssigned: false, userAssignedResourceIds: [] }.') +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] + +@description('Managed identities configuration.') param managedIdentities object = {} // ============================================================================ -// Resource +// AVM Module Deployment // ============================================================================ -resource eventGridSystemTopic 'Microsoft.EventGrid/systemTopics@2025-07-15-preview' = { - name: name - location: location - tags: tags - identity: !empty(managedIdentities) ? { - type: (managedIdentities.?systemAssigned ?? false) && !empty(managedIdentities.?userAssignedResourceIds ?? []) - ? 'SystemAssigned,UserAssigned' - : (managedIdentities.?systemAssigned ?? false) ? 'SystemAssigned' : 'UserAssigned' - userAssignedIdentities: !empty(managedIdentities.?userAssignedResourceIds ?? []) - ? reduce(managedIdentities.userAssignedResourceIds, {}, (cur, next) => union(cur, { '${next}': {} })) - : null - } : null - properties: { +module eventGridSystemTopic 'br/public:avm/res/event-grid/system-topic:0.6.5' = { + name: take('avm.res.event-grid.system-topic.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry source: source topicType: topicType + eventSubscriptions: eventSubscriptions + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + managedIdentities: !empty(managedIdentities) ? managedIdentities : null } } -// ============================================================================ -// Event Subscriptions -// ============================================================================ -resource systemTopicSubscriptions 'Microsoft.EventGrid/systemTopics/eventSubscriptions@2025-07-15-preview' = [ - for sub in eventSubscriptions: { - name: sub.name - parent: eventGridSystemTopic - properties: { - destination: sub.destination - filter: sub.?filter ?? {} - eventDeliverySchema: sub.?eventDeliverySchema ?? 'EventGridSchema' - retryPolicy: sub.?retryPolicy ?? { - maxDeliveryAttempts: 30 - eventTimeToLiveInMinutes: 1440 - } - } - } -] - // ============================================================================ // Outputs // ============================================================================ @description('Name of the Event Grid System Topic.') -output name string = eventGridSystemTopic.name +output name string = eventGridSystemTopic.outputs.name @description('Resource ID of the Event Grid System Topic.') -output resourceId string = eventGridSystemTopic.id +output resourceId string = eventGridSystemTopic.outputs.resourceId @description('System-assigned principal ID (if enabled).') -output systemAssignedMIPrincipalId string = (managedIdentities.?systemAssigned ?? false) ? eventGridSystemTopic.identity.principalId : '' +output systemAssignedMIPrincipalId string = eventGridSystemTopic.outputs.?systemAssignedMIPrincipalId ?? '' diff --git a/infra/bicep/modules/data/event-hub.bicep b/infra/bicep/modules/data/event-hub.bicep index 1dfb8a89c..3e8efd607 100644 --- a/infra/bicep/modules/data/event-hub.bicep +++ b/infra/bicep/modules/data/event-hub.bicep @@ -1,7 +1,5 @@ // ============================================================================ -// Module: Azure Event Hub Namespace -// Description: Creates an Azure Event Hub Namespace with event hubs -// API: Microsoft.EventHub/namespaces@2024-01-01 +// Module: Azure Event Hub Namespace (AVM) // ============================================================================ @description('Solution name used for naming convention.') @@ -10,53 +8,85 @@ param solutionName string @description('Name of the Event Hub namespace.') param name string = 'evhns-${solutionName}' -@description('Azure region for the resource.') +@description('Azure region for deployment.') param location string -@description('Tags to apply to the resource.') +@description('Resource tags.') param tags object = {} -@description('The SKU tier for the Event Hub namespace.') -param sku string = 'Standard' +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true -@description('The throughput unit or processing unit capacity.') -param capacity int = 1 +@description('SKU configuration for the namespace.') +param sku object = { + name: 'Standard' + capacity: 1 +} @description('Event hubs to create within the namespace.') param eventhubs array = [] +@description('Managed identity configuration.') +param managedIdentities object = {} + +@description('Role assignments.') +param roleAssignments array = [] + +@description('Enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs.') +param privateDnsZoneResourceIds array = [] + // ============================================================================ -// Resource Deployment +// Event Hub Namespace (AVM) // ============================================================================ -resource eventHubNamespace 'Microsoft.EventHub/namespaces@2024-01-01' = { - name: name - location: location - tags: tags - sku: { - name: sku - tier: sku - capacity: capacity - } - properties: { - minimumTlsVersion: '1.2' - publicNetworkAccess: 'Enabled' + +var eventHubItems = [for eh in eventhubs: { + name: eh.name + messageRetentionInDays: contains(eh, 'messageRetentionInDays') ? eh.messageRetentionInDays : 1 + partitionCount: contains(eh, 'partitionCount') ? eh.partitionCount : 2 +}] + +var dnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'config${i}' + privateDnsZoneResourceId: zoneId +}] + +var privateEndpointConfig = enablePrivateNetworking && !empty(privateEndpointSubnetId) ? [ + { + subnetResourceId: privateEndpointSubnetId + privateDnsZoneGroup: !empty(privateDnsZoneResourceIds) ? { + privateDnsZoneGroupConfigs: dnsZoneConfigs + } : null } -} +] : [] -resource eventHubResources 'Microsoft.EventHub/namespaces/eventhubs@2024-01-01' = [for eventhub in eventhubs: { - name: eventhub.name - parent: eventHubNamespace - properties: { - messageRetentionInDays: eventhub.?messageRetentionInDays ?? 1 - partitionCount: eventhub.?partitionCount ?? 2 +module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.14.1' = { + name: take('avm.res.eventhub.namespace.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + skuName: sku.name + skuCapacity: sku.capacity + eventhubs: eventHubItems + managedIdentities: !empty(managedIdentities) ? managedIdentities : {} + roleAssignments: !empty(roleAssignments) ? roleAssignments : [] + privateEndpoints: privateEndpointConfig } -}] +} // ============================================================================ // Outputs // ============================================================================ + @description('The name of the Event Hub namespace.') -output name string = eventHubNamespace.name +output name string = eventHubNamespace.outputs.name @description('The resource ID of the Event Hub namespace.') -output resourceId string = eventHubNamespace.id +output resourceId string = eventHubNamespace.outputs.resourceId diff --git a/infra/bicep/modules/data/postgresql-flexible-server.bicep b/infra/bicep/modules/data/postgresql-flexible-server.bicep index 3c6cb0eb2..42cbc4f44 100644 --- a/infra/bicep/modules/data/postgresql-flexible-server.bicep +++ b/infra/bicep/modules/data/postgresql-flexible-server.bicep @@ -1,133 +1,148 @@ +// ============================================================================ +// Module: PostgreSQL Flexible Server +// Description: AVM wrapper for Azure Database for PostgreSQL Flexible Server +// AVM Module: avm/res/db-for-postgre-sql/flexible-server:0.15.4 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/postgresql +// ============================================================================ + @description('Solution name suffix used to derive the resource name.') param solutionName string @description('Name of the PostgreSQL Flexible Server.') param name string = 'psql-${solutionName}' -@description('The Azure region where the PostgreSQL Flexible Server will be deployed.') +@description('Azure region for the resource.') param location string @description('Tags to apply to the resource.') param tags object = {} +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + @description('Azure AD administrators for the server. Each entry requires objectId, principalName, and principalType (User, Group, or ServicePrincipal).') param administrators array @description('The PostgreSQL version to deploy.') param version string = '16' -@description('The SKU name for the PostgreSQL Flexible Server.') +@description('SKU name for the PostgreSQL Flexible Server.') param skuName string = 'Standard_B1ms' -@description('The SKU tier for the PostgreSQL Flexible Server.') +@description('SKU tier for the PostgreSQL Flexible Server.') @allowed(['Burstable', 'GeneralPurpose', 'MemoryOptimized']) param skuTier string = 'Burstable' -@description('The storage size in GB.') +@description('Storage size in GB.') param storageSizeGB int = 32 +@description('Availability zone for the server.') +param availabilityZone int = 1 + @description('Optional databases to create on the server. Each entry should have a name, and optionally charset and collation.') param databases array = [] @description('Optional server configurations (e.g., extensions). Each entry should have a name, value, and source.') param configurations array = [] -resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2026-01-01-preview' = { - name: name - location: location - tags: tags - sku: { - name: skuName +// --- WAF: Monitoring --- +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] + +// --- WAF: Private Networking --- +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' + +@description('Whether to enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for the private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs for PostgreSQL.') +param privateDnsZoneResourceIds array = [] + +var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'dns-zone-${i}' + privateDnsZoneResourceId: zoneId +}] + +// --- WAF: Redundancy --- +@description('High availability mode.') +@allowed(['Disabled', 'SameZone', 'ZoneRedundant']) +param highAvailability string = 'Disabled' + +@description('Standby availability zone for high availability.') +param highAvailabilityZone int = -1 + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module postgresServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.15.4' = { + name: take('avm.res.postgre-sql.flexible-server.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + skuName: skuName tier: skuTier - } - properties: { + storageSizeGB: storageSizeGB version: version - storage: { - storageSizeGB: storageSizeGB - } - authConfig: { - activeDirectoryAuth: 'Enabled' - passwordAuth: 'Disabled' - } - highAvailability: { - mode: 'Disabled' - } - network: { - publicNetworkAccess: 'Enabled' - } - } -} - -resource firewallAllowAzureIPs 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2026-01-01-preview' = { - name: 'AllowAllAzureServicesAndResourcesWithinAzureIps' - parent: postgresServer - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } -} - -resource firewallAllowAll 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2026-01-01-preview' = { - name: 'AllowAll' - parent: postgresServer - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } -} - -// AAD admins must wait for firewall rules — server needs to be fully accessible first -@batchSize(1) -resource postgresAdmins 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2026-01-01-preview' = [ - for admin in administrators: { - parent: postgresServer - name: admin.objectId - dependsOn: [ - firewallAllowAzureIPs - firewallAllowAll - ] - properties: { + availabilityZone: availabilityZone + highAvailability: highAvailability + highAvailabilityZone: highAvailabilityZone + publicNetworkAccess: publicNetworkAccess + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + administrators: [for admin in administrators: { + objectId: admin.objectId principalName: admin.principalName principalType: admin.principalType - tenantId: subscription().tenantId - } - } -] - -resource serverDatabases 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2026-01-01-preview' = [ - for db in databases: { - name: db.name - parent: postgresServer - properties: { + }] + firewallRules: publicNetworkAccess == 'Enabled' ? [ + { + name: 'AllowAllAzureServicesAndResourcesWithinAzureIps' + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } + { + name: 'AllowAll' + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } + ] : [] + privateEndpoints: enablePrivateNetworking ? [ + { + name: 'pep-${name}' + customNetworkInterfaceName: 'nic-${name}' + subnetResourceId: privateEndpointSubnetId + service: 'postgresqlServer' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: privateDnsZoneConfigs + } + } + ] : [] + databases: [for db in databases: { + name: db.name charset: db.?charset ?? 'UTF8' collation: db.?collation ?? 'en_US.utf8' - } - dependsOn: [ - postgresAdmins - ] - } -] - -@batchSize(1) -resource serverConfigurations 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2026-01-01-preview' = [ - for config in configurations: { - name: config.name - parent: postgresServer - properties: { + }] + configurations: [for config in configurations: { + name: config.name value: config.value source: config.source - } - dependsOn: [ - postgresAdmins - ] + }] } -] +} -@description('The fully qualified domain name of the PostgreSQL Flexible Server.') -output serverFqdn string = postgresServer.properties.fullyQualifiedDomainName +// ============================================================================ +// Outputs +// ============================================================================ +@description('Fully qualified domain name of the PostgreSQL Flexible Server.') +output serverFqdn string = postgresServer.outputs.?fqdn ?? '${name}.postgres.database.azure.com' -@description('The name of the PostgreSQL Flexible Server.') -output name string = postgresServer.name +@description('Name of the PostgreSQL Flexible Server.') +output name string = postgresServer.outputs.name -@description('The resource ID of the PostgreSQL Flexible Server.') -output resourceId string = postgresServer.id +@description('Resource ID of the PostgreSQL Flexible Server.') +output resourceId string = postgresServer.outputs.resourceId diff --git a/infra/bicep/modules/data/sql-database.bicep b/infra/bicep/modules/data/sql-database.bicep index 2e647c1e7..4afa61b08 100644 --- a/infra/bicep/modules/data/sql-database.bicep +++ b/infra/bicep/modules/data/sql-database.bicep @@ -1,7 +1,8 @@ // ============================================================================ // Module: SQL Database -// Description: Creates an Azure SQL Server and Database -// API: Microsoft.Sql/servers@2025-01-01 +// Description: AVM wrapper for Azure SQL Server and Database +// AVM Module: avm/res/sql/server:0.21.1 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-sql-database // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -19,6 +20,9 @@ param location string @description('Tags to apply to the resource.') param tags object = {} +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + @description('Principal ID of the deployer for admin access.') param deployerPrincipalId string @@ -40,62 +44,83 @@ param autoPauseDelay int = 60 @description('Minimum capacity (vCores).') param minCapacity int = 1 +// --- WAF: Private Networking --- +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' + +@description('Whether to enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for the private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs for SQL Server.') +param privateDnsZoneResourceIds array = [] + +var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'dns-zone-${i}' + privateDnsZoneResourceId: zoneId +}] + // ============================================================================ -// Resource Deployment +// AVM Module Deployment // ============================================================================ -resource sqlServer 'Microsoft.Sql/servers@2025-01-01' = { - name: name - location: location - tags: tags - properties: { - publicNetworkAccess: 'Enabled' - version: '12.0' - restrictOutboundNetworkAccess: 'Disabled' +module sqlServer 'br/public:avm/res/sql/server:0.21.1' = { + name: take('avm.res.sql.server.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry minimalTlsVersion: '1.2' + publicNetworkAccess: publicNetworkAccess + restrictOutboundNetworkAccess: 'Disabled' administrators: { + azureADOnlyAuthentication: true login: deployerPrincipalId + principalType: 'User' sid: deployerPrincipalId tenantId: subscription().tenantId - administratorType: 'ActiveDirectory' - azureADOnlyAuthentication: true } - } -} - -resource firewallRule 'Microsoft.Sql/servers/firewallRules@2025-01-01' = { - name: 'AllowSpecificRange' - parent: sqlServer - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } -} - -resource AllowAllWindowsAzureIps 'Microsoft.Sql/servers/firewallRules@2025-01-01' = { - name: 'AllowAllWindowsAzureIps' - parent: sqlServer - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } -} - -resource sqlDB 'Microsoft.Sql/servers/databases@2025-01-01' = { - parent: sqlServer - name: databaseName - location: location - sku: { - name: skuName - tier: skuTier - family: skuFamily - capacity: skuCapacity - } - properties: { - collation: 'SQL_Latin1_General_CP1_CI_AS' - autoPauseDelay: autoPauseDelay - minCapacity: minCapacity - readScale: 'Disabled' - zoneRedundant: false + databases: [ + { + name: databaseName + availabilityZone: -1 + collation: 'SQL_Latin1_General_CP1_CI_AS' + autoPauseDelay: autoPauseDelay + minCapacity: '${minCapacity}' + zoneRedundant: false + sku: { + name: skuName + tier: skuTier + family: skuFamily + capacity: skuCapacity + } + } + ] + firewallRules: publicNetworkAccess == 'Enabled' ? [ + { + name: 'AllowSpecificRange' + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } + { + name: 'AllowAllWindowsAzureIps' + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } + ] : [] + privateEndpoints: enablePrivateNetworking ? [ + { + name: 'pep-${name}' + customNetworkInterfaceName: 'nic-${name}' + subnetResourceId: privateEndpointSubnetId + service: 'sqlServer' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: privateDnsZoneConfigs + } + } + ] : [] } } @@ -109,7 +134,7 @@ output serverFqdn string = '${name}.database.windows.net' output databaseName string = databaseName @description('Resource ID of the SQL Server.') -output serverResourceId string = sqlServer.id +output serverResourceId string = sqlServer.outputs.resourceId @description('Name of the SQL Server.') -output name string = sqlServer.name +output name string = sqlServer.outputs.name diff --git a/infra/bicep/modules/data/storage-account.bicep b/infra/bicep/modules/data/storage-account.bicep index dc3e93f18..329a532be 100644 --- a/infra/bicep/modules/data/storage-account.bicep +++ b/infra/bicep/modules/data/storage-account.bicep @@ -1,7 +1,8 @@ // ============================================================================ // Module: Storage Account -// Description: Creates an Azure Storage Account with blob container -// API: Microsoft.Storage/storageAccounts@2025-08-01 +// Description: AVM wrapper for Azure Storage Account with WAF alignment +// AVM Module: avm/res/storage/storage-account:0.32.0 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/storage-accounts // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -35,6 +36,9 @@ param allowSharedKeyAccess bool = true @description('Enable hierarchical namespace (Data Lake Storage Gen2).') param enableHierarchicalNamespace bool = false +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + @description('Blob containers to create.') param containers array = [ { @@ -43,63 +47,93 @@ param containers array = [ } ] +// --- WAF: Monitoring --- +@description('Diagnostic settings for monitoring.') +param diagnosticSettings array = [] + +// --- WAF: Private Networking --- +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' + +@description('Network ACLs for the storage account.') +param networkAcls object = { + defaultAction: 'Allow' + bypass: 'AzureServices' +} + +@description('Whether to enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for the private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs for Storage (blob).') +param privateDnsZoneResourceIds array = [] + +var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'dns-zone-${i}' + privateDnsZoneResourceId: zoneId +}] + +// --- Role Assignments --- +@description('Optional. Array of role assignments to create on the Storage Account.') +param roleAssignments array = [] + // ============================================================================ -// Resource Deployment +// AVM Module Deployment // ============================================================================ -resource storageAccount 'Microsoft.Storage/storageAccounts@2025-08-01' = { - name: name - location: location - tags: tags - kind: kind - sku: { - name: skuName - } - properties: { +module storage 'br/public:avm/res/storage/storage-account:0.32.0' = { + name: take('avm.res.storage.storage-account.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + skuName: skuName + kind: kind accessTier: accessTier allowBlobPublicAccess: allowBlobPublicAccess allowSharedKeyAccess: allowSharedKeyAccess + enableHierarchicalNamespace: enableHierarchicalNamespace minimumTlsVersion: 'TLS1_2' supportsHttpsTrafficOnly: true - isHnsEnabled: enableHierarchicalNamespace - encryption: { - services: { - blob: { - enabled: true - } - file: { - enabled: true + requireInfrastructureEncryption: true + publicNetworkAccess: publicNetworkAccess + networkAcls: networkAcls + blobServices: { + containers: [for container in containers: { + name: container.name + publicAccess: container.publicAccess + }] + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + } + diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] + privateEndpoints: enablePrivateNetworking ? [ + { + name: 'pep-${name}' + customNetworkInterfaceName: 'nic-${name}' + subnetResourceId: privateEndpointSubnetId + service: 'blob' + privateDnsZoneGroup: { + privateDnsZoneGroupConfigs: privateDnsZoneConfigs } } - keySource: 'Microsoft.Storage' - requireInfrastructureEncryption: true - } + ] : [] + roleAssignments: !empty(roleAssignments) ? roleAssignments : [] } } -resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2025-08-01' = { - parent: storageAccount - name: 'default' -} - -resource blobContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-08-01' = [for container in containers: { - parent: blobService - name: container.name - properties: { - publicAccess: container.publicAccess - } -}] - // ============================================================================ // Outputs // ============================================================================ @description('Resource ID of the Storage Account.') -output resourceId string = storageAccount.id +output resourceId string = storage.outputs.resourceId @description('Name of the Storage Account.') -output name string = storageAccount.name +output name string = storage.outputs.name @description('Primary blob endpoint.') -output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob +output blobEndpoint string = storage.outputs.primaryBlobEndpoint -@description('All service endpoints.') -output serviceEndpoints object = storageAccount.properties.primaryEndpoints +@description('Service endpoints.') +output serviceEndpoints object = storage.outputs.serviceEndpoints diff --git a/infra/bicep/modules/fabric/fabric-capacity.bicep b/infra/bicep/modules/fabric/fabric-capacity.bicep index 17f6498bb..664f60e01 100644 --- a/infra/bicep/modules/fabric/fabric-capacity.bicep +++ b/infra/bicep/modules/fabric/fabric-capacity.bicep @@ -1,7 +1,7 @@ // ============================================================================ // Module: Fabric Capacity -// Description: Vanilla Bicep module for Microsoft Fabric Capacity -// Resource: Microsoft.Fabric/capacities@2023-11-01 +// Description: AVM wrapper for Microsoft Fabric Capacity +// AVM Module: avm/res/fabric/capacity:0.1.2 // Docs: https://learn.microsoft.com/azure/templates/microsoft.fabric/capacities // ============================================================================ @@ -36,22 +36,22 @@ param skuName string = 'F2' @description('List of admin members (UPNs for users, object IDs for service principals).') param adminMembers array +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + // ============================================================================ -// Resource +// AVM Module Reference // ============================================================================ -resource fabricCapacity 'Microsoft.Fabric/capacities@2023-11-01' = { - name: name - location: location - tags: tags - sku: { - name: skuName - tier: 'Fabric' - } - properties: { - administration: { - members: adminMembers - } +module fabricCapacity 'br/public:avm/res/fabric/capacity:0.1.2' = { + name: take('avm.res.fabric.capacity.${name}', 64) + params: { + name: name + location: location + skuName: skuName + adminMembers: adminMembers + tags: tags + enableTelemetry: enableTelemetry } } @@ -60,13 +60,13 @@ resource fabricCapacity 'Microsoft.Fabric/capacities@2023-11-01' = { // ============================================================================ @description('The name of the deployed Fabric capacity.') -output name string = fabricCapacity.name +output name string = fabricCapacity.outputs.name @description('The resource ID of the deployed Fabric capacity.') -output resourceId string = fabricCapacity.id +output resourceId string = fabricCapacity.outputs.resourceId @description('The resource group name.') -output resourceGroupName string = resourceGroup().name +output resourceGroupName string = fabricCapacity.outputs.resourceGroupName @description('The location of the deployed Fabric capacity.') -output location string = fabricCapacity.location +output location string = fabricCapacity.outputs.location diff --git a/infra/bicep/modules/identity/cross-scope-role-assignment.bicep b/infra/bicep/modules/identity/cross-scope-role-assignment.bicep index 19e43cc0c..8ed9e3333 100644 --- a/infra/bicep/modules/identity/cross-scope-role-assignment.bicep +++ b/infra/bicep/modules/identity/cross-scope-role-assignment.bicep @@ -14,21 +14,21 @@ param roleDefinitionId string @description('A unique name for the role assignment.') param roleAssignmentName string -@description('The name of the AI Services account to scope the role assignment to.') +@description('The name of the AI Foundry account to scope the role assignment to.') param aiFoundryName string @description('The principal type of the identity being assigned.') @allowed(['ServicePrincipal', 'User']) param principalType string = 'ServicePrincipal' -// Reference the existing AI Services resource in this resource group -resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { +// Reference the existing AI Foundry resource in this resource group +resource aiFoundryAccount 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { name: aiFoundryName } resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: roleAssignmentName - scope: aiServices + scope: aiFoundryAccount properties: { roleDefinitionId: roleDefinitionId principalId: principalId diff --git a/infra/bicep/modules/identity/managed-identity.bicep b/infra/bicep/modules/identity/managed-identity.bicep index e8accb80f..f2d264ee9 100644 --- a/infra/bicep/modules/identity/managed-identity.bicep +++ b/infra/bicep/modules/identity/managed-identity.bicep @@ -1,9 +1,8 @@ // ============================================================================ -// Module: User-Assigned Managed Identity (Generic) -// Description: Creates a user-assigned managed identity. -// This module is NOT called from main.bicep by default. -// Use it when you need a user-assigned identity for specific scenarios -// (e.g., Container Apps, cross-tenant access, pre-provisioned RBAC). +// Module: Managed Identity +// Description: AVM wrapper for User-Assigned Managed Identity +// AVM Module: avm/res/managed-identity/user-assigned-identity +// Usage: Call this module once per identity from main.bicep // ============================================================================ @description('Solution name used for resource naming.') @@ -18,26 +17,33 @@ param location string @description('Tags to apply to the resource.') param tags object = {} +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + // ============================================================================ -// Resource Deployment +// AVM Module Deployment // ============================================================================ -resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { - name: identityName - location: location - tags: tags +module managedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = { + name: take('avm.res.managed-identity.user-assigned-identity.${identityName}', 64) + params: { + name: identityName + location: location + tags: tags + enableTelemetry: enableTelemetry + } } // ============================================================================ // Outputs // ============================================================================ @description('Resource ID of the managed identity.') -output resourceId string = managedIdentity.id +output resourceId string = managedIdentity.outputs.resourceId -@description('Principal ID (object ID) of the managed identity.') -output principalId string = managedIdentity.properties.principalId +@description('Principal ID of the managed identity.') +output principalId string = managedIdentity.outputs.principalId @description('Client ID of the managed identity.') -output clientId string = managedIdentity.properties.clientId +output clientId string = managedIdentity.outputs.clientId @description('Name of the managed identity.') -output name string = managedIdentity.name +output name string = managedIdentity.outputs.name diff --git a/infra/bicep/modules/identity/role-assignments.bicep b/infra/bicep/modules/identity/role-assignments.bicep index 699a16fa7..9d04de79a 100644 --- a/infra/bicep/modules/identity/role-assignments.bicep +++ b/infra/bicep/modules/identity/role-assignments.bicep @@ -28,13 +28,6 @@ param aiSearchPrincipalId string = '' @description('Principal ID of the backend App Service system-assigned identity (empty if not deployed).') param backendAppServicePrincipalId string = '' -@description('Principal ID of the deploying user (for user access roles).') -param deployerPrincipalId string = '' - -@description('Principal type of the deploying user.') -@allowed(['User', 'ServicePrincipal']) -param deployerPrincipalType string = 'User' - // --- Resource References --- @description('Resource ID of the AI Foundry account (empty if not deployed — new project path).') @@ -66,7 +59,6 @@ var roleDefinitions = { cognitiveServicesUser: 'a97b65f3-24c7-4388-baec-2e87135dc908' cognitiveServicesOpenAIUser: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' searchIndexDataReader: '1407120a-92aa-4202-b7e9-c0e197c71c8f' - searchIndexDataContributor: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' searchServiceContributor: '7ca78c08-252a-4471-8644-bb5ff32d4ba0' storageBlobDataContributor: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' storageBlobDataReader: '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' @@ -239,65 +231,3 @@ resource backendAppCosmosRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/s } } -// ============================================================================ -// 5. DEPLOYER (USER) ROLE ASSIGNMENTS -// Deploying user → AI Services, Search, Storage (Bicep-only) -// ============================================================================ - -// Deploying User → Cognitive Services User on AI Services -resource deployerAiServicesAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!useExistingAIProject && !empty(deployerPrincipalId) && !empty(aiFoundryResourceId)) { - scope: aiFoundryAccount - name: guid(solutionName, aiFoundryAccount.id, deployerPrincipalId, roleDefinitions.cognitiveServicesUser) - properties: { - principalId: deployerPrincipalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.cognitiveServicesUser) - principalType: deployerPrincipalType - } -} - -// Deploying User → Foundry User on AI Services -resource deployerAzureAIAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!useExistingAIProject && !empty(deployerPrincipalId) && !empty(aiFoundryResourceId)) { - scope: aiFoundryAccount - name: guid(solutionName, aiFoundryAccount.id, deployerPrincipalId, roleDefinitions.azureAiUser) - properties: { - principalId: deployerPrincipalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.azureAiUser) - principalType: deployerPrincipalType - } -} - -// Deploying User → Search Index Data Contributor on AI Search -resource deployerSearchIndexContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(deployerPrincipalId) && !empty(aiSearchResourceId)) { - scope: aiSearchService - name: guid(solutionName, aiSearchService.id, deployerPrincipalId, roleDefinitions.searchIndexDataContributor) - properties: { - principalId: deployerPrincipalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.searchIndexDataContributor) - principalType: deployerPrincipalType - } -} - -// Deploying User → Search Service Contributor on AI Search -resource deployerSearchServiceContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(deployerPrincipalId) && !empty(aiSearchResourceId)) { - scope: aiSearchService - name: guid(solutionName, aiSearchService.id, deployerPrincipalId, roleDefinitions.searchServiceContributor) - properties: { - principalId: deployerPrincipalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.searchServiceContributor) - principalType: deployerPrincipalType - } -} - -// Deploying User → Storage Blob Data Contributor -resource deployerStorageBlobContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(deployerPrincipalId) && !empty(storageAccountResourceId)) { - scope: storageAccount - name: guid(solutionName, storageAccount.id, deployerPrincipalId, roleDefinitions.storageBlobDataContributor) - properties: { - principalId: deployerPrincipalId - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.storageBlobDataContributor) - principalType: deployerPrincipalType - } -} - -// NOTE: Deployer roles on existing AI Foundry (cross-scope) are assigned via -// 00_build_solution.py to avoid conflicts when the deployer already has the roles. diff --git a/infra/bicep/modules/monitoring/app-insights.bicep b/infra/bicep/modules/monitoring/app-insights.bicep index 21109d756..b726ae81d 100644 --- a/infra/bicep/modules/monitoring/app-insights.bicep +++ b/infra/bicep/modules/monitoring/app-insights.bicep @@ -1,8 +1,8 @@ // ============================================================================ // Module: Application Insights -// Description: Vanilla Bicep module for Application Insights -// Resource: Microsoft.Insights/components@2020-02-02 -// Docs: https://learn.microsoft.com/azure/templates/microsoft.insights/components +// Description: AVM wrapper for Application Insights with WAF alignment +// AVM Module: avm/res/insights/component:0.7.1 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/application-insights // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -23,53 +23,54 @@ param workspaceResourceId string @description('Application type.') param applicationType string = 'web' -@description('Retention period in days.') +@description('Retention period in days. WAF recommends 365.') param retentionInDays int = 365 -@description('Disable IP masking for security.') +@description('Disable IP masking for security. WAF recommends false.') param disableIpMasking bool = false @description('Flow type for Application Insights.') param flowType string = 'Bluefield' +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + @description('Kind of Application Insights resource.') param kind string = 'web' // ============================================================================ -// Resource +// AVM Module Deployment // ============================================================================ - -resource appInsights 'Microsoft.Insights/components@2020-02-02' = { - name: name - location: location - tags: tags - kind: kind - properties: { - Application_Type: applicationType - Flow_Type: flowType - WorkspaceResourceId: workspaceResourceId - RetentionInDays: retentionInDays - DisableIpMasking: disableIpMasking - publicNetworkAccessForIngestion: 'Enabled' - publicNetworkAccessForQuery: 'Enabled' +module appInsights 'br/public:avm/res/insights/component:0.7.1' = { + name: take('avm.res.insights.component.${name}', 64) + params: { + name: name + location: location + tags: tags + workspaceResourceId: workspaceResourceId + kind: kind + applicationType: applicationType + enableTelemetry: enableTelemetry + retentionInDays: retentionInDays + disableIpMasking: disableIpMasking + flowType: flowType } } // ============================================================================ // Outputs // ============================================================================ - @description('Resource ID of the Application Insights instance.') -output resourceId string = appInsights.id +output resourceId string = appInsights.outputs.resourceId @description('Name of the Application Insights instance.') -output name string = appInsights.name +output name string = appInsights.outputs.name @description('Instrumentation key for the Application Insights instance.') -output instrumentationKey string = appInsights.properties.InstrumentationKey +output instrumentationKey string = appInsights.outputs.instrumentationKey @description('Connection string for the Application Insights instance.') -output connectionString string = appInsights.properties.ConnectionString +output connectionString string = appInsights.outputs.connectionString @description('Application ID of the Application Insights instance.') -output applicationId string = appInsights.properties.AppId +output applicationId string = appInsights.outputs.applicationId diff --git a/infra/bicep/modules/monitoring/data-collection-rule.bicep b/infra/bicep/modules/monitoring/data-collection-rule.bicep index 70b19639d..c1fd7606c 100644 --- a/infra/bicep/modules/monitoring/data-collection-rule.bicep +++ b/infra/bicep/modules/monitoring/data-collection-rule.bicep @@ -1,75 +1,149 @@ // ============================================================================ -// Module: Data Collection Rule (Windows Security Events) -// Description: Vanilla Bicep module for an Azure Monitor Data Collection Rule that -// collects Windows Security audit events from the jumpbox VM -// (SFI-AzTBv17 compliance). -// Resource: Microsoft.Insights/dataCollectionRules@2023-03-11 -// NOTE: Not used by the lean main.bicep; retained for module parity with the -// AVM flavor across GSA. +// Module: Data Collection Rule +// Description: AVM wrapper for Azure Monitor Data Collection Rule +// AVM Module: avm/res/insights/data-collection-rule +// WAF: Monitoring for VM observability // ============================================================================ -@description('Required. Name of the data collection rule.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +@description('Optional. Override name for the data collection rule. Defaults to dcr-{solutionName}.') +param name string = 'dcr-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} +@description('Resource ID of the Log Analytics workspace destination.') +param logAnalyticsWorkspaceResourceId string + +@description('Name of the Log Analytics workspace (used for destination naming).') +param logAnalyticsWorkspaceName string = '' + @description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params param enableTelemetry bool = true -@description('Required. Resource ID of the Log Analytics workspace destination.') -param workspaceResourceId string +var dcrLogAnalyticsDestinationName = !empty(logAnalyticsWorkspaceName) ? 'la-${logAnalyticsWorkspaceName}-destination' : 'la-${name}-destination' -@description('Optional. Name of the Log Analytics destination.') -param destinationName string = 'la-destination' - -resource dcr 'Microsoft.Insights/dataCollectionRules@2023-03-11' = { - name: name - location: location - tags: tags - kind: 'Windows' - properties: { - description: 'Collects Windows Security audit success/failure events from jumpbox VM (SFI-AzTBv17 compliance).' - dataSources: { - windowsEventLogs: [ +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module dataCollectionRule 'br/public:avm/res/insights/data-collection-rule:0.11.0' = { + name: take('avm.res.insights.data-collection-rule.${name}', 64) + params: { + name: name + tags: tags + enableTelemetry: enableTelemetry + location: location + dataCollectionRuleProperties: { + kind: 'Windows' + dataSources: { + performanceCounters: [ + { + streams: ['Microsoft-Perf'] + samplingFrequencyInSeconds: 60 + counterSpecifiers: [ + '\\Processor Information(_Total)\\% Processor Time' + '\\Processor Information(_Total)\\% Privileged Time' + '\\Processor Information(_Total)\\% User Time' + '\\Processor Information(_Total)\\Processor Frequency' + '\\System\\Processes' + '\\Process(_Total)\\Thread Count' + '\\Process(_Total)\\Handle Count' + '\\System\\System Up Time' + '\\System\\Context Switches/sec' + '\\System\\Processor Queue Length' + '\\Memory\\% Committed Bytes In Use' + '\\Memory\\Available Bytes' + '\\Memory\\Committed Bytes' + '\\Memory\\Cache Bytes' + '\\Memory\\Pool Paged Bytes' + '\\Memory\\Pool Nonpaged Bytes' + '\\Memory\\Pages/sec' + '\\Memory\\Page Faults/sec' + '\\Process(_Total)\\Working Set' + '\\Process(_Total)\\Working Set - Private' + '\\LogicalDisk(_Total)\\% Disk Time' + '\\LogicalDisk(_Total)\\% Disk Read Time' + '\\LogicalDisk(_Total)\\% Disk Write Time' + '\\LogicalDisk(_Total)\\% Idle Time' + '\\LogicalDisk(_Total)\\Disk Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Read Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Write Bytes/sec' + '\\LogicalDisk(_Total)\\Disk Transfers/sec' + '\\LogicalDisk(_Total)\\Disk Reads/sec' + '\\LogicalDisk(_Total)\\Disk Writes/sec' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Transfer' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Read' + '\\LogicalDisk(_Total)\\Avg. Disk sec/Write' + '\\LogicalDisk(_Total)\\Avg. Disk Queue Length' + '\\LogicalDisk(_Total)\\Avg. Disk Read Queue Length' + '\\LogicalDisk(_Total)\\Avg. Disk Write Queue Length' + '\\LogicalDisk(_Total)\\% Free Space' + '\\LogicalDisk(_Total)\\Free Megabytes' + '\\Network Interface(*)\\Bytes Total/sec' + '\\Network Interface(*)\\Bytes Sent/sec' + '\\Network Interface(*)\\Bytes Received/sec' + '\\Network Interface(*)\\Packets/sec' + '\\Network Interface(*)\\Packets Sent/sec' + '\\Network Interface(*)\\Packets Received/sec' + '\\Network Interface(*)\\Packets Outbound Errors' + '\\Network Interface(*)\\Packets Received Errors' + ] + name: 'perfCounterDataSource60' + } + ] + windowsEventLogs: [ + { + name: 'SecurityAuditEvents' + streams: ['Microsoft-WindowsEvent'] + xPathQueries: [ + 'Security!*[System[(EventID=4624 or EventID=4625)]]' + ] + } + { + name: 'AuditSuccessFailure' + streams: ['Microsoft-Event'] + xPathQueries: [ + 'Security!*[System[(band(Keywords,13510798882111488)) and (EventID != 4624)]]' + ] + } + ] + } + destinations: { + logAnalytics: [ + { + workspaceResourceId: logAnalyticsWorkspaceResourceId + name: dcrLogAnalyticsDestinationName + } + ] + } + dataFlows: [ { - name: 'securityEventLogsDataSource' - streams: [ - 'Microsoft-SecurityEvent' - ] - xPathQueries: [ - 'Security!*[System[(band(Keywords,13510798882111488)) and (EventID != 4624)]]' - ] + streams: ['Microsoft-Perf'] + destinations: [dcrLogAnalyticsDestinationName] + transformKql: 'source' + outputStream: 'Microsoft-Perf' } - ] - } - destinations: { - logAnalytics: [ { - name: destinationName - workspaceResourceId: workspaceResourceId + streams: ['Microsoft-Event'] + destinations: [dcrLogAnalyticsDestinationName] + transformKql: 'source' + outputStream: 'Microsoft-Event' } ] } - dataFlows: [ - { - streams: [ - 'Microsoft-SecurityEvent' - ] - destinations: [ - destinationName - ] - } - ] } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the data collection rule.') -output resourceId string = dcr.id +output resourceId string = dataCollectionRule.outputs.resourceId @description('Name of the data collection rule.') -output name string = dcr.name +output name string = dataCollectionRule.outputs.name diff --git a/infra/bicep/modules/monitoring/log-analytics.bicep b/infra/bicep/modules/monitoring/log-analytics.bicep index 87d79740c..3b231240c 100644 --- a/infra/bicep/modules/monitoring/log-analytics.bicep +++ b/infra/bicep/modules/monitoring/log-analytics.bicep @@ -1,8 +1,8 @@ // ============================================================================ // Module: Log Analytics Workspace -// Description: Vanilla Bicep module for Log Analytics Workspace -// Resource: Microsoft.OperationalInsights/workspaces@2023-09-01 -// Docs: https://learn.microsoft.com/azure/templates/microsoft.operationalinsights/workspaces +// Description: AVM wrapper for Log Analytics Workspace with WAF alignment +// AVM Module: avm/res/operational-insights/workspace:0.15.0 +// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-log-analytics // Note: This module only handles NEW workspace creation. // Existing workspace logic is handled in main.bicep. // ============================================================================ @@ -19,40 +19,72 @@ param location string @description('Tags to apply to the resource.') param tags object = {} -@description('Retention period in days.') +@description('Retention period in days. WAF recommends 365.') param retentionInDays int = 365 @description('SKU name for the workspace.') param skuName string = 'PerGB2018' +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + +// --- WAF: Private Networking --- +@description('Public network access for ingestion.') +param publicNetworkAccessForIngestion string = 'Enabled' + +@description('Public network access for query.') +param publicNetworkAccessForQuery string = 'Enabled' + +// --- WAF: Redundancy --- +@description('Enable workspace replication for redundancy.') +param enableReplication bool = false + +@description('Replication location (paired region).') +param replicationLocation string = '' + +@description('Daily quota in GB. WAF recommends 150 GB/day as starting point.') +param dailyQuotaGb string = '' + +// --- WAF: Monitoring (VM data sources for private networking) --- +@description('Data sources for VM monitoring (Windows events, perf counters).') +param dataSources array = [] + // ============================================================================ -// Resource +// AVM Module Deployment // ============================================================================ - -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { - name: name - location: location - tags: tags - properties: { - retentionInDays: retentionInDays - sku: { - name: skuName - } +module workspace 'br/public:avm/res/operational-insights/workspace:0.15.0' = { + name: take('avm.res.operational-insights.workspace.${name}', 64) + params: { + name: name + location: location + tags: tags + dataRetention: retentionInDays + skuName: skuName + enableTelemetry: enableTelemetry + features: { enableLogAccessUsingOnlyResourcePermissions: true } + diagnosticSettings: [{ useThisWorkspace: true }] + publicNetworkAccessForIngestion: publicNetworkAccessForIngestion + publicNetworkAccessForQuery: publicNetworkAccessForQuery + dailyQuotaGb: !empty(dailyQuotaGb) ? dailyQuotaGb : null + replication: enableReplication ? { + enabled: true + location: replicationLocation + } : null + dataSources: !empty(dataSources) ? dataSources : null } } // ============================================================================ // Outputs // ============================================================================ - @description('Resource ID of the Log Analytics workspace.') -output resourceId string = logAnalytics.id +output resourceId string = workspace.outputs.resourceId @description('Name of the Log Analytics workspace.') -output name string = logAnalytics.name +output name string = workspace.outputs.name @description('Location of the workspace.') -output location string = logAnalytics.location +output location string = location @description('Log Analytics workspace customer ID.') -output logAnalyticsWorkspaceId string = logAnalytics.properties.customerId +output logAnalyticsWorkspaceId string = workspace.outputs.logAnalyticsWorkspaceId diff --git a/infra/bicep/modules/monitoring/portal-dashboard.bicep b/infra/bicep/modules/monitoring/portal-dashboard.bicep index c5c08ec87..5bf9148df 100644 --- a/infra/bicep/modules/monitoring/portal-dashboard.bicep +++ b/infra/bicep/modules/monitoring/portal-dashboard.bicep @@ -1,8 +1,7 @@ // ============================================================================ // Module: Portal Dashboard (Application Insights) -// Description: Vanilla Bicep module for Azure Portal Dashboard -// Resource: Microsoft.Portal/dashboards@2025-04-01-preview -// Docs: https://learn.microsoft.com/azure/templates/microsoft.portal/dashboards +// Description: AVM wrapper for Azure Portal Dashboard +// AVM Module: avm/res/portal/dashboard:0.3.2 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -23,16 +22,21 @@ param lenses array = [] @description('Dashboard metadata (time range, filters, etc.).') param metadata object = {} +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true + // ============================================================================ -// Resource +// AVM Module Deployment // ============================================================================ -resource dashboard 'Microsoft.Portal/dashboards@2025-04-01-preview' = { - name: name - location: location - tags: tags - properties: { +module dashboard 'br/public:avm/res/portal/dashboard:0.3.2' = { + name: take('avm.res.portal.dashboard.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry lenses: lenses - metadata: !empty(metadata) ? metadata : {} + metadata: !empty(metadata) ? metadata : null } } @@ -40,10 +44,10 @@ resource dashboard 'Microsoft.Portal/dashboards@2025-04-01-preview' = { // Outputs // ============================================================================ @description('Resource ID of the dashboard.') -output resourceId string = dashboard.id +output resourceId string = dashboard.outputs.resourceId @description('Name of the dashboard.') -output name string = dashboard.name +output name string = dashboard.outputs.name @description('Resource group the dashboard was deployed to.') -output resourceGroupName string = resourceGroup().name +output resourceGroupName string = dashboard.outputs.resourceGroupName diff --git a/infra/bicep/modules/networking/bastion-host.bicep b/infra/bicep/modules/networking/bastion-host.bicep index 8aaefb0ee..bf524087e 100644 --- a/infra/bicep/modules/networking/bastion-host.bicep +++ b/infra/bicep/modules/networking/bastion-host.bicep @@ -1,82 +1,85 @@ // ============================================================================ // Module: Bastion Host -// Description: Vanilla Bicep module for an Azure Bastion host (Standard SKU). -// Resources: Microsoft.Network/bastionHosts, Microsoft.Network/publicIPAddresses -// NOTE: Not used by the lean main.bicep; retained for module parity with the -// AVM flavor across GSA. +// Description: AVM wrapper for Azure Bastion Host +// AVM Module: avm/res/network/bastion-host // ============================================================================ -@description('Required. Name of the Bastion host.') -param name string +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Required. Azure region for the resource.') +var name = 'bas-${solutionName}' + +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} @description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params param enableTelemetry bool = true -@description('Required. Resource ID of the virtual network to attach the Bastion host to.') +@description('Resource ID of the virtual network.') param virtualNetworkResourceId string -@description('Optional. Resource ID of the Log Analytics workspace for diagnostics. Empty disables diagnostics.') -param logAnalyticsWorkspaceResourceId string = '' +@description('Optional. Diagnostic settings for the resource.') +param diagnosticSettings array? -resource publicIp 'Microsoft.Network/publicIPAddresses@2024-05-01' = { - name: 'pip-${name}' - location: location - tags: tags - sku: { - name: 'Standard' - } - properties: { - publicIPAllocationMethod: 'Static' - } -} +@description('SKU name for the Bastion Host.') +param skuName string = 'Standard' -resource bastion 'Microsoft.Network/bastionHosts@2024-05-01' = { - name: name - location: location - tags: tags - sku: { - name: 'Standard' - } - properties: { - ipConfigurations: [ - { - name: 'IpConf' - properties: { - subnet: { - id: '${virtualNetworkResourceId}/subnets/AzureBastionSubnet' - } - publicIPAddress: { - id: publicIp.id - } - } - } - ] - } -} +@description('Number of scale units.') +param scaleUnits int = 4 + +@description('Disable copy/paste functionality.') +param disableCopyPaste bool = true + +@description('Enable file copy functionality.') +param enableFileCopy bool = false -resource bastionDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(logAnalyticsWorkspaceResourceId)) { - name: 'bastionDiagnostics' - scope: bastion - properties: { - workspaceId: logAnalyticsWorkspaceResourceId - logs: [ - { - categoryGroup: 'allLogs' - enabled: true - } - ] +@description('Enable IP Connect functionality.') +param enableIpConnect bool = false + +@description('Enable shareable link functionality.') +param enableShareableLink bool = false + +@description('Availability zones for the Bastion Host public IP. Pass empty array to disable zone redundancy.') +param availabilityZones array = [] + +@description('Optional. Diagnostic settings for the public IP address.') +param publicIPDiagnosticSettings array? + +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module bastionHost 'br/public:avm/res/network/bastion-host:0.8.2' = { + name: take('avm.res.network.bastion-host.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + skuName: skuName + virtualNetworkResourceId: virtualNetworkResourceId + availabilityZones: availabilityZones + publicIPAddressObject: { + name: 'pip-${name}' + diagnosticSettings: publicIPDiagnosticSettings + tags: tags + } + disableCopyPaste: disableCopyPaste + enableFileCopy: enableFileCopy + enableIpConnect: enableIpConnect + enableShareableLink: enableShareableLink + scaleUnits: scaleUnits + diagnosticSettings: diagnosticSettings } } -@description('Resource ID of the Bastion host.') -output resourceId string = bastion.id +// ============================================================================ +// Outputs +// ============================================================================ +@description('Resource ID of the Bastion Host.') +output resourceId string = bastionHost.outputs.resourceId -@description('Name of the Bastion host.') -output name string = bastion.name +@description('Name of the Bastion Host.') +output name string = bastionHost.outputs.name diff --git a/infra/bicep/modules/networking/private-dns-zone.bicep b/infra/bicep/modules/networking/private-dns-zone.bicep index 74154583f..be1f69733 100644 --- a/infra/bicep/modules/networking/private-dns-zone.bicep +++ b/infra/bicep/modules/networking/private-dns-zone.bicep @@ -1,45 +1,44 @@ // ============================================================================ // Module: Private DNS Zone -// Description: Vanilla Bicep module for a single Private DNS Zone linked to a VNet. -// Resource: Microsoft.Network/privateDnsZones@2024-06-01 -// NOTE: Not used by the lean main.bicep; retained for module parity with the -// AVM flavor across GSA. +// Description: AVM wrapper for Azure Private DNS Zone +// AVM Module: avm/res/network/private-dns-zone +// Usage: Call once per DNS zone from main.bicep // ============================================================================ -@description('Required. Name of the private DNS zone (e.g. privatelink.blob.core.windows.net).') +@description('Name of the private DNS zone (e.g., privatelink.cognitiveservices.azure.com).') param name string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} @description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params param enableTelemetry bool = true -@description('Required. Resource ID of the virtual network to link.') -param virtualNetworkResourceId string +@description('Virtual network links to associate with the DNS zone.') +param virtualNetworkLinks array = [] -resource zone 'Microsoft.Network/privateDnsZones@2024-06-01' = { - name: name - location: 'global' - tags: tags -} +@description('Optional. Array of A records.') +param a array = [] -resource virtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { - parent: zone - name: '${last(split(virtualNetworkResourceId, '/'))}-link' - location: 'global' - tags: tags - properties: { - registrationEnabled: false - virtualNetwork: { - id: virtualNetworkResourceId - } +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module privateDnsZone 'br/public:avm/res/network/private-dns-zone:0.8.1' = { + name: take('avm.res.network.private-dns-zone.${split(name, '.')[1]}', 64) + params: { + name: name + tags: tags + enableTelemetry: enableTelemetry + virtualNetworkLinks: virtualNetworkLinks + a: a } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the private DNS zone.') -output resourceId string = zone.id +output resourceId string = privateDnsZone.outputs.resourceId @description('Name of the private DNS zone.') -output name string = zone.name +output name string = privateDnsZone.outputs.name diff --git a/infra/bicep/modules/networking/private-endpoint.bicep b/infra/bicep/modules/networking/private-endpoint.bicep index 95ada914e..04bfff07c 100644 --- a/infra/bicep/modules/networking/private-endpoint.bicep +++ b/infra/bicep/modules/networking/private-endpoint.bicep @@ -1,73 +1,52 @@ // ============================================================================ // Module: Private Endpoint -// Description: Vanilla Bicep module for an Azure Private Endpoint with DNS zone group. -// Resource: Microsoft.Network/privateEndpoints@2024-05-01 -// NOTE: Not used by the lean main.bicep; retained for module parity with the -// AVM flavor across GSA. +// Description: AVM wrapper for Azure Private Endpoint +// AVM Module: avm/res/network/private-endpoint +// Usage: Call once per private endpoint from main.bicep // ============================================================================ -@description('Required. Name of the private endpoint.') +@description('Name of the private endpoint.') param name string -@description('Required. Azure region for the resource.') +@description('Azure region for the resource.') param location string -@description('Optional. Tags to apply to the resource.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true +@description('Optional. Custom NIC name for the private endpoint.') +param customNetworkInterfaceName string = '' -@description('Required. Resource ID of the subnet to deploy the private endpoint into.') +@description('Resource ID of the subnet for the private endpoint.') param subnetResourceId string -@description('Required. Resource ID of the target resource (private link service).') -param targetResourceId string - -@description('Required. Group IDs (sub-resources) the private endpoint connects to.') -param groupIds array - -@description('Optional. Private DNS zone group configurations. Each item: { name, privateDnsZoneResourceId }.') -param privateDnsZoneConfigs array = [] +@description('Private link service connections configuration.') +param privateLinkServiceConnections array -resource privateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { - name: name - location: location - tags: tags - properties: { - subnet: { - id: subnetResourceId - } - privateLinkServiceConnections: [ - { - name: name - properties: { - privateLinkServiceId: targetResourceId - groupIds: groupIds - } - } - ] - } -} +@description('Optional. Private DNS zone group configuration.') +param privateDnsZoneGroup object? -resource dnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = if (!empty(privateDnsZoneConfigs)) { - parent: privateEndpoint - name: 'default' - properties: { - privateDnsZoneConfigs: [ - for (config, i) in privateDnsZoneConfigs: { - name: config.?name ?? 'config-${i}' - properties: { - privateDnsZoneId: config.privateDnsZoneResourceId - } - } - ] +// ============================================================================ +// AVM Module Deployment +// ============================================================================ +module privateEndpoint 'br/public:avm/res/network/private-endpoint:0.12.0' = { + name: take('avm.res.network.private-endpoint.${name}', 64) + params: { + name: name + location: location + tags: tags + customNetworkInterfaceName: !empty(customNetworkInterfaceName) ? customNetworkInterfaceName : 'nic-${name}' + subnetResourceId: subnetResourceId + privateLinkServiceConnections: privateLinkServiceConnections + privateDnsZoneGroup: privateDnsZoneGroup } } +// ============================================================================ +// Outputs +// ============================================================================ @description('Resource ID of the private endpoint.') -output resourceId string = privateEndpoint.id +output resourceId string = privateEndpoint.outputs.resourceId @description('Name of the private endpoint.') -output name string = privateEndpoint.name +output name string = privateEndpoint.outputs.name diff --git a/infra/bicep/modules/networking/virtual-network.bicep b/infra/bicep/modules/networking/virtual-network.bicep index 65e276d91..ca6f68946 100644 --- a/infra/bicep/modules/networking/virtual-network.bicep +++ b/infra/bicep/modules/networking/virtual-network.bicep @@ -1,125 +1,122 @@ -/****************************************************************************************************************************/ -// Networking - NSGs, VNET and Subnets for Content Generation Solution -// Vanilla Bicep implementation (raw resources, no AVM registry dependency). -// NOTE: Not used by the lean main.bicep; retained for module parity with the -// AVM flavor across GSA. -/****************************************************************************************************************************/ -@description('Name of the virtual network.') -param vnetName string +// ============================================================================ +// Module: Virtual Network +// Description: VNet, Subnets, and NSGs using AVM modules. +// Each subnet gets its own NSG. Subnet config is passed as param. +// AVM Modules: +// - avm/res/network/network-security-group:0.5.3 +// - avm/res/network/virtual-network:0.8.0 +// ============================================================================ -@description('Azure region to deploy resources.') -param location string = resourceGroup().location - -@description('Required. An Array of 1 or more IP Address Prefixes for the Virtual Network.') -param addressPrefixes array = ['10.0.0.0/20'] +@description('Solution name suffix used to derive the resource name.') +param solutionName string -@description('Optional. Deploy Azure Bastion and Jumpbox subnets for VM-based administration.') -param deployBastionAndJumpbox bool = false - -@description('Optional. Tags to be applied to the resources.') -param tags object = {} +var name = 'vnet-${solutionName}' -@description('Optional. The resource ID of the Log Analytics Workspace to send diagnostic logs to.') -param logAnalyticsWorkspaceId string = '' - -@description('Optional. Enable/Disable usage telemetry for module.') -#disable-next-line no-unused-params -param enableTelemetry bool = true +@description('Azure region for the resource.') +param location string = resourceGroup().location -@description('Required. Suffix for resource naming.') -param resourceSuffix string +@description('Address prefixes for the virtual network.') +param addressPrefixes array -// Core subnets: web (App Service), peps (Private Endpoints), aci (Container Instance) -// Optional: AzureBastionSubnet and jumpbox (only when deployBastionAndJumpbox is true) -var coreSubnets = [ +@description('Subnet configurations.') +param subnets subnetType[] = [ { - name: 'web' - addressPrefixes: ['10.0.0.0/23'] - delegation: 'Microsoft.Web/serverFarms' + name: 'backend' + addressPrefixes: ['10.0.0.0/27'] networkSecurityGroup: { - name: 'nsg-web' + name: 'nsg-backend' securityRules: [ { - name: 'AllowHttpsInbound' + name: 'deny-hop-outbound' properties: { - access: 'Allow' - direction: 'Inbound' - priority: 100 + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: ['22', '3389'] + direction: 'Outbound' + priority: 200 protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefixes: ['0.0.0.0/0'] - destinationAddressPrefixes: ['10.0.0.0/23'] } } + ] + } + } + { + name: 'containers' + addressPrefixes: ['10.0.2.0/23'] + delegation: 'Microsoft.App/environments' + privateEndpointNetworkPolicies: 'Enabled' + privateLinkServiceNetworkPolicies: 'Enabled' + networkSecurityGroup: { + name: 'nsg-containers' + securityRules: [ { - name: 'AllowIntraSubnetTraffic' + name: 'deny-hop-outbound' properties: { - access: 'Allow' - direction: 'Inbound' + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: ['22', '3389'] + direction: 'Outbound' priority: 200 - protocol: '*' + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefixes: ['10.0.0.0/23'] - destinationAddressPrefixes: ['10.0.0.0/23'] } } + ] + } + } + { + name: 'webserverfarm' + addressPrefixes: ['10.0.4.0/27'] + delegation: 'Microsoft.Web/serverfarms' + privateEndpointNetworkPolicies: 'Enabled' + privateLinkServiceNetworkPolicies: 'Enabled' + networkSecurityGroup: { + name: 'nsg-webserverfarm' + securityRules: [ { - name: 'AllowAzureLoadBalancer' + name: 'deny-hop-outbound' properties: { - access: 'Allow' - direction: 'Inbound' - priority: 300 - protocol: '*' + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: ['22', '3389'] + direction: 'Outbound' + priority: 200 + protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: 'AzureLoadBalancer' - destinationAddressPrefix: '10.0.0.0/23' } } ] } } { - name: 'peps' - addressPrefixes: ['10.0.2.0/23'] - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Disabled' + name: 'administration' + addressPrefixes: ['10.0.0.32/27'] networkSecurityGroup: { - name: 'nsg-peps' - securityRules: [] - } - } - { - name: 'aci' - addressPrefixes: ['10.0.4.0/24'] - delegation: 'Microsoft.ContainerInstance/containerGroups' - networkSecurityGroup: { - name: 'nsg-aci' + name: 'nsg-administration' securityRules: [ { - name: 'AllowHttpsInbound' + name: 'deny-hop-outbound' properties: { - access: 'Allow' - direction: 'Inbound' - priority: 100 + access: 'Deny' + destinationAddressPrefix: '*' + destinationPortRanges: ['22', '3389'] + direction: 'Outbound' + priority: 200 protocol: 'Tcp' + sourceAddressPrefix: 'VirtualNetwork' sourcePortRange: '*' - destinationPortRange: '8000' - sourceAddressPrefixes: ['10.0.0.0/20'] - destinationAddressPrefixes: ['10.0.4.0/24'] } } ] } } -] - -var bastionSubnets = deployBastionAndJumpbox ? [ { name: 'AzureBastionSubnet' - addressPrefixes: ['10.0.10.0/26'] + addressPrefixes: ['10.0.0.64/26'] networkSecurityGroup: { name: 'nsg-bastion' securityRules: [ @@ -178,107 +175,157 @@ var bastionSubnets = deployBastionAndJumpbox ? [ ] } } - { - name: 'jumpbox' - addressPrefixes: ['10.0.12.0/23'] - networkSecurityGroup: { - name: 'nsg-jumpbox' - securityRules: [ - { - name: 'AllowRdpFromBastion' - properties: { - access: 'Allow' - direction: 'Inbound' - priority: 100 - protocol: 'Tcp' - sourcePortRange: '*' - destinationPortRange: '3389' - sourceAddressPrefixes: ['10.0.10.0/26'] - destinationAddressPrefixes: ['10.0.12.0/23'] - } - } - ] - } - } -] : [] +] + +@description('Tags to apply to the resources.') +param tags object = {} + +@description('Resource ID of the Log Analytics Workspace for diagnostics.') +param logAnalyticsWorkspaceId string + +@description('Optional. Enable/Disable usage telemetry for module.') +param enableTelemetry bool = true -var vnetSubnets = concat(coreSubnets, bastionSubnets) -var subnetNames = map(vnetSubnets, subnet => subnet.name) +@description('Suffix for resource naming.') +param resourceSuffix string -// Create NSGs for subnets +// ============================================================================ +// NSGs — one per subnet +// ============================================================================ @batchSize(1) -resource nsgs 'Microsoft.Network/networkSecurityGroups@2024-05-01' = [ - for (subnet, i) in vnetSubnets: { - name: '${subnet.networkSecurityGroup.name}-${resourceSuffix}' - location: location - tags: tags - properties: { - securityRules: subnet.networkSecurityGroup.securityRules +module nsgs 'br/public:avm/res/network/network-security-group:0.5.3' = [ + for (subnet, i) in subnets: if (!empty(subnet.?networkSecurityGroup)) { + name: take('avm.res.network.nsg.${subnet.?networkSecurityGroup.name}.${resourceSuffix}', 64) + params: { + name: '${subnet.?networkSecurityGroup.name}-${resourceSuffix}' + location: location + securityRules: subnet.?networkSecurityGroup.securityRules + tags: tags + enableTelemetry: enableTelemetry } } ] -// Create VNet and subnets -resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = { - name: vnetName - location: location - tags: tags - properties: { - addressSpace: { - addressPrefixes: addressPrefixes - } +// ============================================================================ +// Virtual Network + Subnets +// ============================================================================ +module virtualNetwork 'br/public:avm/res/network/virtual-network:0.8.0' = { + name: take('avm.res.network.virtual-network.${name}', 64) + params: { + name: name + location: location + addressPrefixes: addressPrefixes subnets: [ - for (subnet, i) in vnetSubnets: { + for (subnet, i) in subnets: { name: subnet.name - properties: { - addressPrefixes: subnet.addressPrefixes - networkSecurityGroup: { - id: nsgs[i].id - } - privateEndpointNetworkPolicies: subnet.?privateEndpointNetworkPolicies - privateLinkServiceNetworkPolicies: subnet.?privateLinkServiceNetworkPolicies - delegations: empty(subnet.?delegation) ? [] : [ - { - name: 'delegation' - properties: { - serviceName: subnet.?delegation - } - } - ] - } - } - ] - } -} - -resource vnetDiagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(logAnalyticsWorkspaceId)) { - name: 'vnetDiagnostics' - scope: virtualNetwork - properties: { - workspaceId: logAnalyticsWorkspaceId - logs: [ - { - categoryGroup: 'allLogs' - enabled: true + addressPrefixes: subnet.?addressPrefixes + networkSecurityGroupResourceId: !empty(subnet.?networkSecurityGroup) ? nsgs[i]!.outputs.resourceId : null + privateEndpointNetworkPolicies: subnet.?privateEndpointNetworkPolicies + privateLinkServiceNetworkPolicies: subnet.?privateLinkServiceNetworkPolicies + delegation: subnet.?delegation } ] - metrics: [ + diagnosticSettings: [ { - category: 'AllMetrics' - enabled: true + name: 'vnetDiagnostics' + workspaceResourceId: logAnalyticsWorkspaceId + logCategoriesAndGroups: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metricCategories: [ + { + category: 'AllMetrics' + enabled: true + } + ] } ] + tags: tags + enableTelemetry: enableTelemetry } } -output name string = virtualNetwork.name -output resourceId string = virtualNetwork.id +// ============================================================================ +// Outputs +// ============================================================================ +output name string = virtualNetwork.outputs.name +output resourceId string = virtualNetwork.outputs.resourceId -// Core subnet outputs (always present) -output webSubnetResourceId string = contains(subnetNames, 'web') ? virtualNetwork.properties.subnets[indexOf(subnetNames, 'web')].id : '' -output pepsSubnetResourceId string = contains(subnetNames, 'peps') ? virtualNetwork.properties.subnets[indexOf(subnetNames, 'peps')].id : '' -output aciSubnetResourceId string = contains(subnetNames, 'aci') ? virtualNetwork.properties.subnets[indexOf(subnetNames, 'aci')].id : '' +output subnets subnetOutputType[] = [ + for (subnet, i) in subnets: { + name: subnet.name + resourceId: virtualNetwork.outputs.subnetResourceIds[i] + nsgName: !empty(subnet.?networkSecurityGroup) ? subnet.?networkSecurityGroup.name : null + nsgResourceId: !empty(subnet.?networkSecurityGroup) ? nsgs[i]!.outputs.resourceId : null + } +] -// Bastion/jumpbox subnet outputs (always declared; will be empty when those subnets are not deployed) -output bastionSubnetResourceId string = contains(subnetNames, 'AzureBastionSubnet') ? virtualNetwork.properties.subnets[indexOf(subnetNames, 'AzureBastionSubnet')].id : '' -output jumpboxSubnetResourceId string = contains(subnetNames, 'jumpbox') ? virtualNetwork.properties.subnets[indexOf(subnetNames, 'jumpbox')].id : '' +// Individual subnet outputs for backward compatibility +output backendSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'backend') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'backend')] + : '' +output containerSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'containers') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'containers')] + : '' +output webserverfarmSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'webserverfarm') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'webserverfarm')] + : '' +output administrationSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'administration') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'administration')] + : '' +output bastionSubnetResourceId string = contains(map(subnets, subnet => subnet.name), 'AzureBastionSubnet') + ? virtualNetwork.outputs.subnetResourceIds[indexOf(map(subnets, subnet => subnet.name), 'AzureBastionSubnet')] + : '' + +// ============================================================================ +// Custom Types +// ============================================================================ +@export() +@description('Subnet output type') +type subnetOutputType = { + @description('The name of the subnet.') + name: string + @description('The resource ID of the subnet.') + resourceId: string + @description('The name of the associated NSG, if any.') + nsgName: string? + @description('The resource ID of the associated NSG, if any.') + nsgResourceId: string? +} + +@export() +@description('Subnet configuration type') +type subnetType = { + @description('Required. The name of the subnet.') + name: string + @description('Required. Address prefixes for the subnet.') + addressPrefixes: string[] + @description('Optional. Delegation for the subnet.') + delegation: string? + @description('Optional. Private endpoint network policies.') + privateEndpointNetworkPolicies: ('Disabled' | 'Enabled' | 'NetworkSecurityGroupEnabled' | 'RouteTableEnabled')? + @description('Optional. Private link service network policies.') + privateLinkServiceNetworkPolicies: ('Disabled' | 'Enabled')? + @description('Optional. NSG configuration for the subnet.') + networkSecurityGroup: networkSecurityGroupType? + @description('Optional. Route table resource ID.') + routeTableResourceId: string? + @description('Optional. Service endpoint policies.') + serviceEndpointPolicies: object[]? + @description('Optional. Service endpoints to enable.') + serviceEndpoints: string[]? + @description('Optional. Disable default outbound connectivity.') + defaultOutboundAccess: bool? +} + +@export() +@description('NSG configuration type') +type networkSecurityGroupType = { + @description('Required. The name of the NSG.') + name: string + @description('Required. Security rules for the NSG.') + securityRules: object[] +} diff --git a/infra/bicep/modules/security/key-vault.bicep b/infra/bicep/modules/security/key-vault.bicep index acd258db0..924eea5b1 100644 --- a/infra/bicep/modules/security/key-vault.bicep +++ b/infra/bicep/modules/security/key-vault.bicep @@ -1,8 +1,6 @@ // ============================================================================ -// Module: Azure Key Vault -// Description: Vanilla Bicep module for Azure Key Vault -// Resource: Microsoft.KeyVault/vaults@2023-07-01 -// Docs: https://learn.microsoft.com/azure/templates/microsoft.keyvault/vaults +// Module: Azure Key Vault (AVM) +// AVM Module: avm/res/key-vault/vault:0.12.1 // ============================================================================ @description('Solution name used for naming convention.') @@ -37,33 +35,63 @@ param enablePurgeProtection bool = true @allowed(['Enabled', 'Disabled']) param publicNetworkAccess string = 'Enabled' -@description('The Microsoft Entra tenant ID for the Key Vault.') -param tenantId string = subscription().tenantId +@description('Secrets to store in the vault (name/value pairs).') +param secrets array = [] + +@description('Enable Azure telemetry collection.') +param enableTelemetry bool = true + +@description('Role assignments.') +param roleAssignments array = [] + +@description('Enable private networking.') +param enablePrivateNetworking bool = false + +@description('Subnet resource ID for private endpoint.') +param privateEndpointSubnetId string = '' + +@description('Private DNS zone resource IDs.') +param privateDnsZoneResourceIds array = [] // ============================================================================ -// Key Vault Resource +// Key Vault (AVM) // ============================================================================ -resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { - name: name - location: location - tags: tags - properties: { - tenantId: tenantId - sku: { - family: 'A' - name: sku - } - accessPolicies: [] +var secretItems = [for secret in secrets: { + name: secret.name + value: secret.value +}] + +var dnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { + name: 'config${i}' + privateDnsZoneResourceId: zoneId +}] + +var privateEndpointConfig = enablePrivateNetworking && !empty(privateEndpointSubnetId) ? [ + { + subnetResourceId: privateEndpointSubnetId + privateDnsZoneGroup: !empty(privateDnsZoneResourceIds) ? { + privateDnsZoneGroupConfigs: dnsZoneConfigs + } : null + } +] : [] + +module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = { + name: take('avm.res.keyvault.vault.${name}', 64) + params: { + name: name + location: location + tags: tags + enableTelemetry: enableTelemetry + sku: sku enableRbacAuthorization: enableRbacAuthorization enableSoftDelete: enableSoftDelete softDeleteRetentionInDays: softDeleteRetentionInDays enablePurgeProtection: enablePurgeProtection publicNetworkAccess: publicNetworkAccess - networkAcls: { - bypass: 'AzureServices' - defaultAction: publicNetworkAccess == 'Disabled' ? 'Deny' : 'Allow' - } + roleAssignments: !empty(roleAssignments) ? roleAssignments : [] + secrets: !empty(secrets) ? secretItems : [] + privateEndpoints: privateEndpointConfig } } @@ -71,11 +99,11 @@ resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { // Outputs // ============================================================================ -@description('The name of the Key Vault.') -output name string = keyVault.name +@description('The name of the key vault.') +output name string = keyVault.outputs.name -@description('The URI of the Key Vault.') -output uri string = keyVault.properties.vaultUri +@description('The URI of the key vault.') +output uri string = keyVault.outputs.uri -@description('The resource ID of the Key Vault.') -output resourceId string = keyVault.id +@description('The resource ID of the key vault.') +output resourceId string = keyVault.outputs.resourceId From b0046fa6194d47f7550fd632e36e15af588245ee Mon Sep 17 00:00:00 2001 From: Nagshetti Date: Tue, 16 Jun 2026 19:16:01 +0530 Subject: [PATCH 4/4] latest cahnges --- .../modules/ai/ai-foundry-connection.bicep | 2 +- .../ai/ai-foundry-model-deployment.bicep | 4 +- .../bicep/modules/ai/ai-foundry-project.bicep | 62 ++---- infra/bicep/modules/ai/ai-search.bicep | 66 ++---- infra/bicep/modules/ai/ai-services.bicep | 74 ++----- .../modules/ai/existing-project-setup.bicep | 8 +- .../modules/compute/app-service-plan.bicep | 37 ++-- infra/bicep/modules/compute/app-service.bicep | 135 +++++------- .../compute/container-app-environment.bicep | 77 +++---- .../bicep/modules/compute/container-app.bicep | 74 ++++--- .../modules/compute/container-instance.bicep | 83 ++++---- .../modules/compute/container-registry.bicep | 100 +++------ .../bicep/modules/compute/function-app.bicep | 77 +++---- infra/bicep/modules/compute/kubernetes.bicep | 110 ++++------ .../modules/data/app-configuration.bicep | 91 +++----- .../bicep/modules/data/cosmos-db-mongo.bicep | 125 +++++------ .../bicep/modules/data/cosmos-db-nosql.bicep | 136 ++++-------- infra/bicep/modules/data/event-grid.bicep | 62 ++++-- infra/bicep/modules/data/event-hub.bicep | 94 +++------ .../data/postgresql-flexible-server.bicep | 197 ++++++++---------- infra/bicep/modules/data/sql-database.bicep | 129 +++++------- .../bicep/modules/data/storage-account.bicep | 116 ++++------- .../modules/fabric/fabric-capacity.bicep | 38 ++-- .../cross-scope-role-assignment.bicep | 8 +- .../modules/identity/managed-identity.bicep | 36 ++-- .../modules/identity/role-assignments.bicep | 70 +++++++ .../modules/monitoring/app-insights.bicep | 53 +++-- .../modules/monitoring/log-analytics.bicep | 72 ++----- .../modules/monitoring/portal-dashboard.bicep | 30 ++- infra/bicep/modules/security/key-vault.bicep | 84 +++----- 30 files changed, 927 insertions(+), 1323 deletions(-) diff --git a/infra/bicep/modules/ai/ai-foundry-connection.bicep b/infra/bicep/modules/ai/ai-foundry-connection.bicep index 443de377c..6649b5f74 100644 --- a/infra/bicep/modules/ai/ai-foundry-connection.bicep +++ b/infra/bicep/modules/ai/ai-foundry-connection.bicep @@ -1,5 +1,5 @@ // ============================================================================ -// Module: AI Foundry Project Connection (Single) +// Module: AI Foundry Project Connection (Single) — Vanilla Bicep // Description: Creates a single connection on an AI Foundry project. // Generic, reusable — call once per connection type from main.bicep. // Supports any connection category (CognitiveSearch, AzureBlob, diff --git a/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep b/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep index 1c534fd88..4ed69a72c 100644 --- a/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep +++ b/infra/bicep/modules/ai/ai-foundry-model-deployment.bicep @@ -1,10 +1,12 @@ // ============================================================================ -// Module: Model Deployment +// Module: Model Deployment — Vanilla Bicep // Description: Deploys a single AI model to an existing AI Services account. // Called repetitively from main.bicep for each model in the array. // Generic, reusable across GSAs. // ============================================================================ +targetScope = 'resourceGroup' + @description('Required. Name of the parent AI Services account.') param aiServicesAccountName string diff --git a/infra/bicep/modules/ai/ai-foundry-project.bicep b/infra/bicep/modules/ai/ai-foundry-project.bicep index 69fc4fa7c..362dbbad9 100644 --- a/infra/bicep/modules/ai/ai-foundry-project.bicep +++ b/infra/bicep/modules/ai/ai-foundry-project.bicep @@ -1,11 +1,11 @@ // ============================================================================ -// Module: AI Foundry Project (Account + Project) -// Description: AVM wrapper for Azure AI Services account creation and -// AI Foundry project provisioning. Generic, reusable across GSAs. -// AVM Module: avm/res/cognitive-services/account -// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-openai +// Module: AI Foundry Project (Account + Project) — Vanilla Bicep +// Description: Creates an Azure AI Services account and AI Foundry project. +// Generic, reusable across GSAs — no app-specific parameters. // ============================================================================ +targetScope = 'resourceGroup' + @description('Required. Solution name suffix used to generate resource names.') param solutionName string @@ -41,31 +41,21 @@ param identityType string = 'SystemAssigned' @allowed(['Allow', 'Deny']) param networkAclsDefaultAction string = 'Allow' -// --- WAF: Monitoring --- -@description('Optional. Diagnostic settings for the resource.') -param diagnosticSettings array? - -// --- WAF: Telemetry --- -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -// --- Role Assignments --- -@description('Optional. Array of role assignments to create on the AI Services account.') -param roleAssignments array? - // ============================================================================ -// AI Services Account (AVM Module) +// AI Services Account // ============================================================================ -module aiServicesAccount 'br/public:avm/res/cognitive-services/account:0.14.2' = { - name: take('avm.res.cognitive-services.account.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - sku: skuName - kind: 'AIServices' - disableLocalAuth: disableLocalAuth +resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' = { + name: name + location: location + tags: tags + sku: { + name: skuName + } + kind: 'AIServices' + identity: { + type: identityType + } + properties: { allowProjectManagement: allowProjectManagement customSubDomainName: name networkAcls: { @@ -74,36 +64,22 @@ module aiServicesAccount 'br/public:avm/res/cognitive-services/account:0.14.2' = ipRules: [] } publicNetworkAccess: publicNetworkAccess - managedIdentities: { - systemAssigned: true - } - diagnosticSettings: diagnosticSettings - deployments: [] - roleAssignments: roleAssignments - // Private endpoints deployed separately to avoid AccountProvisioningStateInvalid - privateEndpoints: [] + disableLocalAuth: disableLocalAuth } } // ============================================================================ // AI Foundry Project // ============================================================================ -resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { - name: name - dependsOn: [aiServicesAccount] -} - resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-12-01' = { parent: aiServices name: projectName location: location - tags: tags kind: 'AIServices' identity: { type: identityType } properties: {} - dependsOn: [aiServicesAccount] } // ============================================================================ diff --git a/infra/bicep/modules/ai/ai-search.bicep b/infra/bicep/modules/ai/ai-search.bicep index aa0843542..798a0f74c 100644 --- a/infra/bicep/modules/ai/ai-search.bicep +++ b/infra/bicep/modules/ai/ai-search.bicep @@ -2,13 +2,13 @@ // Module: AI Search // Description: Deploys Azure AI Search with a two-step pattern: // Step 1: Plain Bicep resource for fast initial creation (name, location, SKU) -// Step 2: AVM module update to enable managed identity & full configuration +// Step 2: Separate module deployment to enable managed identity & full config // This reduces deployment time by making the resource available immediately -// while identity enablement proceeds separately. -// AVM Module: avm/res/search/search-service:0.12.0 -// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-cognitive-search +// while identity enablement proceeds as a separate ARM deployment. // ============================================================================ +targetScope = 'resourceGroup' + @description('Solution name suffix used to derive the resource name.') @minLength(3) param solutionName string @@ -43,10 +43,10 @@ param semanticSearch string = 'free' @description('Whether to disable local authentication.') param disableLocalAuth bool = true -@description('Optional. Authentication options for the search service (e.g., aadOrApiKey).') +@description('Optional. Authentication options for the search service.') param authOptions object = {} -@description('Optional. Network rule set for the search service (e.g., bypass: AzureServices).') +@description('Optional. Network rule set for the search service.') param networkRuleSet object = {} @description('Managed identity type for the search service.') @@ -55,26 +55,10 @@ param managedIdentityType string = 'SystemAssigned' @description('Public network access setting.') param publicNetworkAccess string = 'Enabled' -// --- WAF: Telemetry --- -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -// --- WAF: Monitoring --- -@description('Diagnostic settings for monitoring.') -param diagnosticSettings array = [] - -// --- WAF: Private Networking --- -@description('Private endpoint configurations.') -param privateEndpoints array = [] - -// --- Role Assignments --- -@description('Optional. Array of role assignments to create on the AI Search service.') -param roleAssignments array = [] - // ============================================================================ -// Step 1: Initial resource creation (plain Bicep — fast) +// Step 1: Initial resource creation (fast — no identity) // ============================================================================ -resource searchService 'Microsoft.Search/searchServices@2025-05-01' = { +resource aiSearch 'Microsoft.Search/searchServices@2025-05-01' = { name: name location: location sku: { @@ -83,47 +67,39 @@ resource searchService 'Microsoft.Search/searchServices@2025-05-01' = { } // ============================================================================ -// Step 2: AVM update — enables identity & full configuration +// Step 2: Separate deployment — enables identity & full configuration // ============================================================================ -module searchServiceUpdate 'br/public:avm/res/search/search-service:0.12.0' = { - name: take('avm.res.search.update.${name}', 64) +module searchServiceUpdate 'ai-search-identity.bicep' = { + name: 'searchServiceUpdate' params: { - name: name + name: aiSearch.name location: location tags: tags - enableTelemetry: enableTelemetry - sku: skuName + skuName: skuName replicaCount: replicaCount partitionCount: partitionCount hostingMode: hostingMode semanticSearch: semanticSearch - authOptions: !empty(authOptions) ? authOptions : null disableLocalAuth: disableLocalAuth - networkRuleSet: !empty(networkRuleSet) ? networkRuleSet : null + authOptions: authOptions + networkRuleSet: networkRuleSet + managedIdentityType: managedIdentityType publicNetworkAccess: publicNetworkAccess - managedIdentities: { - systemAssigned: managedIdentityType == 'SystemAssigned' - } - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] - privateEndpoints: privateEndpoints - roleAssignments: !empty(roleAssignments) ? roleAssignments : [] } - dependsOn: [ - searchService - ] } // ============================================================================ // Outputs // ============================================================================ + @description('Resource ID of the AI Search service.') -output resourceId string = searchService.id +output resourceId string = aiSearch.id @description('Name of the AI Search service.') -output name string = searchService.name +output name string = aiSearch.name @description('Endpoint URL of the AI Search service.') -output endpoint string = 'https://${searchService.name}.search.windows.net' +output endpoint string = 'https://${aiSearch.name}.search.windows.net' @description('System-assigned identity principal ID.') -output identityPrincipalId string = searchServiceUpdate.outputs.?systemAssignedMIPrincipalId ?? '' +output identityPrincipalId string = searchServiceUpdate.outputs.systemAssignedMIPrincipalId diff --git a/infra/bicep/modules/ai/ai-services.bicep b/infra/bicep/modules/ai/ai-services.bicep index 7e27f49d6..4c3d6128b 100644 --- a/infra/bicep/modules/ai/ai-services.bicep +++ b/infra/bicep/modules/ai/ai-services.bicep @@ -1,8 +1,8 @@ // ============================================================================ // Module: Azure AI Services (Generic) -// Description: AVM wrapper for Cognitive Services — supports Content Safety, +// Description: Deploys Cognitive Services — supports Content Safety, // Speech, Computer Vision, Document Intelligence, and others. -// AVM Module: avm/res/cognitive-services/account:0.14.2 +// API: Microsoft.CognitiveServices/accounts@2025-04-01 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -34,9 +34,6 @@ param location string @description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = false - @description('SKU for the Cognitive Services account.') @allowed(['F0', 'S0', 'S1']) param sku string = 'S0' @@ -51,57 +48,26 @@ param disableLocalAuth bool = true @allowed(['Enabled', 'Disabled']) param publicNetworkAccess string = 'Enabled' -@description('Whether to enable private networking.') -param enablePrivateNetworking bool = false - -@description('Subnet resource ID for the private endpoint.') -param privateEndpointSubnetId string = '' - -@description('Private DNS zone resource IDs.') -param privateDnsZoneResourceIds array = [] - -@description('Diagnostic settings for monitoring.') -param diagnosticSettings array = [] - -@description('Optional. Role assignments for the resource.') -param roleAssignments array = [] - var effectiveSubDomain = !empty(customSubDomainName) ? customSubDomainName : name -var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { - name: 'dns-zone-${i}' - privateDnsZoneResourceId: zoneId -}] - // ============================================================================ -// AVM Module Deployment +// Resource // ============================================================================ -module aiService 'br/public:avm/res/cognitive-services/account:0.14.2' = { - name: take('avm.res.cognitive-services.${namePrefix}.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - kind: kind - sku: sku +resource aiService 'Microsoft.CognitiveServices/accounts@2025-12-01' = { + name: name + location: location + tags: tags + kind: kind + sku: { + name: sku + } + identity: { + type: 'SystemAssigned' + } + properties: { customSubDomainName: effectiveSubDomain - disableLocalAuth: disableLocalAuth - managedIdentities: { systemAssigned: true } publicNetworkAccess: publicNetworkAccess - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] - roleAssignments: !empty(roleAssignments) ? roleAssignments : [] - privateEndpoints: enablePrivateNetworking ? [ - { - name: 'pep-${name}' - customNetworkInterfaceName: 'nic-${name}' - subnetResourceId: privateEndpointSubnetId - service: 'account' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: privateDnsZoneConfigs - } - } - ] : [] + disableLocalAuth: disableLocalAuth } } @@ -109,13 +75,13 @@ module aiService 'br/public:avm/res/cognitive-services/account:0.14.2' = { // Outputs // ============================================================================ @description('Name of the AI Services account.') -output name string = aiService.outputs.name +output name string = aiService.name @description('Resource ID of the AI Services account.') -output resourceId string = aiService.outputs.resourceId +output resourceId string = aiService.id @description('Endpoint of the AI Services account.') -output endpoint string = aiService.outputs.endpoint +output endpoint string = aiService.properties.endpoint @description('System-assigned identity principal ID.') -output identityPrincipalId string = aiService.outputs.?systemAssignedMIPrincipalId ?? '' +output identityPrincipalId string = aiService.identity.principalId diff --git a/infra/bicep/modules/ai/existing-project-setup.bicep b/infra/bicep/modules/ai/existing-project-setup.bicep index cd0fe1f2c..df0acdc5e 100644 --- a/infra/bicep/modules/ai/existing-project-setup.bicep +++ b/infra/bicep/modules/ai/existing-project-setup.bicep @@ -1,5 +1,5 @@ // ============================================================================ -// Module: Existing AI Foundry Project Reference +// Module: Existing AI Foundry Project Reference — Vanilla Bicep // Description: References an existing AI Services account and project to // retrieve their identities. No deployments, no connections. // Use generic ai-foundry-connection and ai-foundry-model-deployment @@ -15,6 +15,7 @@ param projectName string // ============================================================================ // Existing Resource References // ============================================================================ + resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { name: name } @@ -44,7 +45,7 @@ output cognitiveServicesEndpoint string = aiServices.properties.endpoint output azureOpenAiCuEndpoint string = aiServices.properties.endpoints['Content Understanding'] @description('System-assigned identity principal ID of the AI Services account (empty if none).') -output principalId string = aiServices.identity.?principalId ?? '' +output principalId string = contains(aiServices, 'identity') && contains(aiServices.identity, 'principalId') ? aiServices.identity.principalId : '' @description('Resource ID of the AI Foundry project.') output projectResourceId string = aiProject.id @@ -56,5 +57,4 @@ output projectName string = aiProject.name output projectEndpoint string = aiProject.properties.endpoints['AI Foundry API'] @description('System-assigned identity principal ID of the project (empty if none).') -output projectIdentityPrincipalId string = aiProject.identity.?principalId ?? '' - +output projectIdentityPrincipalId string = contains(aiProject, 'identity') && contains(aiProject.identity, 'principalId') ? aiProject.identity.principalId : '' diff --git a/infra/bicep/modules/compute/app-service-plan.bicep b/infra/bicep/modules/compute/app-service-plan.bicep index 6e9e72d0c..f9409f0cf 100644 --- a/infra/bicep/modules/compute/app-service-plan.bicep +++ b/infra/bicep/modules/compute/app-service-plan.bicep @@ -1,7 +1,7 @@ // ============================================================================ // Module: App Service Plan -// Description: AVM wrapper for Azure App Service Plan -// AVM Module: avm/res/web/serverfarm:0.7.0 +// Description: Creates an Azure App Service Plan +// API: Microsoft.Web/serverfarms@2025-05-01 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -26,33 +26,26 @@ param reserved bool = true @description('Kind of the App Service Plan.') param kind string = 'linux' -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - @description('Number of instances (workers).') param skuCapacity int = 1 -@description('Diagnostic settings for monitoring.') -param diagnosticSettings array = [] - @description('Enable zone redundancy. Requires Premium SKU (P1v3+).') param zoneRedundant bool = false // ============================================================================ -// AVM Module Deployment +// Resource Deployment // ============================================================================ -module appServicePlan 'br/public:avm/res/web/serverfarm:0.7.0' = { - name: take('avm.res.web.serverfarm.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - skuName: skuName - skuCapacity: skuCapacity +resource appServicePlan 'Microsoft.Web/serverfarms@2025-05-01' = { + name: name + location: location + tags: tags + kind: kind + sku: { + name: skuName + capacity: skuCapacity + } + properties: { reserved: reserved - kind: kind - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] zoneRedundant: zoneRedundant } } @@ -61,7 +54,7 @@ module appServicePlan 'br/public:avm/res/web/serverfarm:0.7.0' = { // Outputs // ============================================================================ @description('Resource ID of the App Service Plan.') -output resourceId string = appServicePlan.outputs.resourceId +output resourceId string = appServicePlan.id @description('Name of the App Service Plan.') -output name string = appServicePlan.outputs.name +output name string = appServicePlan.name diff --git a/infra/bicep/modules/compute/app-service.bicep b/infra/bicep/modules/compute/app-service.bicep index bbbcbf68b..39cd9565c 100644 --- a/infra/bicep/modules/compute/app-service.bicep +++ b/infra/bicep/modules/compute/app-service.bicep @@ -1,7 +1,7 @@ // ============================================================================ // Module: App Service -// Description: AVM wrapper for Azure App Service (Web App) -// AVM Module: avm/res/web/site:0.23.1 +// Description: Creates an Azure App Service (Web App) +// API: Microsoft.Web/sites@2025-05-01 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -25,9 +25,6 @@ param linuxFxVersion string @description('Application settings key-value pairs.') param appSettings object = {} -@description('Optional. Resource ID of Application Insights for monitoring integration.') -param applicationInsightResourceId string = '' - @description('Whether to enable Always On.') param alwaysOn bool = true @@ -57,46 +54,23 @@ param appCommandLine string = '' ]) param kind string = 'app,linux' -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -@description('Diagnostic settings for monitoring.') -param diagnosticSettings array = [] - -@description('Subnet resource ID for VNet integration.') -param virtualNetworkSubnetId string = '' - @description('Public network access setting.') param publicNetworkAccess string = 'Enabled' -@description('Optional. Whether to route all outbound traffic through the virtual network.') -param vnetRouteAllEnabled bool = false - -@description('Optional. Whether to route image pull traffic through the virtual network.') -param imagePullTraffic bool = false - -@description('Optional. Whether to route content share traffic through the virtual network.') -param contentShareTraffic bool = false - -import { privateEndpointSingleServiceType } from 'br/public:avm/utl/types/avm-common-types:0.5.1' -@description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.') -param privateEndpoints privateEndpointSingleServiceType[]? - // ============================================================================ -// AVM Module Deployment +// Resource Deployment // ============================================================================ -module appService 'br/public:avm/res/web/site:0.23.1' = { - name: take('avm.res.web.site.${name}', 64) - params: { - name: name - location: location - tags: tags - kind: kind - enableTelemetry: enableTelemetry - serverFarmResourceId: serverFarmResourceId - managedIdentities: { - systemAssigned: true - } +resource appService 'Microsoft.Web/sites@2025-05-01' = { + name: name + location: location + tags: tags + kind: kind + identity: { + type: 'SystemAssigned' + } + properties: { + serverFarmId: serverFarmResourceId + publicNetworkAccess: publicNetworkAccess siteConfig: { alwaysOn: alwaysOn ftpsState: 'Disabled' @@ -106,64 +80,55 @@ module appService 'br/public:avm/res/web/site:0.23.1' = { webSocketsEnabled: webSocketsEnabled appCommandLine: appCommandLine } - e2eEncryptionEnabled: true - configs: [ - { - name: 'appsettings' - properties: appSettings - applicationInsightResourceId: !empty(applicationInsightResourceId) ? applicationInsightResourceId : null - } - { - name: 'logs' - properties: { - applicationLogs: { fileSystem: { level: 'Verbose' } } - detailedErrorMessages: { enabled: true } - failedRequestsTracing: { enabled: true } - httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } - } - } - { - name:'web' - properties: { - vnetRouteAllEnabled: vnetRouteAllEnabled - } - } - ] - outboundVnetRouting: { - contentShareTraffic: contentShareTraffic - imagePullTraffic: imagePullTraffic + endToEndEncryptionEnabled: true + } + + resource basicPublishingCredentialsPoliciesFtp 'basicPublishingCredentialsPolicies' = { + name: 'ftp' + properties: { + allow: false } - publicNetworkAccess: publicNetworkAccess - privateEndpoints: privateEndpoints - virtualNetworkSubnetResourceId: !empty(virtualNetworkSubnetId) ? virtualNetworkSubnetId : null - basicPublishingCredentialsPolicies: [ - { - name: 'ftp' - allow: false - } - { - name: 'scm' - allow: false - } - ] - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] } + resource basicPublishingCredentialsPoliciesScm 'basicPublishingCredentialsPolicies' = { + name: 'scm' + properties: { + allow: false + } + } +} + +resource configAppSettings 'Microsoft.Web/sites/config@2025-05-01' = { + name: 'appsettings' + parent: appService + properties: appSettings +} + +resource configLogs 'Microsoft.Web/sites/config@2025-05-01' = { + name: 'logs' + parent: appService + properties: { + applicationLogs: { fileSystem: { level: 'Verbose' } } + detailedErrorMessages: { enabled: true } + failedRequestsTracing: { enabled: true } + httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } + } + dependsOn: [configAppSettings] } // ============================================================================ // Outputs // ============================================================================ @description('Resource ID of the App Service.') -output resourceId string = appService.outputs.resourceId +output resourceId string = appService.id @description('Name of the App Service.') -output name string = appService.outputs.name +output name string = appService.name @description('Default hostname of the App Service.') -output defaultHostname string = appService.outputs.defaultHostname +output defaultHostname string = appService.properties.defaultHostName @description('URL of the App Service.') -output appUrl string = 'https://${appService.outputs.defaultHostname}' +output appUrl string = 'https://${appService.properties.defaultHostName}' @description('System-assigned identity principal ID.') -output identityPrincipalId string = appService.outputs.?systemAssignedMIPrincipalId ?? '' +output identityPrincipalId string = appService.identity.principalId diff --git a/infra/bicep/modules/compute/container-app-environment.bicep b/infra/bicep/modules/compute/container-app-environment.bicep index 1f488eed9..af51d7366 100644 --- a/infra/bicep/modules/compute/container-app-environment.bicep +++ b/infra/bicep/modules/compute/container-app-environment.bicep @@ -1,6 +1,7 @@ // ============================================================================ -// Module: Azure Container Apps Environment (AVM) -// AVM Module: avm/res/app/managed-environment:0.13.3 +// Module: Azure Container Apps Environment +// Description: Creates an Azure Container Apps managed environment +// API: Microsoft.App/managedEnvironments@2024-03-01 // ============================================================================ @description('Solution name used for naming convention.') @@ -15,33 +16,12 @@ param location string @description('Resource tags.') param tags object = {} -@description('Resource ID of the Log Analytics workspace (required when enableMonitoring is true).') -param logAnalyticsWorkspaceResourceId string = '' - -@description('Subnet resource ID for VNet integration (required when enablePrivateNetworking is true).') -param infrastructureSubnetId string = '' +@description('Resource ID of the Log Analytics workspace.') +param logAnalyticsWorkspaceResourceId string @description('Enable zone redundancy.') param zoneRedundant bool = false -@description('Enable Azure telemetry collection.') -param enableTelemetry bool = true - -@description('Enable private networking (internal environment, public access disabled).') -param enablePrivateNetworking bool = false - -@description('Enable monitoring (Log Analytics + App Insights).') -param enableMonitoring bool = true - -@description('Application Insights connection string (optional, for App Insights integration).') -param appInsightsConnectionString string = '' - -@description('Enable redundancy (dedicated workload profiles + infra resource group).') -param enableRedundancy bool = false - -@description('Infrastructure resource group name (used when zone redundancy is enabled). Defaults to "{resourceGroup}-infra" if empty.') -param infrastructureResourceGroupName string = '${resourceGroup().name}-infra' - @description('Workload profiles configuration (e.g., Consumption or dedicated D4 profiles).') param workloadProfiles array = [ { @@ -51,31 +31,22 @@ param workloadProfiles array = [ ] // ============================================================================ -// Container Apps Environment (AVM) +// Resource Deployment // ============================================================================ -module managedEnvironment 'br/public:avm/res/app/managed-environment:0.13.3' = { - name: take('avm.res.app.managedenvironment.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - // WAF: Private networking - publicNetworkAccess: enablePrivateNetworking ? 'Disabled' : 'Enabled' - internal: enablePrivateNetworking - infrastructureSubnetResourceId: !empty(infrastructureSubnetId) ? infrastructureSubnetId : null - // WAF: Monitoring - appLogsConfiguration: enableMonitoring && !empty(logAnalyticsWorkspaceResourceId) - ? { - destination: 'log-analytics' - logAnalyticsWorkspaceResourceId: logAnalyticsWorkspaceResourceId - } - : null - appInsightsConnectionString: !empty(appInsightsConnectionString) ? appInsightsConnectionString : null - // WAF: Redundancy - zoneRedundant: zoneRedundant || enableRedundancy - infrastructureResourceGroupName: !empty(infrastructureResourceGroupName) ? infrastructureResourceGroupName : null +resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: name + location: location + tags: tags + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: reference(logAnalyticsWorkspaceResourceId, '2023-09-01').customerId + sharedKey: listKeys(logAnalyticsWorkspaceResourceId, '2023-09-01').primarySharedKey + } + } workloadProfiles: workloadProfiles + zoneRedundant: zoneRedundant } } @@ -83,13 +54,13 @@ module managedEnvironment 'br/public:avm/res/app/managed-environment:0.13.3' = { // Outputs // ============================================================================ @description('The name of the Container Apps Environment.') -output name string = managedEnvironment.outputs.name +output name string = containerAppEnvironment.name @description('The resource ID of the Container Apps Environment.') -output resourceId string = managedEnvironment.outputs.resourceId +output resourceId string = containerAppEnvironment.id @description('The default domain of the Container Apps Environment.') -output defaultDomain string = managedEnvironment.outputs.defaultDomain +output defaultDomain string = containerAppEnvironment.properties.defaultDomain -@description('The static IP of the Container Apps Environment.') -output staticIp string = managedEnvironment.outputs.staticIp +@description('The static IP address of the Container Apps Environment.') +output staticIp string = containerAppEnvironment.properties.staticIp diff --git a/infra/bicep/modules/compute/container-app.bicep b/infra/bicep/modules/compute/container-app.bicep index 07e7b4f9e..15596c7d2 100644 --- a/infra/bicep/modules/compute/container-app.bicep +++ b/infra/bicep/modules/compute/container-app.bicep @@ -1,6 +1,7 @@ // ============================================================================ -// Module: Azure Container App (AVM) -// AVM Module: avm/res/app/container-app:0.22.1 +// Module: Azure Container App +// Description: Creates an Azure Container App +// API: Microsoft.App/containerApps@2024-10-02-preview // ============================================================================ @description('Name of the container app.') @@ -50,7 +51,7 @@ param corsPolicy object = {} @allowed(['Single', 'Multiple']) param activeRevisionsMode string = 'Single' -@description('Scale settings (maxReplicas, minReplicas, rules, cooldownPeriod, pollingInterval).') +@description('Scale settings (maxReplicas, minReplicas, rules).') param scaleSettings object = { maxReplicas: 10 minReplicas: 0 @@ -59,33 +60,44 @@ param scaleSettings object = { @description('Workload profile name.') param workloadProfileName string? -@description('Enable Azure telemetry collection.') -param enableTelemetry bool = true - // ============================================================================ -// Container App (AVM) +// Resource Deployment // ============================================================================ -module containerApp 'br/public:avm/res/app/container-app:0.22.1' = { - name: take('avm.res.app.containerapp.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - environmentResourceId: environmentResourceId - containers: containers - ingressExternal: disableIngress ? false : ingressExternal - ingressTargetPort: ingressTargetPort - ingressTransport: ingressTransport - ingressAllowInsecure: ingressAllowInsecure - disableIngress: disableIngress - registries: registries - secrets: secrets - managedIdentities: !empty(managedIdentities) ? managedIdentities : {} - corsPolicy: !empty(corsPolicy) ? corsPolicy : null - activeRevisionsMode: activeRevisionsMode - scaleSettings: scaleSettings +var identityConfig = empty(managedIdentities) ? { type: 'None' } : { + type: contains(managedIdentities, 'userAssignedResourceIds') ? (contains(managedIdentities, 'systemAssigned') && managedIdentities.systemAssigned ? 'SystemAssigned,UserAssigned' : 'UserAssigned') : 'SystemAssigned' + userAssignedIdentities: contains(managedIdentities, 'userAssignedResourceIds') ? reduce(managedIdentities.userAssignedResourceIds, {}, (cur, id) => union(cur, { '${id}': {} })) : null +} + +var ingressConfig = disableIngress ? null : { + external: ingressExternal + targetPort: ingressTargetPort + transport: ingressTransport + allowInsecure: ingressAllowInsecure + corsPolicy: !empty(corsPolicy) ? corsPolicy : null +} + +resource containerApp 'Microsoft.App/containerApps@2024-10-02-preview' = { + name: name + location: location + tags: tags + identity: identityConfig + properties: { + managedEnvironmentId: environmentResourceId workloadProfileName: workloadProfileName + configuration: { + activeRevisionsMode: activeRevisionsMode + ingress: ingressConfig + registries: registries + secrets: secrets + } + template: { + containers: containers + scale: { + minReplicas: scaleSettings.minReplicas + maxReplicas: scaleSettings.maxReplicas + rules: contains(scaleSettings, 'rules') ? scaleSettings.rules : null + } + } } } @@ -93,13 +105,13 @@ module containerApp 'br/public:avm/res/app/container-app:0.22.1' = { // Outputs // ============================================================================ @description('The name of the container app.') -output name string = containerApp.outputs.name +output name string = containerApp.name @description('The resource ID of the container app.') -output resourceId string = containerApp.outputs.resourceId +output resourceId string = containerApp.id @description('The FQDN of the container app.') -output fqdn string = containerApp.outputs.fqdn +output fqdn string = !disableIngress ? containerApp.properties.configuration.ingress.fqdn : '' @description('System-assigned identity principal ID.') -output principalId string = containerApp.outputs.?systemAssignedMIPrincipalId ?? '' +output principalId string = contains(containerApp.identity.type, 'SystemAssigned') ? containerApp.identity.principalId : '' diff --git a/infra/bicep/modules/compute/container-instance.bicep b/infra/bicep/modules/compute/container-instance.bicep index c840be20d..e3d690ff6 100644 --- a/infra/bicep/modules/compute/container-instance.bicep +++ b/infra/bicep/modules/compute/container-instance.bicep @@ -1,6 +1,7 @@ // ============================================================================ -// Module: Azure Container Instance (AVM) -// AVM Module: avm/res/container-instance/container-group:0.7.0 +// Module: Azure Container Instance +// Description: Creates an Azure Container Instance group +// API: Microsoft.ContainerInstance/containerGroups@2025-09-01 // ============================================================================ @description('Name of the container group.') @@ -47,50 +48,51 @@ param subnetResourceId string = '' @description('Availability zone for the container group. Use -1 for no zone.') param availabilityZone int = -1 -@description('Enable Azure telemetry collection.') -param enableTelemetry bool = true - // ============================================================================ // Variables // ============================================================================ var isPrivateNetworking = !empty(subnetResourceId) -var containers = [ - { - name: name - properties: { - image: containerImage - resources: { - requests: { - cpu: cpu - memoryInGB: string(memoryInGB) - } - } - ports: [ - { - port: port - protocol: 'TCP' - } - ] - environmentVariables: environmentVariables - } - } -] +var identityConfig = empty(managedIdentities) ? { type: 'None' } : { + type: contains(managedIdentities, 'userAssignedResourceIds') ? 'UserAssigned' : 'SystemAssigned' + userAssignedIdentities: contains(managedIdentities, 'userAssignedResourceIds') ? reduce(managedIdentities.userAssignedResourceIds, {}, (cur, id) => union(cur, { '${id}': {} })) : null +} // ============================================================================ -// Container Instance (AVM) +// Resource Deployment // ============================================================================ -module containerGroup 'br/public:avm/res/container-instance/container-group:0.7.0' = { - name: take('avm.res.containerinstance.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - containers: containers +resource containerGroup 'Microsoft.ContainerInstance/containerGroups@2025-09-01' = { + name: name + location: location + tags: tags + identity: identityConfig + zones: availabilityZone != -1 ? [string(availabilityZone)] : null + properties: { osType: osType restartPolicy: restartPolicy - managedIdentities: !empty(managedIdentities) ? managedIdentities : {} + containers: [ + { + name: name + properties: { + image: containerImage + resources: { + requests: { + cpu: cpu + memoryInGB: memoryInGB + } + } + ports: [ + { + port: port + protocol: 'TCP' + } + ] + environmentVariables: environmentVariables + } + } + ] + imageRegistryCredentials: imageRegistryCredentials + subnetIds: isPrivateNetworking ? [{ id: subnetResourceId }] : null ipAddress: { type: isPrivateNetworking ? 'Private' : 'Public' ports: [ @@ -101,9 +103,6 @@ module containerGroup 'br/public:avm/res/container-instance/container-group:0.7. ] dnsNameLabel: isPrivateNetworking ? null : name } - imageRegistryCredentials: !empty(imageRegistryCredentials) ? imageRegistryCredentials : [] - subnets: isPrivateNetworking ? [{ subnetResourceId: subnetResourceId }] : [] - availabilityZone: availabilityZone } } @@ -111,10 +110,10 @@ module containerGroup 'br/public:avm/res/container-instance/container-group:0.7. // Outputs // ============================================================================ @description('The name of the container group.') -output name string = containerGroup.outputs.name +output name string = containerGroup.name @description('The resource ID of the container group.') -output resourceId string = containerGroup.outputs.resourceId +output resourceId string = containerGroup.id @description('The IP address of the container group.') -output ipAddress string = containerGroup.outputs.?iPv4Address ?? '' +output ipAddress string = containerGroup.properties.ipAddress.ip diff --git a/infra/bicep/modules/compute/container-registry.bicep b/infra/bicep/modules/compute/container-registry.bicep index 9e1783be4..9566d2182 100644 --- a/infra/bicep/modules/compute/container-registry.bicep +++ b/infra/bicep/modules/compute/container-registry.bicep @@ -1,6 +1,7 @@ // ============================================================================ -// Module: Azure Container Registry (AVM) -// AVM Module: avm/res/container-registry/registry:0.12.1 +// Module: Azure Container Registry +// Description: Creates an Azure Container Registry +// API: Microsoft.ContainerRegistry/registries@2025-04-01 // ============================================================================ @description('Solution name used for naming convention.') @@ -19,80 +20,45 @@ param tags object = {} @allowed(['Basic', 'Standard', 'Premium']) param sku string = 'Premium' -@description('Enable admin user for the registry.') +@description('Enable admin user.') param adminUserEnabled bool = false @description('Public network access setting.') @allowed(['Enabled', 'Disabled']) param publicNetworkAccess string = 'Enabled' -@description('Export policy status. Must be "enabled" when publicNetworkAccess is "Enabled".') +@description('Export policy status.') param exportPolicyStatus string = 'enabled' -@description('Principal IDs to assign AcrPull role.') -param acrPullPrincipalIds array = [] - -@description('Enable private networking.') -param enablePrivateNetworking bool = false - -@description('Subnet resource ID for private endpoint.') -param privateEndpointSubnetId string = '' - -@description('Private DNS zone resource IDs for private endpoint.') -param privateDnsZoneResourceIds array = [] - -@description('Default action for the network rule set. Use Allow when no private endpoint is in place; Deny for private-only.') -@allowed(['Allow', 'Deny']) -param networkRuleSetDefaultAction string = 'Allow' - -@description('Enable Azure telemetry collection.') -param enableTelemetry bool = true - -// ============================================================================ -// Role Assignments // ============================================================================ -var acrPullRoleId = '7f951dda-4ed3-4680-a7ca-43fe172d538d' - -var roleAssignments = [for principalId in acrPullPrincipalIds: { - principalId: principalId - roleDefinitionIdOrName: acrPullRoleId - principalType: 'ServicePrincipal' -}] - +// Resource Deployment // ============================================================================ -// Private Endpoint Config -// ============================================================================ -var dnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { - name: 'config${i}' - privateDnsZoneResourceId: zoneId -}] - -var privateEndpointConfig = enablePrivateNetworking && !empty(privateEndpointSubnetId) ? [ - { - subnetResourceId: privateEndpointSubnetId - privateDnsZoneGroup: !empty(privateDnsZoneResourceIds) ? { - privateDnsZoneGroupConfigs: dnsZoneConfigs - } : null +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2025-04-01' = { + name: name + location: location + tags: tags + sku: { + name: sku } -] : [] - -// ============================================================================ -// Container Registry (AVM) -// ============================================================================ -module containerRegistry 'br/public:avm/res/container-registry/registry:0.12.1' = { - name: take('avm.res.containerregistry.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - acrSku: sku - acrAdminUserEnabled: adminUserEnabled + properties: { + adminUserEnabled: adminUserEnabled publicNetworkAccess: publicNetworkAccess - exportPolicyStatus: exportPolicyStatus - roleAssignments: !empty(acrPullPrincipalIds) ? roleAssignments : [] - privateEndpoints: privateEndpointConfig - networkRuleSetDefaultAction: networkRuleSetDefaultAction + dataEndpointEnabled: false + networkRuleBypassOptions: 'AzureServices' + policies: { + exportPolicy: { + status: exportPolicyStatus + } + retentionPolicy: { + status: 'enabled' + days: 7 + } + trustPolicy: { + status: 'disabled' + type: 'Notary' + } + } + zoneRedundancy: 'Disabled' } } @@ -100,10 +66,10 @@ module containerRegistry 'br/public:avm/res/container-registry/registry:0.12.1' // Outputs // ============================================================================ @description('The name of the container registry.') -output name string = containerRegistry.outputs.name +output name string = containerRegistry.name @description('The login server URL.') -output loginServer string = containerRegistry.outputs.loginServer +output loginServer string = containerRegistry.properties.loginServer @description('The resource ID of the container registry.') -output resourceId string = containerRegistry.outputs.resourceId +output resourceId string = containerRegistry.id \ No newline at end of file diff --git a/infra/bicep/modules/compute/function-app.bicep b/infra/bicep/modules/compute/function-app.bicep index f6d849458..4756b20fe 100644 --- a/infra/bicep/modules/compute/function-app.bicep +++ b/infra/bicep/modules/compute/function-app.bicep @@ -1,6 +1,7 @@ // ============================================================================ -// Module: Azure Function App (AVM) -// AVM Module: avm/res/web/site:0.23.1 +// Module: Azure Function App +// Description: Creates an Azure Function App on Linux +// API: Microsoft.Web/sites@2024-04-01 // ============================================================================ @description('Name of the function app.') @@ -38,44 +39,48 @@ param runtimeStack string = 'python' @description('Runtime version.') param runtimeVersion string = '3.11' -@description('Enable Azure telemetry collection.') -param enableTelemetry bool = true - // ============================================================================ // Variables // ============================================================================ -var baseAppSettings = { - AzureWebJobsStorage__accountName: storageAccountName - FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_WORKER_RUNTIME: runtimeStack +var identityConfig = empty(managedIdentities) ? null : { + type: contains(managedIdentities, 'userAssignedResourceIds') ? (contains(managedIdentities, 'systemAssigned') && managedIdentities.systemAssigned ? 'SystemAssigned,UserAssigned' : 'UserAssigned') : 'SystemAssigned' + userAssignedIdentities: contains(managedIdentities, 'userAssignedResourceIds') ? reduce(managedIdentities.userAssignedResourceIds, {}, (cur, id) => union(cur, { '${id}': {} })) : null +} + +var storageConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageAccountName};AccountKey=${listKeys(storageAccountResourceId, '2023-05-01').keys[0].value};EndpointSuffix=${environment().suffixes.storage}' +var linuxFxVersion = '${toUpper(runtimeStack)}|${runtimeVersion}' + +var baseSettings = [ + { name: 'AzureWebJobsStorage', value: storageConnectionString } + { name: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' } + { name: 'FUNCTIONS_WORKER_RUNTIME', value: toLower(runtimeStack) } + { name: 'WEBSITE_RUN_FROM_PACKAGE', value: '1' } +] + +var mergedSettings = concat(baseSettings, appSettings) + +var defaultSiteConfig = { + linuxFxVersion: linuxFxVersion + ftpsState: 'Disabled' + minTlsVersion: '1.2' + appSettings: mergedSettings } -var customAppSettings = reduce(appSettings, {}, (cur, next) => union(cur, { '${next.name}': next.value })) -var mergedAppSettings = union(baseAppSettings, customAppSettings) +var effectiveSiteConfig = union(defaultSiteConfig, siteConfig) // ============================================================================ -// Function App (AVM) +// Resource Deployment // ============================================================================ -module functionApp 'br/public:avm/res/web/site:0.23.1' = { - name: take('avm.res.web.site.func.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - kind: 'functionapp,linux' - serverFarmResourceId: serverFarmResourceId - storageAccountRequired: false - managedIdentities: managedIdentities - configs: [ - { - name: 'appsettings' - properties: mergedAppSettings - } - ] - siteConfig: union({ - linuxFxVersion: '${toUpper(runtimeStack)}|${runtimeVersion}' - }, siteConfig) +resource functionApp 'Microsoft.Web/sites@2024-04-01' = { + name: name + location: location + tags: tags + kind: 'functionapp,linux' + identity: identityConfig + properties: { + serverFarmId: serverFarmResourceId + siteConfig: effectiveSiteConfig + httpsOnly: true } } @@ -83,13 +88,13 @@ module functionApp 'br/public:avm/res/web/site:0.23.1' = { // Outputs // ============================================================================ @description('The name of the function app.') -output name string = functionApp.outputs.name +output name string = functionApp.name @description('The resource ID of the function app.') -output resourceId string = functionApp.outputs.resourceId +output resourceId string = functionApp.id @description('The default hostname of the function app.') -output defaultHostName string = functionApp.outputs.defaultHostname +output defaultHostName string = functionApp.properties.defaultHostName @description('The principal ID of the system-assigned managed identity.') -output principalId string = functionApp.outputs.?systemAssignedMIPrincipalId ?? '' +output principalId string = contains(functionApp.identity, 'principalId') ? functionApp.identity.principalId : '' diff --git a/infra/bicep/modules/compute/kubernetes.bicep b/infra/bicep/modules/compute/kubernetes.bicep index a15a362ef..44e294404 100644 --- a/infra/bicep/modules/compute/kubernetes.bicep +++ b/infra/bicep/modules/compute/kubernetes.bicep @@ -1,7 +1,7 @@ // ============================================================================ // Module: Azure Kubernetes Service (AKS) -// Description: AVM wrapper for Azure Kubernetes Service Managed Cluster -// AVM Module: avm/res/container-service/managed-cluster:0.13.1 +// Description: Deploys Azure Kubernetes Service Managed Cluster +// API: Microsoft.ContainerService/managedClusters@2025-03-01 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -22,17 +22,14 @@ param kubernetesVersion string = '1.34' @description('Agent pool configurations. Each entry requires name, vmSize, count, mode (System/User).') param agentPools array = [ { - name: 'agentpool' + name: 'systempool' vmSize: 'Standard_D4ds_v5' count: 2 minCount: 1 - maxCount: 2 + maxCount: 3 enableAutoScaling: true osType: 'Linux' mode: 'System' - type: 'VirtualMachineScaleSets' - scaleSetEvictionPolicy: 'Delete' - scaleSetPriority: 'Regular' } ] @@ -70,78 +67,57 @@ param autoUpgradeChannel string = 'stable' @description('Log Analytics workspace resource ID for monitoring.') param logAnalyticsWorkspaceResourceId string = '' -// --- WAF: Networking --- -@description('Public network access setting.') -@allowed(['Enabled', 'Disabled']) -param publicNetworkAccess string = 'Enabled' - -@description('Enable private cluster (API server not publicly accessible).') -param enablePrivateCluster bool = false - -@description('Subnet resource ID for the agent pool (for VNet integration).') -param agentPoolSubnetId string = '' - -@description('Enable Microsoft Defender for Containers.') -param enableDefender bool = false - -@description('Diagnostic settings for monitoring.') -param diagnosticSettings array = [] - -@description('Role assignments for the cluster.') -param roleAssignments array = [] - -@description('Enable Azure telemetry collection.') -param enableTelemetry bool = true - // ============================================================================ // Variables // ============================================================================ var effectiveDnsPrefix = !empty(dnsPrefix) ? dnsPrefix : name -var enableMonitoring = !empty(logAnalyticsWorkspaceResourceId) - -var effectiveAgentPools = [for pool in agentPools: union(pool, !empty(agentPoolSubnetId) ? { vnetSubnetResourceId: agentPoolSubnetId } : {})] // ============================================================================ -// AVM Module Deployment +// Resource Deployment // ============================================================================ -module aksCluster 'br/public:avm/res/container-service/managed-cluster:0.13.1' = { - name: take('avm.res.container-service.managed-cluster.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry +resource aksCluster 'Microsoft.ContainerService/managedClusters@2025-03-01' = { + name: name + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + sku: { + name: 'Base' + tier: skuTier + } + properties: { kubernetesVersion: kubernetesVersion - primaryAgentPoolProfiles: effectiveAgentPools + dnsPrefix: effectiveDnsPrefix enableRBAC: enableRBAC disableLocalAccounts: disableLocalAccounts - networkPlugin: networkPlugin - networkPolicy: networkPolicy - dnsPrefix: effectiveDnsPrefix - skuTier: skuTier - serviceCidr: serviceCidr - dnsServiceIP: dnsServiceIP - publicNetworkAccess: publicNetworkAccess - apiServerAccessProfile: { - enablePrivateCluster: enablePrivateCluster + agentPoolProfiles: [for pool in agentPools: { + name: pool.name + vmSize: pool.vmSize + count: pool.count + minCount: pool.?enableAutoScaling == true ? pool.?minCount : null + maxCount: pool.?enableAutoScaling == true ? pool.?maxCount : null + enableAutoScaling: pool.?enableAutoScaling ?? false + osType: pool.?osType ?? 'Linux' + mode: pool.mode + }] + networkProfile: { + networkPlugin: networkPlugin + networkPolicy: !empty(networkPolicy) ? networkPolicy : null + serviceCidr: serviceCidr + dnsServiceIP: dnsServiceIP } autoUpgradeProfile: { upgradeChannel: autoUpgradeChannel - nodeOSUpgradeChannel: 'Unmanaged' } - managedIdentities: { systemAssigned: true } - omsAgentEnabled: enableMonitoring - monitoringWorkspaceResourceId: enableMonitoring ? logAnalyticsWorkspaceResourceId : null - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] - securityProfile: enableDefender && enableMonitoring ? { - defender: { - logAnalyticsWorkspaceResourceId: logAnalyticsWorkspaceResourceId - securityMonitoring: { - enabled: true + addonProfiles: !empty(logAnalyticsWorkspaceResourceId) ? { + omsagent: { + enabled: true + config: { + logAnalyticsWorkspaceResourceID: logAnalyticsWorkspaceResourceId } } } : {} - roleAssignments: roleAssignments } } @@ -149,16 +125,16 @@ module aksCluster 'br/public:avm/res/container-service/managed-cluster:0.13.1' = // Outputs // ============================================================================ @description('Name of the AKS cluster.') -output name string = aksCluster.outputs.name +output name string = aksCluster.name @description('Resource ID of the AKS cluster.') -output resourceId string = aksCluster.outputs.resourceId +output resourceId string = aksCluster.id @description('FQDN of the AKS cluster.') -output fqdn string = aksCluster.outputs.?fqdn ?? '' +output fqdn string = aksCluster.properties.fqdn @description('Object ID of the AKS kubelet system-assigned managed identity (used by pods at runtime via IMDS).') -output kubeletIdentityObjectId string = aksCluster.outputs.?kubeletIdentityObjectId ?? '' +output kubeletIdentityObjectId string = aksCluster.properties.?identityProfile.?kubeletidentity.?objectId ?? '' @description('Principal ID of the AKS control-plane system-assigned managed identity.') -output systemAssignedMIPrincipalId string = aksCluster.outputs.?systemAssignedMIPrincipalId ?? '' +output systemAssignedMIPrincipalId string = aksCluster.identity.?principalId ?? '' diff --git a/infra/bicep/modules/data/app-configuration.bicep b/infra/bicep/modules/data/app-configuration.bicep index ee3fb3187..6df7aea16 100644 --- a/infra/bicep/modules/data/app-configuration.bicep +++ b/infra/bicep/modules/data/app-configuration.bicep @@ -1,5 +1,7 @@ // ============================================================================ -// Module: Azure App Configuration (AVM) +// Module: Azure App Configuration +// Description: Creates an Azure App Configuration store +// API: Microsoft.AppConfiguration/configurationStores@2023-03-01 // ============================================================================ @description('Solution name used for naming convention.') @@ -8,15 +10,12 @@ param solutionName string @description('Name of the App Configuration store.') param name string = 'appcs-${solutionName}' -@description('Azure region for deployment.') +@description('Azure region for the resource.') param location string -@description('Resource tags.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Enable Azure telemetry collection.') -param enableTelemetry bool = true - @description('SKU for the configuration store.') @allowed(['Free', 'Standard']) param sku string = 'Standard' @@ -24,75 +23,41 @@ param sku string = 'Standard' @description('Disable local (key-based) authentication.') param disableLocalAuth bool = true -@description('Enable purge protection.') -param enablePurgeProtection bool = false - -@description('Soft delete retention in days.') -param softDeleteRetentionInDays int = 7 - -@description('Managed identity configuration.') -param managedIdentities object = {} - -@description('Role assignments.') -param roleAssignments array = [] - @description('Key-value pairs to store in the configuration.') param keyValues array = [] -@description('Enable private networking.') -param enablePrivateNetworking bool = false - -@description('Subnet resource ID for private endpoint.') -param privateEndpointSubnetId string = '' - -@description('Private DNS zone resource IDs.') -param privateDnsZoneResourceIds array = [] - // ============================================================================ -// App Configuration (AVM) +// Resource Deployment // ============================================================================ - -var dnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { - name: 'config${i}' - privateDnsZoneResourceId: zoneId -}] - -var privateEndpointConfig = enablePrivateNetworking && !empty(privateEndpointSubnetId) ? [ - { - subnetResourceId: privateEndpointSubnetId - privateDnsZoneGroup: !empty(privateDnsZoneResourceIds) ? { - privateDnsZoneGroupConfigs: dnsZoneConfigs - } : null +resource appConfiguration 'Microsoft.AppConfiguration/configurationStores@2023-03-01' = { + name: name + location: location + tags: tags + sku: { + name: sku } -] : [] - -module configStore 'br/public:avm/res/app-configuration/configuration-store:0.9.2' = { - name: take('avm.res.appconfiguration.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - sku: sku + properties: { disableLocalAuth: disableLocalAuth - enablePurgeProtection: enablePurgeProtection - softDeleteRetentionInDays: softDeleteRetentionInDays - managedIdentities: !empty(managedIdentities) ? managedIdentities : {} - roleAssignments: !empty(roleAssignments) ? roleAssignments : [] - keyValues: !empty(keyValues) ? keyValues : [] - privateEndpoints: privateEndpointConfig + publicNetworkAccess: 'Enabled' } } +resource configurationKeyValues 'Microsoft.AppConfiguration/configurationStores/keyValues@2023-03-01' = [for keyValue in keyValues: { + name: keyValue.name + parent: appConfiguration + properties: { + value: keyValue.value + } +}] + // ============================================================================ // Outputs // ============================================================================ +@description('The name of the App Configuration store.') +output name string = appConfiguration.name -@description('The name of the configuration store.') -output name string = configStore.outputs.name - -@description('The endpoint of the configuration store.') -output endpoint string = configStore.outputs.endpoint +@description('The endpoint of the App Configuration store.') +output endpoint string = appConfiguration.properties.endpoint -@description('The resource ID of the configuration store.') -output resourceId string = configStore.outputs.resourceId +@description('The resource ID of the App Configuration store.') +output resourceId string = appConfiguration.id diff --git a/infra/bicep/modules/data/cosmos-db-mongo.bicep b/infra/bicep/modules/data/cosmos-db-mongo.bicep index 36baef6c6..620c2f632 100644 --- a/infra/bicep/modules/data/cosmos-db-mongo.bicep +++ b/infra/bicep/modules/data/cosmos-db-mongo.bicep @@ -1,8 +1,7 @@ // ============================================================================ // Module: Cosmos DB (MongoDB) -// Description: AVM wrapper for Azure Cosmos DB with MongoDB API -// AVM Module: avm/res/document-db/database-account:0.19.0 -// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/cosmos-db +// Description: Creates an Azure Cosmos DB account with MongoDB API +// API: Microsoft.DocumentDB/databaseAccounts@2025-10-15 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -27,105 +26,83 @@ param collections array = [] @allowed(['4.2', '5.0', '6.0', '7.0']) param serverVersion string = '7.0' -@description('Enable analytical storage (Synapse Link).') -param enableAnalyticalStorage bool = false - @description('Default consistency level.') @allowed(['Eventual', 'ConsistentPrefix', 'Session', 'BoundedStaleness', 'Strong']) param consistencyLevel string = 'Session' -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -// --- WAF: Monitoring --- -@description('Diagnostic settings for monitoring.') -param diagnosticSettings array = [] - -// --- WAF: Private Networking --- -@description('Public network access setting.') -param publicNetworkAccess string = 'Enabled' - -@description('Whether to enable private networking.') -param enablePrivateNetworking bool = false - -@description('Subnet resource ID for the private endpoint.') -param privateEndpointSubnetId string = '' - -@description('Private DNS zone resource IDs for Cosmos DB (MongoDB).') -param privateDnsZoneResourceIds array = [] - -var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { - name: 'dns-zone-${i}' - privateDnsZoneResourceId: zoneId -}] +@description('Enable analytical storage (Synapse Link).') +param enableAnalyticalStorage bool = false -// --- WAF: Redundancy --- @description('Enable zone redundancy.') param zoneRedundant bool = false @description('Enable automatic failover.') param enableAutomaticFailover bool = false -@description('Optional. HA paired region for multi-region failover when redundancy is enabled.') +@description('HA paired region for multi-region failover.') param haLocation string = '' +@description('Public network access setting.') +param publicNetworkAccess string = 'Enabled' + // ============================================================================ -// AVM Module Deployment +// Resource Deployment // ============================================================================ -module cosmosAccount 'br/public:avm/res/document-db/database-account:0.19.0' = { - name: take('avm.res.document-db.database-account.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - capabilitiesToAdd: ['EnableMongo'] - serverVersion: serverVersion - enableAnalyticalStorage: enableAnalyticalStorage - defaultConsistencyLevel: consistencyLevel - mongodbDatabases: [ - { - name: databaseName - collections: collections - } - ] - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] - networkRestrictions: { - networkAclBypass: 'None' - publicNetworkAccess: publicNetworkAccess - } - privateEndpoints: enablePrivateNetworking ? [ - { - name: 'pep-${name}' - customNetworkInterfaceName: 'nic-${name}' - subnetResourceId: privateEndpointSubnetId - service: 'MongoDB' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: privateDnsZoneConfigs - } - } - ] : [] - zoneRedundant: zoneRedundant - enableAutomaticFailover: enableAutomaticFailover - failoverLocations: zoneRedundant && !empty(haLocation) +resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2025-10-15' = { + name: name + location: location + tags: tags + kind: 'MongoDB' + properties: { + consistencyPolicy: { defaultConsistencyLevel: consistencyLevel } + locations: zoneRedundant && !empty(haLocation) ? [ - { failoverPriority: 0, isZoneRedundant: true, locationName: location } - { failoverPriority: 1, isZoneRedundant: true, locationName: haLocation } + { locationName: location, failoverPriority: 0, isZoneRedundant: true } + { locationName: haLocation, failoverPriority: 1, isZoneRedundant: true } ] : [ - { locationName: location, failoverPriority: 0, isZoneRedundant: false } + { locationName: location, failoverPriority: 0, isZoneRedundant: zoneRedundant } ] + databaseAccountOfferType: 'Standard' + enableAutomaticFailover: enableAutomaticFailover + enableMultipleWriteLocations: false + apiProperties: { serverVersion: serverVersion } + enableAnalyticalStorage: enableAnalyticalStorage + capabilities: [{ name: 'EnableMongo' }] + publicNetworkAccess: publicNetworkAccess } } +resource database 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases@2025-10-15' = { + parent: cosmos + name: databaseName + properties: { + resource: { id: databaseName } + } +} + +resource mongoCollections 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections@2025-10-15' = [for collection in collections: { + parent: database + name: collection.name + properties: { + resource: { + id: collection.name + shardKey: collection.?shardKey ?? {} + indexes: collection.?indexes ?? [ + { key: { keys: ['_id'] } } + ] + } + } +}] + // ============================================================================ // Outputs // ============================================================================ @description('Resource ID of the Cosmos DB account.') -output resourceId string = cosmosAccount.outputs.resourceId +output resourceId string = cosmos.id @description('Name of the Cosmos DB account.') -output name string = cosmosAccount.outputs.name +output name string = cosmos.name @description('MongoDB connection string (without credentials — use Key Vault for secrets).') output connectionString string = 'mongodb+srv://${name}.mongo.cosmos.azure.com:443/?ssl=true&retrywrites=false&maxIdleTimeMS=120000' diff --git a/infra/bicep/modules/data/cosmos-db-nosql.bicep b/infra/bicep/modules/data/cosmos-db-nosql.bicep index 49c39f760..9c758c2ec 100644 --- a/infra/bicep/modules/data/cosmos-db-nosql.bicep +++ b/infra/bicep/modules/data/cosmos-db-nosql.bicep @@ -1,8 +1,7 @@ // ============================================================================ // Module: Cosmos DB -// Description: AVM wrapper for Azure Cosmos DB (NoSQL) with WAF alignment -// AVM Module: avm/res/document-db/database-account:0.19.0 -// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/cosmos-db +// Description: Creates an Azure Cosmos DB (NoSQL) account with database/container +// API: Microsoft.DocumentDB/databaseAccounts@2025-10-15 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -28,113 +27,58 @@ param containers array = [ } ] -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -// --- WAF: Monitoring --- -@description('Diagnostic settings for monitoring.') -param diagnosticSettings array = [] - -// --- WAF: Private Networking --- -@description('Public network access setting.') -param publicNetworkAccess string = 'Enabled' - -@description('Whether to enable private networking.') -param enablePrivateNetworking bool = false - -@description('Subnet resource ID for the private endpoint.') -param privateEndpointSubnetId string = '' - -@description('Private DNS zone resource IDs for Cosmos DB.') -param privateDnsZoneResourceIds array = [] - -var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { - name: 'dns-zone-${i}' - privateDnsZoneResourceId: zoneId -}] - -// --- WAF: Redundancy --- -@description('Enable zone redundancy.') -param zoneRedundant bool = false - -@description('Enable automatic failover.') -param enableAutomaticFailover bool = false - -@description('Optional. HA paired region for multi-region failover when redundancy is enabled.') -param haLocation string = '' - // ============================================================================ -// AVM Module Deployment +// Resource Deployment // ============================================================================ -module cosmosAccount 'br/public:avm/res/document-db/database-account:0.19.0' = { - name: take('avm.res.document-db.database-account.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - capabilitiesToAdd: zoneRedundant ? [] : ['EnableServerless'] - sqlDatabases: [ +resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2025-10-15' = { + name: name + location: location + tags: tags + kind: 'GlobalDocumentDB' + properties: { + consistencyPolicy: { defaultConsistencyLevel: 'Session' } + locations: [ { - name: databaseName - containers: [for container in containers: { - name: container.name - paths: [container.partitionKeyPath] - kind: 'Hash' - version: 2 - }] + locationName: location + failoverPriority: 0 + isZoneRedundant: false } ] - sqlRoleAssignments: [] - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] - networkRestrictions: { - networkAclBypass: 'None' - publicNetworkAccess: publicNetworkAccess - } - privateEndpoints: enablePrivateNetworking ? [ - { - name: 'pep-${name}' - customNetworkInterfaceName: 'nic-${name}' - subnetResourceId: privateEndpointSubnetId - service: 'Sql' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: privateDnsZoneConfigs - } - } - ] : [] - zoneRedundant: zoneRedundant - enableAutomaticFailover: enableAutomaticFailover - failoverLocations: zoneRedundant - ? [ - { - failoverPriority: 0 - isZoneRedundant: true - locationName: location - } - { - failoverPriority: 1 - isZoneRedundant: true - locationName: haLocation - } - ] - : [ - { - locationName: location - failoverPriority: 0 - isZoneRedundant: false - } - ] + databaseAccountOfferType: 'Standard' + enableAutomaticFailover: false + enableMultipleWriteLocations: false + disableLocalAuth: true + capabilities: [ { name: 'EnableServerless' } ] } } +resource database 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases@2025-10-15' = { + parent: cosmos + name: databaseName + properties: { + resource: { id: databaseName } + } + + resource list 'containers' = [for container in containers: { + name: container.name + properties: { + resource: { + id: container.name + partitionKey: { paths: [ container.partitionKeyPath ] } + } + options: {} + } + }] +} + // ============================================================================ // Outputs // ============================================================================ @description('Resource ID of the Cosmos DB account.') -output resourceId string = cosmosAccount.outputs.resourceId +output resourceId string = cosmos.id @description('Name of the Cosmos DB account.') -output name string = cosmosAccount.outputs.name +output name string = cosmos.name @description('Endpoint of the Cosmos DB account.') output endpoint string = 'https://${name}.documents.azure.com:443/' diff --git a/infra/bicep/modules/data/event-grid.bicep b/infra/bicep/modules/data/event-grid.bicep index 16c675167..724481e12 100644 --- a/infra/bicep/modules/data/event-grid.bicep +++ b/infra/bicep/modules/data/event-grid.bicep @@ -1,7 +1,7 @@ // ============================================================================ // Module: Azure Event Grid System Topic -// Description: AVM wrapper for Azure Event Grid System Topic -// AVM Module: avm/res/event-grid/system-topic:0.6.5 +// Description: Deploys Azure Event Grid System Topic +// API: Microsoft.EventGrid/systemTopics@2025-07-15-preview // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -16,9 +16,6 @@ param location string @description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - @description('Resource ID of the source that publishes events (e.g., Storage Account resource ID).') param source string @@ -28,38 +25,57 @@ param topicType string @description('Event subscriptions to create on the system topic.') param eventSubscriptions array = [] -@description('Diagnostic settings for monitoring.') -param diagnosticSettings array = [] - -@description('Managed identities configuration.') +@description('Managed identities configuration. E.g., { systemAssigned: false, userAssignedResourceIds: [] }.') param managedIdentities object = {} // ============================================================================ -// AVM Module Deployment +// Resource // ============================================================================ -module eventGridSystemTopic 'br/public:avm/res/event-grid/system-topic:0.6.5' = { - name: take('avm.res.event-grid.system-topic.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry +resource eventGridSystemTopic 'Microsoft.EventGrid/systemTopics@2025-07-15-preview' = { + name: name + location: location + tags: tags + identity: !empty(managedIdentities) ? { + type: (managedIdentities.?systemAssigned ?? false) && !empty(managedIdentities.?userAssignedResourceIds ?? []) + ? 'SystemAssigned,UserAssigned' + : (managedIdentities.?systemAssigned ?? false) ? 'SystemAssigned' : 'UserAssigned' + userAssignedIdentities: !empty(managedIdentities.?userAssignedResourceIds ?? []) + ? reduce(managedIdentities.userAssignedResourceIds, {}, (cur, next) => union(cur, { '${next}': {} })) + : null + } : null + properties: { source: source topicType: topicType - eventSubscriptions: eventSubscriptions - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] - managedIdentities: !empty(managedIdentities) ? managedIdentities : null } } +// ============================================================================ +// Event Subscriptions +// ============================================================================ +resource systemTopicSubscriptions 'Microsoft.EventGrid/systemTopics/eventSubscriptions@2025-07-15-preview' = [ + for sub in eventSubscriptions: { + name: sub.name + parent: eventGridSystemTopic + properties: { + destination: sub.destination + filter: sub.?filter ?? {} + eventDeliverySchema: sub.?eventDeliverySchema ?? 'EventGridSchema' + retryPolicy: sub.?retryPolicy ?? { + maxDeliveryAttempts: 30 + eventTimeToLiveInMinutes: 1440 + } + } + } +] + // ============================================================================ // Outputs // ============================================================================ @description('Name of the Event Grid System Topic.') -output name string = eventGridSystemTopic.outputs.name +output name string = eventGridSystemTopic.name @description('Resource ID of the Event Grid System Topic.') -output resourceId string = eventGridSystemTopic.outputs.resourceId +output resourceId string = eventGridSystemTopic.id @description('System-assigned principal ID (if enabled).') -output systemAssignedMIPrincipalId string = eventGridSystemTopic.outputs.?systemAssignedMIPrincipalId ?? '' +output systemAssignedMIPrincipalId string = (managedIdentities.?systemAssigned ?? false) ? eventGridSystemTopic.identity.principalId : '' diff --git a/infra/bicep/modules/data/event-hub.bicep b/infra/bicep/modules/data/event-hub.bicep index 3e8efd607..1dfb8a89c 100644 --- a/infra/bicep/modules/data/event-hub.bicep +++ b/infra/bicep/modules/data/event-hub.bicep @@ -1,5 +1,7 @@ // ============================================================================ -// Module: Azure Event Hub Namespace (AVM) +// Module: Azure Event Hub Namespace +// Description: Creates an Azure Event Hub Namespace with event hubs +// API: Microsoft.EventHub/namespaces@2024-01-01 // ============================================================================ @description('Solution name used for naming convention.') @@ -8,85 +10,53 @@ param solutionName string @description('Name of the Event Hub namespace.') param name string = 'evhns-${solutionName}' -@description('Azure region for deployment.') +@description('Azure region for the resource.') param location string -@description('Resource tags.') +@description('Tags to apply to the resource.') param tags object = {} -@description('Enable Azure telemetry collection.') -param enableTelemetry bool = true +@description('The SKU tier for the Event Hub namespace.') +param sku string = 'Standard' -@description('SKU configuration for the namespace.') -param sku object = { - name: 'Standard' - capacity: 1 -} +@description('The throughput unit or processing unit capacity.') +param capacity int = 1 @description('Event hubs to create within the namespace.') param eventhubs array = [] -@description('Managed identity configuration.') -param managedIdentities object = {} - -@description('Role assignments.') -param roleAssignments array = [] - -@description('Enable private networking.') -param enablePrivateNetworking bool = false - -@description('Subnet resource ID for private endpoint.') -param privateEndpointSubnetId string = '' - -@description('Private DNS zone resource IDs.') -param privateDnsZoneResourceIds array = [] - // ============================================================================ -// Event Hub Namespace (AVM) +// Resource Deployment // ============================================================================ - -var eventHubItems = [for eh in eventhubs: { - name: eh.name - messageRetentionInDays: contains(eh, 'messageRetentionInDays') ? eh.messageRetentionInDays : 1 - partitionCount: contains(eh, 'partitionCount') ? eh.partitionCount : 2 -}] - -var dnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { - name: 'config${i}' - privateDnsZoneResourceId: zoneId -}] - -var privateEndpointConfig = enablePrivateNetworking && !empty(privateEndpointSubnetId) ? [ - { - subnetResourceId: privateEndpointSubnetId - privateDnsZoneGroup: !empty(privateDnsZoneResourceIds) ? { - privateDnsZoneGroupConfigs: dnsZoneConfigs - } : null +resource eventHubNamespace 'Microsoft.EventHub/namespaces@2024-01-01' = { + name: name + location: location + tags: tags + sku: { + name: sku + tier: sku + capacity: capacity } -] : [] - -module eventHubNamespace 'br/public:avm/res/event-hub/namespace:0.14.1' = { - name: take('avm.res.eventhub.namespace.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - skuName: sku.name - skuCapacity: sku.capacity - eventhubs: eventHubItems - managedIdentities: !empty(managedIdentities) ? managedIdentities : {} - roleAssignments: !empty(roleAssignments) ? roleAssignments : [] - privateEndpoints: privateEndpointConfig + properties: { + minimumTlsVersion: '1.2' + publicNetworkAccess: 'Enabled' } } +resource eventHubResources 'Microsoft.EventHub/namespaces/eventhubs@2024-01-01' = [for eventhub in eventhubs: { + name: eventhub.name + parent: eventHubNamespace + properties: { + messageRetentionInDays: eventhub.?messageRetentionInDays ?? 1 + partitionCount: eventhub.?partitionCount ?? 2 + } +}] + // ============================================================================ // Outputs // ============================================================================ - @description('The name of the Event Hub namespace.') -output name string = eventHubNamespace.outputs.name +output name string = eventHubNamespace.name @description('The resource ID of the Event Hub namespace.') -output resourceId string = eventHubNamespace.outputs.resourceId +output resourceId string = eventHubNamespace.id diff --git a/infra/bicep/modules/data/postgresql-flexible-server.bicep b/infra/bicep/modules/data/postgresql-flexible-server.bicep index 42cbc4f44..3c6cb0eb2 100644 --- a/infra/bicep/modules/data/postgresql-flexible-server.bicep +++ b/infra/bicep/modules/data/postgresql-flexible-server.bicep @@ -1,148 +1,133 @@ -// ============================================================================ -// Module: PostgreSQL Flexible Server -// Description: AVM wrapper for Azure Database for PostgreSQL Flexible Server -// AVM Module: avm/res/db-for-postgre-sql/flexible-server:0.15.4 -// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/postgresql -// ============================================================================ - @description('Solution name suffix used to derive the resource name.') param solutionName string @description('Name of the PostgreSQL Flexible Server.') param name string = 'psql-${solutionName}' -@description('Azure region for the resource.') +@description('The Azure region where the PostgreSQL Flexible Server will be deployed.') param location string @description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - @description('Azure AD administrators for the server. Each entry requires objectId, principalName, and principalType (User, Group, or ServicePrincipal).') param administrators array @description('The PostgreSQL version to deploy.') param version string = '16' -@description('SKU name for the PostgreSQL Flexible Server.') +@description('The SKU name for the PostgreSQL Flexible Server.') param skuName string = 'Standard_B1ms' -@description('SKU tier for the PostgreSQL Flexible Server.') +@description('The SKU tier for the PostgreSQL Flexible Server.') @allowed(['Burstable', 'GeneralPurpose', 'MemoryOptimized']) param skuTier string = 'Burstable' -@description('Storage size in GB.') +@description('The storage size in GB.') param storageSizeGB int = 32 -@description('Availability zone for the server.') -param availabilityZone int = 1 - @description('Optional databases to create on the server. Each entry should have a name, and optionally charset and collation.') param databases array = [] @description('Optional server configurations (e.g., extensions). Each entry should have a name, value, and source.') param configurations array = [] -// --- WAF: Monitoring --- -@description('Diagnostic settings for monitoring.') -param diagnosticSettings array = [] - -// --- WAF: Private Networking --- -@description('Public network access setting.') -param publicNetworkAccess string = 'Enabled' - -@description('Whether to enable private networking.') -param enablePrivateNetworking bool = false - -@description('Subnet resource ID for the private endpoint.') -param privateEndpointSubnetId string = '' - -@description('Private DNS zone resource IDs for PostgreSQL.') -param privateDnsZoneResourceIds array = [] - -var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { - name: 'dns-zone-${i}' - privateDnsZoneResourceId: zoneId -}] - -// --- WAF: Redundancy --- -@description('High availability mode.') -@allowed(['Disabled', 'SameZone', 'ZoneRedundant']) -param highAvailability string = 'Disabled' - -@description('Standby availability zone for high availability.') -param highAvailabilityZone int = -1 - -// ============================================================================ -// AVM Module Deployment -// ============================================================================ -module postgresServer 'br/public:avm/res/db-for-postgre-sql/flexible-server:0.15.4' = { - name: take('avm.res.postgre-sql.flexible-server.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - skuName: skuName +resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2026-01-01-preview' = { + name: name + location: location + tags: tags + sku: { + name: skuName tier: skuTier - storageSizeGB: storageSizeGB + } + properties: { version: version - availabilityZone: availabilityZone - highAvailability: highAvailability - highAvailabilityZone: highAvailabilityZone - publicNetworkAccess: publicNetworkAccess - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] - administrators: [for admin in administrators: { - objectId: admin.objectId + storage: { + storageSizeGB: storageSizeGB + } + authConfig: { + activeDirectoryAuth: 'Enabled' + passwordAuth: 'Disabled' + } + highAvailability: { + mode: 'Disabled' + } + network: { + publicNetworkAccess: 'Enabled' + } + } +} + +resource firewallAllowAzureIPs 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2026-01-01-preview' = { + name: 'AllowAllAzureServicesAndResourcesWithinAzureIps' + parent: postgresServer + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } +} + +resource firewallAllowAll 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2026-01-01-preview' = { + name: 'AllowAll' + parent: postgresServer + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } +} + +// AAD admins must wait for firewall rules — server needs to be fully accessible first +@batchSize(1) +resource postgresAdmins 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2026-01-01-preview' = [ + for admin in administrators: { + parent: postgresServer + name: admin.objectId + dependsOn: [ + firewallAllowAzureIPs + firewallAllowAll + ] + properties: { principalName: admin.principalName principalType: admin.principalType - }] - firewallRules: publicNetworkAccess == 'Enabled' ? [ - { - name: 'AllowAllAzureServicesAndResourcesWithinAzureIps' - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } - { - name: 'AllowAll' - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - ] : [] - privateEndpoints: enablePrivateNetworking ? [ - { - name: 'pep-${name}' - customNetworkInterfaceName: 'nic-${name}' - subnetResourceId: privateEndpointSubnetId - service: 'postgresqlServer' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: privateDnsZoneConfigs - } - } - ] : [] - databases: [for db in databases: { - name: db.name + tenantId: subscription().tenantId + } + } +] + +resource serverDatabases 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2026-01-01-preview' = [ + for db in databases: { + name: db.name + parent: postgresServer + properties: { charset: db.?charset ?? 'UTF8' collation: db.?collation ?? 'en_US.utf8' - }] - configurations: [for config in configurations: { - name: config.name + } + dependsOn: [ + postgresAdmins + ] + } +] + +@batchSize(1) +resource serverConfigurations 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2026-01-01-preview' = [ + for config in configurations: { + name: config.name + parent: postgresServer + properties: { value: config.value source: config.source - }] + } + dependsOn: [ + postgresAdmins + ] } -} +] -// ============================================================================ -// Outputs -// ============================================================================ -@description('Fully qualified domain name of the PostgreSQL Flexible Server.') -output serverFqdn string = postgresServer.outputs.?fqdn ?? '${name}.postgres.database.azure.com' +@description('The fully qualified domain name of the PostgreSQL Flexible Server.') +output serverFqdn string = postgresServer.properties.fullyQualifiedDomainName -@description('Name of the PostgreSQL Flexible Server.') -output name string = postgresServer.outputs.name +@description('The name of the PostgreSQL Flexible Server.') +output name string = postgresServer.name -@description('Resource ID of the PostgreSQL Flexible Server.') -output resourceId string = postgresServer.outputs.resourceId +@description('The resource ID of the PostgreSQL Flexible Server.') +output resourceId string = postgresServer.id diff --git a/infra/bicep/modules/data/sql-database.bicep b/infra/bicep/modules/data/sql-database.bicep index 4afa61b08..2e647c1e7 100644 --- a/infra/bicep/modules/data/sql-database.bicep +++ b/infra/bicep/modules/data/sql-database.bicep @@ -1,8 +1,7 @@ // ============================================================================ // Module: SQL Database -// Description: AVM wrapper for Azure SQL Server and Database -// AVM Module: avm/res/sql/server:0.21.1 -// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-sql-database +// Description: Creates an Azure SQL Server and Database +// API: Microsoft.Sql/servers@2025-01-01 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -20,9 +19,6 @@ param location string @description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - @description('Principal ID of the deployer for admin access.') param deployerPrincipalId string @@ -44,83 +40,62 @@ param autoPauseDelay int = 60 @description('Minimum capacity (vCores).') param minCapacity int = 1 -// --- WAF: Private Networking --- -@description('Public network access setting.') -param publicNetworkAccess string = 'Enabled' - -@description('Whether to enable private networking.') -param enablePrivateNetworking bool = false - -@description('Subnet resource ID for the private endpoint.') -param privateEndpointSubnetId string = '' - -@description('Private DNS zone resource IDs for SQL Server.') -param privateDnsZoneResourceIds array = [] - -var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { - name: 'dns-zone-${i}' - privateDnsZoneResourceId: zoneId -}] - // ============================================================================ -// AVM Module Deployment +// Resource Deployment // ============================================================================ -module sqlServer 'br/public:avm/res/sql/server:0.21.1' = { - name: take('avm.res.sql.server.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - minimalTlsVersion: '1.2' - publicNetworkAccess: publicNetworkAccess +resource sqlServer 'Microsoft.Sql/servers@2025-01-01' = { + name: name + location: location + tags: tags + properties: { + publicNetworkAccess: 'Enabled' + version: '12.0' restrictOutboundNetworkAccess: 'Disabled' + minimalTlsVersion: '1.2' administrators: { - azureADOnlyAuthentication: true login: deployerPrincipalId - principalType: 'User' sid: deployerPrincipalId tenantId: subscription().tenantId + administratorType: 'ActiveDirectory' + azureADOnlyAuthentication: true } - databases: [ - { - name: databaseName - availabilityZone: -1 - collation: 'SQL_Latin1_General_CP1_CI_AS' - autoPauseDelay: autoPauseDelay - minCapacity: '${minCapacity}' - zoneRedundant: false - sku: { - name: skuName - tier: skuTier - family: skuFamily - capacity: skuCapacity - } - } - ] - firewallRules: publicNetworkAccess == 'Enabled' ? [ - { - name: 'AllowSpecificRange' - startIpAddress: '0.0.0.0' - endIpAddress: '255.255.255.255' - } - { - name: 'AllowAllWindowsAzureIps' - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } - ] : [] - privateEndpoints: enablePrivateNetworking ? [ - { - name: 'pep-${name}' - customNetworkInterfaceName: 'nic-${name}' - subnetResourceId: privateEndpointSubnetId - service: 'sqlServer' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: privateDnsZoneConfigs - } - } - ] : [] + } +} + +resource firewallRule 'Microsoft.Sql/servers/firewallRules@2025-01-01' = { + name: 'AllowSpecificRange' + parent: sqlServer + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } +} + +resource AllowAllWindowsAzureIps 'Microsoft.Sql/servers/firewallRules@2025-01-01' = { + name: 'AllowAllWindowsAzureIps' + parent: sqlServer + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } +} + +resource sqlDB 'Microsoft.Sql/servers/databases@2025-01-01' = { + parent: sqlServer + name: databaseName + location: location + sku: { + name: skuName + tier: skuTier + family: skuFamily + capacity: skuCapacity + } + properties: { + collation: 'SQL_Latin1_General_CP1_CI_AS' + autoPauseDelay: autoPauseDelay + minCapacity: minCapacity + readScale: 'Disabled' + zoneRedundant: false } } @@ -134,7 +109,7 @@ output serverFqdn string = '${name}.database.windows.net' output databaseName string = databaseName @description('Resource ID of the SQL Server.') -output serverResourceId string = sqlServer.outputs.resourceId +output serverResourceId string = sqlServer.id @description('Name of the SQL Server.') -output name string = sqlServer.outputs.name +output name string = sqlServer.name diff --git a/infra/bicep/modules/data/storage-account.bicep b/infra/bicep/modules/data/storage-account.bicep index 329a532be..dc3e93f18 100644 --- a/infra/bicep/modules/data/storage-account.bicep +++ b/infra/bicep/modules/data/storage-account.bicep @@ -1,8 +1,7 @@ // ============================================================================ // Module: Storage Account -// Description: AVM wrapper for Azure Storage Account with WAF alignment -// AVM Module: avm/res/storage/storage-account:0.32.0 -// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/storage-accounts +// Description: Creates an Azure Storage Account with blob container +// API: Microsoft.Storage/storageAccounts@2025-08-01 // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -36,9 +35,6 @@ param allowSharedKeyAccess bool = true @description('Enable hierarchical namespace (Data Lake Storage Gen2).') param enableHierarchicalNamespace bool = false -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - @description('Blob containers to create.') param containers array = [ { @@ -47,93 +43,63 @@ param containers array = [ } ] -// --- WAF: Monitoring --- -@description('Diagnostic settings for monitoring.') -param diagnosticSettings array = [] - -// --- WAF: Private Networking --- -@description('Public network access setting.') -param publicNetworkAccess string = 'Enabled' - -@description('Network ACLs for the storage account.') -param networkAcls object = { - defaultAction: 'Allow' - bypass: 'AzureServices' -} - -@description('Whether to enable private networking.') -param enablePrivateNetworking bool = false - -@description('Subnet resource ID for the private endpoint.') -param privateEndpointSubnetId string = '' - -@description('Private DNS zone resource IDs for Storage (blob).') -param privateDnsZoneResourceIds array = [] - -var privateDnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { - name: 'dns-zone-${i}' - privateDnsZoneResourceId: zoneId -}] - -// --- Role Assignments --- -@description('Optional. Array of role assignments to create on the Storage Account.') -param roleAssignments array = [] - // ============================================================================ -// AVM Module Deployment +// Resource Deployment // ============================================================================ -module storage 'br/public:avm/res/storage/storage-account:0.32.0' = { - name: take('avm.res.storage.storage-account.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - skuName: skuName - kind: kind +resource storageAccount 'Microsoft.Storage/storageAccounts@2025-08-01' = { + name: name + location: location + tags: tags + kind: kind + sku: { + name: skuName + } + properties: { accessTier: accessTier allowBlobPublicAccess: allowBlobPublicAccess allowSharedKeyAccess: allowSharedKeyAccess - enableHierarchicalNamespace: enableHierarchicalNamespace minimumTlsVersion: 'TLS1_2' supportsHttpsTrafficOnly: true - requireInfrastructureEncryption: true - publicNetworkAccess: publicNetworkAccess - networkAcls: networkAcls - blobServices: { - containers: [for container in containers: { - name: container.name - publicAccess: container.publicAccess - }] - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] - } - diagnosticSettings: !empty(diagnosticSettings) ? diagnosticSettings : [] - privateEndpoints: enablePrivateNetworking ? [ - { - name: 'pep-${name}' - customNetworkInterfaceName: 'nic-${name}' - subnetResourceId: privateEndpointSubnetId - service: 'blob' - privateDnsZoneGroup: { - privateDnsZoneGroupConfigs: privateDnsZoneConfigs + isHnsEnabled: enableHierarchicalNamespace + encryption: { + services: { + blob: { + enabled: true + } + file: { + enabled: true } } - ] : [] - roleAssignments: !empty(roleAssignments) ? roleAssignments : [] + keySource: 'Microsoft.Storage' + requireInfrastructureEncryption: true + } } } +resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2025-08-01' = { + parent: storageAccount + name: 'default' +} + +resource blobContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-08-01' = [for container in containers: { + parent: blobService + name: container.name + properties: { + publicAccess: container.publicAccess + } +}] + // ============================================================================ // Outputs // ============================================================================ @description('Resource ID of the Storage Account.') -output resourceId string = storage.outputs.resourceId +output resourceId string = storageAccount.id @description('Name of the Storage Account.') -output name string = storage.outputs.name +output name string = storageAccount.name @description('Primary blob endpoint.') -output blobEndpoint string = storage.outputs.primaryBlobEndpoint +output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob -@description('Service endpoints.') -output serviceEndpoints object = storage.outputs.serviceEndpoints +@description('All service endpoints.') +output serviceEndpoints object = storageAccount.properties.primaryEndpoints diff --git a/infra/bicep/modules/fabric/fabric-capacity.bicep b/infra/bicep/modules/fabric/fabric-capacity.bicep index 664f60e01..17f6498bb 100644 --- a/infra/bicep/modules/fabric/fabric-capacity.bicep +++ b/infra/bicep/modules/fabric/fabric-capacity.bicep @@ -1,7 +1,7 @@ // ============================================================================ // Module: Fabric Capacity -// Description: AVM wrapper for Microsoft Fabric Capacity -// AVM Module: avm/res/fabric/capacity:0.1.2 +// Description: Vanilla Bicep module for Microsoft Fabric Capacity +// Resource: Microsoft.Fabric/capacities@2023-11-01 // Docs: https://learn.microsoft.com/azure/templates/microsoft.fabric/capacities // ============================================================================ @@ -36,22 +36,22 @@ param skuName string = 'F2' @description('List of admin members (UPNs for users, object IDs for service principals).') param adminMembers array -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - // ============================================================================ -// AVM Module Reference +// Resource // ============================================================================ -module fabricCapacity 'br/public:avm/res/fabric/capacity:0.1.2' = { - name: take('avm.res.fabric.capacity.${name}', 64) - params: { - name: name - location: location - skuName: skuName - adminMembers: adminMembers - tags: tags - enableTelemetry: enableTelemetry +resource fabricCapacity 'Microsoft.Fabric/capacities@2023-11-01' = { + name: name + location: location + tags: tags + sku: { + name: skuName + tier: 'Fabric' + } + properties: { + administration: { + members: adminMembers + } } } @@ -60,13 +60,13 @@ module fabricCapacity 'br/public:avm/res/fabric/capacity:0.1.2' = { // ============================================================================ @description('The name of the deployed Fabric capacity.') -output name string = fabricCapacity.outputs.name +output name string = fabricCapacity.name @description('The resource ID of the deployed Fabric capacity.') -output resourceId string = fabricCapacity.outputs.resourceId +output resourceId string = fabricCapacity.id @description('The resource group name.') -output resourceGroupName string = fabricCapacity.outputs.resourceGroupName +output resourceGroupName string = resourceGroup().name @description('The location of the deployed Fabric capacity.') -output location string = fabricCapacity.outputs.location +output location string = fabricCapacity.location diff --git a/infra/bicep/modules/identity/cross-scope-role-assignment.bicep b/infra/bicep/modules/identity/cross-scope-role-assignment.bicep index 8ed9e3333..19e43cc0c 100644 --- a/infra/bicep/modules/identity/cross-scope-role-assignment.bicep +++ b/infra/bicep/modules/identity/cross-scope-role-assignment.bicep @@ -14,21 +14,21 @@ param roleDefinitionId string @description('A unique name for the role assignment.') param roleAssignmentName string -@description('The name of the AI Foundry account to scope the role assignment to.') +@description('The name of the AI Services account to scope the role assignment to.') param aiFoundryName string @description('The principal type of the identity being assigned.') @allowed(['ServicePrincipal', 'User']) param principalType string = 'ServicePrincipal' -// Reference the existing AI Foundry resource in this resource group -resource aiFoundryAccount 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { +// Reference the existing AI Services resource in this resource group +resource aiServices 'Microsoft.CognitiveServices/accounts@2025-12-01' existing = { name: aiFoundryName } resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { name: roleAssignmentName - scope: aiFoundryAccount + scope: aiServices properties: { roleDefinitionId: roleDefinitionId principalId: principalId diff --git a/infra/bicep/modules/identity/managed-identity.bicep b/infra/bicep/modules/identity/managed-identity.bicep index f2d264ee9..e8accb80f 100644 --- a/infra/bicep/modules/identity/managed-identity.bicep +++ b/infra/bicep/modules/identity/managed-identity.bicep @@ -1,8 +1,9 @@ // ============================================================================ -// Module: Managed Identity -// Description: AVM wrapper for User-Assigned Managed Identity -// AVM Module: avm/res/managed-identity/user-assigned-identity -// Usage: Call this module once per identity from main.bicep +// Module: User-Assigned Managed Identity (Generic) +// Description: Creates a user-assigned managed identity. +// This module is NOT called from main.bicep by default. +// Use it when you need a user-assigned identity for specific scenarios +// (e.g., Container Apps, cross-tenant access, pre-provisioned RBAC). // ============================================================================ @description('Solution name used for resource naming.') @@ -17,33 +18,26 @@ param location string @description('Tags to apply to the resource.') param tags object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - // ============================================================================ -// AVM Module Deployment +// Resource Deployment // ============================================================================ -module managedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.5.0' = { - name: take('avm.res.managed-identity.user-assigned-identity.${identityName}', 64) - params: { - name: identityName - location: location - tags: tags - enableTelemetry: enableTelemetry - } +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: identityName + location: location + tags: tags } // ============================================================================ // Outputs // ============================================================================ @description('Resource ID of the managed identity.') -output resourceId string = managedIdentity.outputs.resourceId +output resourceId string = managedIdentity.id -@description('Principal ID of the managed identity.') -output principalId string = managedIdentity.outputs.principalId +@description('Principal ID (object ID) of the managed identity.') +output principalId string = managedIdentity.properties.principalId @description('Client ID of the managed identity.') -output clientId string = managedIdentity.outputs.clientId +output clientId string = managedIdentity.properties.clientId @description('Name of the managed identity.') -output name string = managedIdentity.outputs.name +output name string = managedIdentity.name diff --git a/infra/bicep/modules/identity/role-assignments.bicep b/infra/bicep/modules/identity/role-assignments.bicep index 9d04de79a..699a16fa7 100644 --- a/infra/bicep/modules/identity/role-assignments.bicep +++ b/infra/bicep/modules/identity/role-assignments.bicep @@ -28,6 +28,13 @@ param aiSearchPrincipalId string = '' @description('Principal ID of the backend App Service system-assigned identity (empty if not deployed).') param backendAppServicePrincipalId string = '' +@description('Principal ID of the deploying user (for user access roles).') +param deployerPrincipalId string = '' + +@description('Principal type of the deploying user.') +@allowed(['User', 'ServicePrincipal']) +param deployerPrincipalType string = 'User' + // --- Resource References --- @description('Resource ID of the AI Foundry account (empty if not deployed — new project path).') @@ -59,6 +66,7 @@ var roleDefinitions = { cognitiveServicesUser: 'a97b65f3-24c7-4388-baec-2e87135dc908' cognitiveServicesOpenAIUser: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' searchIndexDataReader: '1407120a-92aa-4202-b7e9-c0e197c71c8f' + searchIndexDataContributor: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' searchServiceContributor: '7ca78c08-252a-4471-8644-bb5ff32d4ba0' storageBlobDataContributor: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' storageBlobDataReader: '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' @@ -231,3 +239,65 @@ resource backendAppCosmosRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/s } } +// ============================================================================ +// 5. DEPLOYER (USER) ROLE ASSIGNMENTS +// Deploying user → AI Services, Search, Storage (Bicep-only) +// ============================================================================ + +// Deploying User → Cognitive Services User on AI Services +resource deployerAiServicesAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!useExistingAIProject && !empty(deployerPrincipalId) && !empty(aiFoundryResourceId)) { + scope: aiFoundryAccount + name: guid(solutionName, aiFoundryAccount.id, deployerPrincipalId, roleDefinitions.cognitiveServicesUser) + properties: { + principalId: deployerPrincipalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.cognitiveServicesUser) + principalType: deployerPrincipalType + } +} + +// Deploying User → Foundry User on AI Services +resource deployerAzureAIAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!useExistingAIProject && !empty(deployerPrincipalId) && !empty(aiFoundryResourceId)) { + scope: aiFoundryAccount + name: guid(solutionName, aiFoundryAccount.id, deployerPrincipalId, roleDefinitions.azureAiUser) + properties: { + principalId: deployerPrincipalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.azureAiUser) + principalType: deployerPrincipalType + } +} + +// Deploying User → Search Index Data Contributor on AI Search +resource deployerSearchIndexContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(deployerPrincipalId) && !empty(aiSearchResourceId)) { + scope: aiSearchService + name: guid(solutionName, aiSearchService.id, deployerPrincipalId, roleDefinitions.searchIndexDataContributor) + properties: { + principalId: deployerPrincipalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.searchIndexDataContributor) + principalType: deployerPrincipalType + } +} + +// Deploying User → Search Service Contributor on AI Search +resource deployerSearchServiceContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(deployerPrincipalId) && !empty(aiSearchResourceId)) { + scope: aiSearchService + name: guid(solutionName, aiSearchService.id, deployerPrincipalId, roleDefinitions.searchServiceContributor) + properties: { + principalId: deployerPrincipalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.searchServiceContributor) + principalType: deployerPrincipalType + } +} + +// Deploying User → Storage Blob Data Contributor +resource deployerStorageBlobContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!empty(deployerPrincipalId) && !empty(storageAccountResourceId)) { + scope: storageAccount + name: guid(solutionName, storageAccount.id, deployerPrincipalId, roleDefinitions.storageBlobDataContributor) + properties: { + principalId: deployerPrincipalId + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.storageBlobDataContributor) + principalType: deployerPrincipalType + } +} + +// NOTE: Deployer roles on existing AI Foundry (cross-scope) are assigned via +// 00_build_solution.py to avoid conflicts when the deployer already has the roles. diff --git a/infra/bicep/modules/monitoring/app-insights.bicep b/infra/bicep/modules/monitoring/app-insights.bicep index b726ae81d..21109d756 100644 --- a/infra/bicep/modules/monitoring/app-insights.bicep +++ b/infra/bicep/modules/monitoring/app-insights.bicep @@ -1,8 +1,8 @@ // ============================================================================ // Module: Application Insights -// Description: AVM wrapper for Application Insights with WAF alignment -// AVM Module: avm/res/insights/component:0.7.1 -// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/application-insights +// Description: Vanilla Bicep module for Application Insights +// Resource: Microsoft.Insights/components@2020-02-02 +// Docs: https://learn.microsoft.com/azure/templates/microsoft.insights/components // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -23,54 +23,53 @@ param workspaceResourceId string @description('Application type.') param applicationType string = 'web' -@description('Retention period in days. WAF recommends 365.') +@description('Retention period in days.') param retentionInDays int = 365 -@description('Disable IP masking for security. WAF recommends false.') +@description('Disable IP masking for security.') param disableIpMasking bool = false @description('Flow type for Application Insights.') param flowType string = 'Bluefield' -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - @description('Kind of Application Insights resource.') param kind string = 'web' // ============================================================================ -// AVM Module Deployment +// Resource // ============================================================================ -module appInsights 'br/public:avm/res/insights/component:0.7.1' = { - name: take('avm.res.insights.component.${name}', 64) - params: { - name: name - location: location - tags: tags - workspaceResourceId: workspaceResourceId - kind: kind - applicationType: applicationType - enableTelemetry: enableTelemetry - retentionInDays: retentionInDays - disableIpMasking: disableIpMasking - flowType: flowType + +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { + name: name + location: location + tags: tags + kind: kind + properties: { + Application_Type: applicationType + Flow_Type: flowType + WorkspaceResourceId: workspaceResourceId + RetentionInDays: retentionInDays + DisableIpMasking: disableIpMasking + publicNetworkAccessForIngestion: 'Enabled' + publicNetworkAccessForQuery: 'Enabled' } } // ============================================================================ // Outputs // ============================================================================ + @description('Resource ID of the Application Insights instance.') -output resourceId string = appInsights.outputs.resourceId +output resourceId string = appInsights.id @description('Name of the Application Insights instance.') -output name string = appInsights.outputs.name +output name string = appInsights.name @description('Instrumentation key for the Application Insights instance.') -output instrumentationKey string = appInsights.outputs.instrumentationKey +output instrumentationKey string = appInsights.properties.InstrumentationKey @description('Connection string for the Application Insights instance.') -output connectionString string = appInsights.outputs.connectionString +output connectionString string = appInsights.properties.ConnectionString @description('Application ID of the Application Insights instance.') -output applicationId string = appInsights.outputs.applicationId +output applicationId string = appInsights.properties.AppId diff --git a/infra/bicep/modules/monitoring/log-analytics.bicep b/infra/bicep/modules/monitoring/log-analytics.bicep index 3b231240c..87d79740c 100644 --- a/infra/bicep/modules/monitoring/log-analytics.bicep +++ b/infra/bicep/modules/monitoring/log-analytics.bicep @@ -1,8 +1,8 @@ // ============================================================================ // Module: Log Analytics Workspace -// Description: AVM wrapper for Log Analytics Workspace with WAF alignment -// AVM Module: avm/res/operational-insights/workspace:0.15.0 -// WAF: https://learn.microsoft.com/azure/well-architected/service-guides/azure-log-analytics +// Description: Vanilla Bicep module for Log Analytics Workspace +// Resource: Microsoft.OperationalInsights/workspaces@2023-09-01 +// Docs: https://learn.microsoft.com/azure/templates/microsoft.operationalinsights/workspaces // Note: This module only handles NEW workspace creation. // Existing workspace logic is handled in main.bicep. // ============================================================================ @@ -19,72 +19,40 @@ param location string @description('Tags to apply to the resource.') param tags object = {} -@description('Retention period in days. WAF recommends 365.') +@description('Retention period in days.') param retentionInDays int = 365 @description('SKU name for the workspace.') param skuName string = 'PerGB2018' -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - -// --- WAF: Private Networking --- -@description('Public network access for ingestion.') -param publicNetworkAccessForIngestion string = 'Enabled' - -@description('Public network access for query.') -param publicNetworkAccessForQuery string = 'Enabled' - -// --- WAF: Redundancy --- -@description('Enable workspace replication for redundancy.') -param enableReplication bool = false - -@description('Replication location (paired region).') -param replicationLocation string = '' - -@description('Daily quota in GB. WAF recommends 150 GB/day as starting point.') -param dailyQuotaGb string = '' - -// --- WAF: Monitoring (VM data sources for private networking) --- -@description('Data sources for VM monitoring (Windows events, perf counters).') -param dataSources array = [] - // ============================================================================ -// AVM Module Deployment +// Resource // ============================================================================ -module workspace 'br/public:avm/res/operational-insights/workspace:0.15.0' = { - name: take('avm.res.operational-insights.workspace.${name}', 64) - params: { - name: name - location: location - tags: tags - dataRetention: retentionInDays - skuName: skuName - enableTelemetry: enableTelemetry - features: { enableLogAccessUsingOnlyResourcePermissions: true } - diagnosticSettings: [{ useThisWorkspace: true }] - publicNetworkAccessForIngestion: publicNetworkAccessForIngestion - publicNetworkAccessForQuery: publicNetworkAccessForQuery - dailyQuotaGb: !empty(dailyQuotaGb) ? dailyQuotaGb : null - replication: enableReplication ? { - enabled: true - location: replicationLocation - } : null - dataSources: !empty(dataSources) ? dataSources : null + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: name + location: location + tags: tags + properties: { + retentionInDays: retentionInDays + sku: { + name: skuName + } } } // ============================================================================ // Outputs // ============================================================================ + @description('Resource ID of the Log Analytics workspace.') -output resourceId string = workspace.outputs.resourceId +output resourceId string = logAnalytics.id @description('Name of the Log Analytics workspace.') -output name string = workspace.outputs.name +output name string = logAnalytics.name @description('Location of the workspace.') -output location string = location +output location string = logAnalytics.location @description('Log Analytics workspace customer ID.') -output logAnalyticsWorkspaceId string = workspace.outputs.logAnalyticsWorkspaceId +output logAnalyticsWorkspaceId string = logAnalytics.properties.customerId diff --git a/infra/bicep/modules/monitoring/portal-dashboard.bicep b/infra/bicep/modules/monitoring/portal-dashboard.bicep index 5bf9148df..c5c08ec87 100644 --- a/infra/bicep/modules/monitoring/portal-dashboard.bicep +++ b/infra/bicep/modules/monitoring/portal-dashboard.bicep @@ -1,7 +1,8 @@ // ============================================================================ // Module: Portal Dashboard (Application Insights) -// Description: AVM wrapper for Azure Portal Dashboard -// AVM Module: avm/res/portal/dashboard:0.3.2 +// Description: Vanilla Bicep module for Azure Portal Dashboard +// Resource: Microsoft.Portal/dashboards@2025-04-01-preview +// Docs: https://learn.microsoft.com/azure/templates/microsoft.portal/dashboards // ============================================================================ @description('Solution name suffix used to derive the resource name.') @@ -22,21 +23,16 @@ param lenses array = [] @description('Dashboard metadata (time range, filters, etc.).') param metadata object = {} -@description('Optional. Enable/Disable usage telemetry for module.') -param enableTelemetry bool = true - // ============================================================================ -// AVM Module Deployment +// Resource // ============================================================================ -module dashboard 'br/public:avm/res/portal/dashboard:0.3.2' = { - name: take('avm.res.portal.dashboard.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry +resource dashboard 'Microsoft.Portal/dashboards@2025-04-01-preview' = { + name: name + location: location + tags: tags + properties: { lenses: lenses - metadata: !empty(metadata) ? metadata : null + metadata: !empty(metadata) ? metadata : {} } } @@ -44,10 +40,10 @@ module dashboard 'br/public:avm/res/portal/dashboard:0.3.2' = { // Outputs // ============================================================================ @description('Resource ID of the dashboard.') -output resourceId string = dashboard.outputs.resourceId +output resourceId string = dashboard.id @description('Name of the dashboard.') -output name string = dashboard.outputs.name +output name string = dashboard.name @description('Resource group the dashboard was deployed to.') -output resourceGroupName string = dashboard.outputs.resourceGroupName +output resourceGroupName string = resourceGroup().name diff --git a/infra/bicep/modules/security/key-vault.bicep b/infra/bicep/modules/security/key-vault.bicep index 924eea5b1..acd258db0 100644 --- a/infra/bicep/modules/security/key-vault.bicep +++ b/infra/bicep/modules/security/key-vault.bicep @@ -1,6 +1,8 @@ // ============================================================================ -// Module: Azure Key Vault (AVM) -// AVM Module: avm/res/key-vault/vault:0.12.1 +// Module: Azure Key Vault +// Description: Vanilla Bicep module for Azure Key Vault +// Resource: Microsoft.KeyVault/vaults@2023-07-01 +// Docs: https://learn.microsoft.com/azure/templates/microsoft.keyvault/vaults // ============================================================================ @description('Solution name used for naming convention.') @@ -35,63 +37,33 @@ param enablePurgeProtection bool = true @allowed(['Enabled', 'Disabled']) param publicNetworkAccess string = 'Enabled' -@description('Secrets to store in the vault (name/value pairs).') -param secrets array = [] - -@description('Enable Azure telemetry collection.') -param enableTelemetry bool = true - -@description('Role assignments.') -param roleAssignments array = [] - -@description('Enable private networking.') -param enablePrivateNetworking bool = false - -@description('Subnet resource ID for private endpoint.') -param privateEndpointSubnetId string = '' - -@description('Private DNS zone resource IDs.') -param privateDnsZoneResourceIds array = [] +@description('The Microsoft Entra tenant ID for the Key Vault.') +param tenantId string = subscription().tenantId // ============================================================================ -// Key Vault (AVM) +// Key Vault Resource // ============================================================================ -var secretItems = [for secret in secrets: { - name: secret.name - value: secret.value -}] - -var dnsZoneConfigs = [for (zoneId, i) in privateDnsZoneResourceIds: { - name: 'config${i}' - privateDnsZoneResourceId: zoneId -}] - -var privateEndpointConfig = enablePrivateNetworking && !empty(privateEndpointSubnetId) ? [ - { - subnetResourceId: privateEndpointSubnetId - privateDnsZoneGroup: !empty(privateDnsZoneResourceIds) ? { - privateDnsZoneGroupConfigs: dnsZoneConfigs - } : null - } -] : [] - -module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = { - name: take('avm.res.keyvault.vault.${name}', 64) - params: { - name: name - location: location - tags: tags - enableTelemetry: enableTelemetry - sku: sku +resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = { + name: name + location: location + tags: tags + properties: { + tenantId: tenantId + sku: { + family: 'A' + name: sku + } + accessPolicies: [] enableRbacAuthorization: enableRbacAuthorization enableSoftDelete: enableSoftDelete softDeleteRetentionInDays: softDeleteRetentionInDays enablePurgeProtection: enablePurgeProtection publicNetworkAccess: publicNetworkAccess - roleAssignments: !empty(roleAssignments) ? roleAssignments : [] - secrets: !empty(secrets) ? secretItems : [] - privateEndpoints: privateEndpointConfig + networkAcls: { + bypass: 'AzureServices' + defaultAction: publicNetworkAccess == 'Disabled' ? 'Deny' : 'Allow' + } } } @@ -99,11 +71,11 @@ module keyVault 'br/public:avm/res/key-vault/vault:0.12.1' = { // Outputs // ============================================================================ -@description('The name of the key vault.') -output name string = keyVault.outputs.name +@description('The name of the Key Vault.') +output name string = keyVault.name -@description('The URI of the key vault.') -output uri string = keyVault.outputs.uri +@description('The URI of the Key Vault.') +output uri string = keyVault.properties.vaultUri -@description('The resource ID of the key vault.') -output resourceId string = keyVault.outputs.resourceId +@description('The resource ID of the Key Vault.') +output resourceId string = keyVault.id