diff --git a/README.md b/README.md
index 78f922d8..2a7f038d 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ The Modernize your code solution accelerator allows users to specify a group of
-
+
[**SOLUTION OVERVIEW**](#solution-overview) \| [**QUICK DEPLOY**](#quick-deploy) \| [**BUSINESS SCENARIO**](#business-scenario) \| [**SUPPORTING DOCUMENTATION**](#supporting-documentation)
@@ -24,7 +24,10 @@ The solution leverages Azure AI Foundry, Azure OpenAI Service, Azure Container A
||
|---|
+This architecture will be deployed with the 'sandbox' setting of our deployment process. Optionally you can deploy [Well-Architected Framework (WAF) aligned](https://learn.microsoft.com/en-us/azure/well-architected/) architecture, described in [WAF-Aligned Solution Architecture](./docs/ArchitectureWAF.md), with the WAF-Aligned deployment option described in [Deployment Guide](./docs/DeploymentGuide.md).
+
### Agentic architecture
+
||
|---|
@@ -51,16 +54,16 @@ If you'd like to customize the solution accelerator, here are some common areas
Click to learn more about the key features this solution enables
- **Code language modernization**
- Modernizing outdated code ensures compatibility with current technologies, reduces reliance on legacy expertise, and keeps businesses competitive.
+ Modernizing outdated code ensures compatibility with current technologies, reduces reliance on legacy expertise, and keeps businesses competitive.
- **Summary and review of new code**
- Generating summaries and translating code files keeps humans in the loop, enhances their understanding, and facilitates timely interventions, ensuring the files are ready to export.
+ Generating summaries and translating code files keeps humans in the loop, enhances their understanding, and facilitates timely interventions, ensuring the files are ready to export.
- **Business logic analysis**
- Leveraging AI to decipher business logic from legacy code helps minimizes the risk of human error.
+ Leveraging AI to decipher business logic from legacy code helps minimizes the risk of human error.
- **Efficient code transformation**
- Streamlining the process of analyzing, converting, and iterative error testing reduces time and effort required to modernize the systems.
+ Streamlining the process of analyzing, converting, and iterative error testing reduces time and effort required to modernize the systems.
@@ -77,7 +80,7 @@ Follow the quick deploy steps on the deployment guide to deploy this solution to
| [](https://codespaces.new/microsoft/Modernize-your-Code-Solution-Accelerator) | [](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/microsoft/Modernize-your-Code-Solution-Accelerator) |
|---|---|
-
+
> ⚠️ **Important: Check Azure OpenAI Quota Availability**
@@ -141,19 +144,19 @@ The sample data used in this repository is synthetic and generated using Azure O
Click to learn more about what value this solution provides
- **Accelerated Migration**
- Automate the translation of SQL queries, significantly reducing migration time and effort.
+ Automate the translation of SQL queries, significantly reducing migration time and effort.
- **Error Reduction**
- Multi-agent validation ensures accurate translations and maintains data integrity.
+ Multi-agent validation ensures accurate translations and maintains data integrity.
- **Knowledge Preservation**
- Captures and preserves business logic during the modernization process.
+ Captures and preserves business logic during the modernization process.
- **Cost Efficiency**
- Reduces reliance on specialized legacy system expertise and manual translation efforts.
+ Reduces reliance on specialized legacy system expertise and manual translation efforts.
- **Standardization**
- Ensures consistent query translation across the organization.
+ Ensures consistent query translation across the organization.
diff --git a/docs/ArchitectureWAF.md b/docs/ArchitectureWAF.md
new file mode 100644
index 00000000..8ae2da55
--- /dev/null
+++ b/docs/ArchitectureWAF.md
@@ -0,0 +1,59 @@
+# Azure WAF-Aligned Architecture
+
+This architecture implements [Azure Well-Architected Framework (WAF)](https://learn.microsoft.com/en-us/azure/well-architected/) principles for enterprise-grade deployments, deployed with the WAF-Aligned deployment option:
+
+
+
+## WAF Pillars Implementation
+
+### Security
+- **Zero Trust Network:** Private VNet with private endpoints for all PaaS services
+- **Identity & Access:** Managed identities with RBAC and least-privilege access
+- **Secure Admin Access:** Azure Bastion + Jumpbox for internal administration
+- **Secrets Management:** Azure Key Vault integration
+
+### Operational Excellence
+- **Observability:** Centralized logging via Log Analytics Workspace
+- **Application Monitoring:** Application Insights for telemetry and diagnostics
+- **Infrastructure as Code:** Bicep templates with parameterized configurations
+
+### Performance Efficiency
+- **Auto-scaling:** Container Apps with configurable scaling policies
+- **Regional Proximity:** Resources deployed in optimal Azure regions
+
+### Cost Optimization
+- **Right-sizing:** Parameterized SKUs and capacity settings
+- **Resource Sharing:** Shared networking and monitoring infrastructure
+
+### Reliability
+- **High Availability:** Multi-zone deployment options
+- **Data Redundancy:** Configurable geo-replication for critical data stores
+- **Private Connectivity:** Eliminates internet dependencies
+
+## Core Architecture Components
+
+| Component | Purpose | WAF Alignment |
+|-----------|---------|---------------|
+| **Virtual Network** | Network isolation boundary | Security, Reliability |
+| **Private Endpoints** | Secure PaaS connectivity (AI Services, Storage, Cosmos DB, Key Vault) | Security |
+| **Private DNS Zones** | Internal name resolution | Security, Reliability |
+| **Azure Bastion + Jumpbox** | Secure administrative access | Security |
+| **Container Apps** | Application hosting with VNet integration | Performance, Reliability |
+| **Log Analytics + App Insights** | Centralized monitoring and diagnostics | Operational Excellence |
+
+## Deployment Configuration
+- **Parameter File:** `infra/main.waf-aligned.bicepparam` - Controls all WAF features
+- **Network-first Design:** All components deployed within private network boundaries
+- **Enterprise-ready:** Production-grade security and monitoring enabled
+
+## Application Information Flow
+
+The application information flow remains the same for both 'sandbox' and 'waf-aligned' configuration.
+
+The solution is composed of several services:
+
+- The web app front end and the backend app logic are containerized and run from Azure Container service instances.
+- When a request for conversion is created in the web app admin console, the user specifies what files should be converted and the target SQL dialect for conversion.
+- These files are then uploaded to blob storage and initial data about the request is stored in Cosmos DB.
+- The conversion takes place using appropriate LLM models using multiple agents, with each agent having a dedicated purpose in the conversion process. As files are converted, they are placed into blob storage, with metadata collected into Cosmos detailing the conversion process and the current state of the batch.
+- Cosmos also stores the logs from the individual agents so the results can be fully reviewed before any of the converted files are put into production.
diff --git a/docs/CmsaArchitectureSource.pptx b/docs/CmsaArchitectureSource.pptx
new file mode 100644
index 00000000..92707ff7
Binary files /dev/null and b/docs/CmsaArchitectureSource.pptx differ
diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md
index 3c610182..d782cc35 100644
--- a/docs/DeploymentGuide.md
+++ b/docs/DeploymentGuide.md
@@ -18,16 +18,15 @@ Here are some example regions where the services are available: East US, East US
| [](https://codespaces.new/microsoft/Modernize-your-Code-Solution-Accelerator) | [](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/microsoft/Modernize-your-Code-Solution-Accelerator) |
|---|---|
-
+
### **Configurable Deployment Settings**
When you start the deployment, most parameters will have **default values**, but you can update the following settings by following the steps [here](../docs/CustomizingAzdParameters.md):
| **Setting** | **Description** | **Default value** |
|------------|----------------| ------------|
-| **Azure Region** | The region where resources will be created. | East US|
-| **Resource Prefix** | Prefix for all resources created by this template. This prefix will be used to create unique names for all resources. The prefix must be unique within the resource group. | None |
-| **AI Location** | Location for all AI services resources. This location can be different from the resource group location | None |
+| **Azure Region** | The region where resources will be created. | None |
+| **SolutionName** | Text/String used for part of all resources created by this template. | None |
| **Capacity** | Configure capacity for **gpt-4o**. | 5k |
This accelerator can be configured to use authentication.
@@ -42,7 +41,29 @@ By default, the **GPT model capacity** in deployment is set to **5k tokens**.
To adjust quota settings, follow these [steps](../docs/AzureGPTQuotaSettings.md)
-### Deployment Options
+### Deployment Options & Steps
+### Sandbox or WAF Aligned Deployment Options
+
+The [`infra`](../infra) folder contains the [`main.bicep`](../infra/main.bicep) Bicep script, which defines all Azure infrastructure components for this solution.
+
+By default, the `azd up` command uses the [`main.bicepparam`](../infra/main.bicepparam) file to deploy the solution. This file is pre-configured for a **sandbox environment** — ideal for development and proof-of-concept scenarios, with minimal security and cost controls for rapid iteration.
+
+For **production deployments**, the repository also provides [`main.waf-aligned.bicepparam`](../infra/main.waf-aligned.bicepparam), which applies a [WAF-aligned](https://learn.microsoft.com/en-us/azure/well-architected/) configuration. This option enables additional Azure best practices for reliability, security, cost optimization, operational excellence, and performance efficiency, such as:
+
+- Enhanced network security (e.g., Network protection with private endpoints)
+- Stricter access controls and managed identities
+- Logging, monitoring, and diagnostics enabled by default
+- Resource tagging and cost management recommendations
+
+**How to choose your deployment configuration:**
+
+- Use the default [`main.bicepparam`](../infra/main.bicepparam) for a sandbox/dev environment.
+- For a WAF-aligned, production-ready deployment, copy the contents of [`main.waf-aligned.bicepparam`](../infra/main.waf-aligned.bicepparam) into `main.bicepparam` before running `azd up`.
+
+> [!TIP]
+> Always review and adjust parameter values (such as region, capacity, security settings and log analytics workspace configuration) to match your organization’s requirements before deploying. For production, ensure you have sufficient quota and follow the principle of least privilege for all identities and role assignments.
+
+
Pick from the options below to see step-by-step instructions for: GitHub Codespaces, VS Code Dev Containers, Local Environments, and Bicep deployments.
@@ -114,23 +135,28 @@ To change the azd parameters from the default values, follow the steps [here](..
1. Login to Azure:
- ```shell
- azd auth login
- ```
+ ```shell
+ azd auth login
+ ```
+
+ #### Note: To authenticate with Azure Developer CLI (`azd`) to a specific tenant, use the previous command with your **Tenant ID**:
+
+ ```sh
+ azd auth login --tenant-id
+ ```
- #### Note: To authenticate with Azure Developer CLI (`azd`) to a specific tenant, use the previous command with your **Tenant ID**:
+2. Provide an `azd` environment name (like "cmsaapp")
- ```sh
- azd auth login --tenant-id
+ ```sh
+ azd env new
```
-2. Provision and deploy all the resources:
+3. Provision and deploy all the resources:
```shell
azd up
```
-3. Provide an `azd` environment name (like "cmsaapp")
4. Select a subscription from your Azure account, and select a location which has quota for all the resources.
* This deployment will take *6-9 minutes* to provision the resources in your account and set up the solution with sample data.
* If you get an error or timeout with deployment, changing the location can help, as there may be availability constraints for the resources.
diff --git a/docs/images/read_me/solArchitectureWAF.png b/docs/images/read_me/solArchitectureWAF.png
new file mode 100644
index 00000000..673657a3
Binary files /dev/null and b/docs/images/read_me/solArchitectureWAF.png differ
diff --git a/infra/main.bicep b/infra/main.bicep
index 2e5d476f..483545bc 100644
--- a/infra/main.bicep
+++ b/infra/main.bicep
@@ -1,14 +1,19 @@
+metadata name = 'Modernize Your Code Solution Accelerator'
+metadata description = '''CSA CTO Gold Standard Solution Accelerator for Modernize Your Code.
+'''
+
@minLength(3)
@maxLength(16)
-@description('A unique application/solution name for all resources in this deployment. This should be 3-16 characters long.')
+@description('Required. A unique application/solution name for all resources in this deployment. This should be 3-16 characters long.')
param solutionName string
@maxLength(5)
-@description('A unique token for the solution. This is used to ensure resource names are unique for global resources. Defaults to a 5-character substring of the unique string generated from the subscription ID, resource group name, and solution name.')
+@description('Optional. A unique token for the solution. This is used to ensure resource names are unique for global resources. Defaults to a 5-character substring of the unique string generated from the subscription ID, resource group name, and solution name.')
param solutionUniqueToken string = substring(uniqueString(subscription().id, resourceGroup().name, solutionName), 0, 5)
@minLength(3)
-@description('Azure region for all services.')
+@metadata({ azd: { type: 'location' } })
+@description('Optional. Azure region for all services. Defaults to the resource group location.')
param location string = resourceGroup().location
@allowed([
@@ -36,22 +41,26 @@ param location string = resourceGroup().location
'westus'
'westus3'
])
-@description('Location for all AI service resources. This location can be different from the resource group location.')
+@metadata({ azd: { type: 'location' } })
+
+
+@description('Optional. Location for all AI service resources. This location can be different from the resource group location.')
param azureAiServiceLocation string = location
-@description('AI model deployment token capacity. Defaults to 5K tokens per minute.')
-param capacity int = 5
-@description('Enable monitoring for the resources. This will enable Application Insights and Log Analytics. Defaults to false.')
+@description('Optional. AI model deployment token capacity. Defaults to 5K tokens per minute.')
+param capacity int = 5 // was 5 before = 5K
+
+@description('Optional. Enable monitoring for the resources. This will enable Application Insights and Log Analytics. Defaults to false.')
param enableMonitoring bool = false
-@description('Enable scaling for the container apps. Defaults to false.')
+@description('Optional. Enable scaling for the container apps. Defaults to false.')
param enableScaling bool = false
-@description('Enable redundancy for applicable resources. Defaults to false.')
+@description('Optional. Enable redundancy for applicable resources. Defaults to false.')
param enableRedundancy bool = false
-@description('Optional. The secondary location for the Cosmos DB account if redundancy is enabled. Defaults to false.')
+@description('Optional. The secondary location for the Cosmos DB account if redundancy is enabled.')
param secondaryLocation string?
@description('Optional. Enable private networking for the resources. Set to true to enable private networking. Defaults to false.')
@@ -59,11 +68,13 @@ param enablePrivateNetworking bool = false
@description('Optional. Admin username for the Jumpbox Virtual Machine. Set to custom value if enablePrivateNetworking is true.')
@secure()
-param vmAdminUsername string
+//param vmAdminUsername string = take(newGuid(), 20)
+param vmAdminUsername string?
@description('Optional. Admin password for the Jumpbox Virtual Machine. Set to custom value if enablePrivateNetworking is true.')
@secure()
-param vmAdminPassword string
+//param vmAdminPassword string = newGuid()
+param vmAdminPassword string?
@description('Optional. Specifies the resource tags for all the resources. Tag "azd-env-name" is automatically added to all resources.')
param tags object = {}
@@ -71,13 +82,24 @@ param tags object = {}
@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true
-var allTags = union({
- 'azd-env-name': solutionName
-}, tags)
+var allTags = union(
+ {
+ 'azd-env-name': solutionName
+ },
+ tags
+)
-var resourcesName = trim(replace(replace(replace(replace(replace('${solutionName}${solutionUniqueToken}', '-', ''), '_', ''), '.', ''),'/', ''), ' ', ''))
+var resourcesName = toLower(trim(replace(
+ replace(
+ replace(replace(replace(replace('${solutionName}${solutionUniqueToken}', '-', ''), '_', ''), '.', ''), '/', ''),
+ ' ',
+ ''
+ ),
+ '*',
+ ''
+)))
-var modelDeployment = {
+var modelDeployment = {
name: 'gpt-4o'
model: {
name: 'gpt-4o'
@@ -91,20 +113,32 @@ var modelDeployment = {
raiPolicyName: 'Microsoft.Default'
}
-module appIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = {
- name: take('identity-app-${resourcesName}-deployment', 64)
- params: {
- name: 'id-app-${resourcesName}'
- location: location
- tags: allTags
- enableTelemetry: enableTelemetry
+#disable-next-line no-deployments-resources
+resource avmTelemetry 'Microsoft.Resources/deployments@2024-03-01' = if (enableTelemetry) {
+ name: take(
+ '46d3xbcp.ptn.sa-modernizeyourcode.${replace('-..--..-', '.', '-')}.${substring(uniqueString(deployment().name, location), 0, 4)}',
+ 64
+ )
+ properties: {
+ mode: 'Incremental'
+ template: {
+ '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
+ contentVersion: '1.0.0.0'
+ resources: []
+ outputs: {
+ telemetry: {
+ type: 'String'
+ value: 'For more information, see https://aka.ms/avm/TelemetryInfo'
+ }
+ }
+ }
}
}
-module aiFoundryIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = {
- name: take('identity-proj-${resourcesName}-deployment', 64)
+module appIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.1' = {
+ name: take('identity-app-${resourcesName}-deployment', 64)
params: {
- name: 'id-proj-${resourcesName}'
+ name: 'id-app-${resourcesName}'
location: location
tags: allTags
enableTelemetry: enableTelemetry
@@ -116,7 +150,7 @@ module logAnalyticsWorkspace 'br/public:avm/res/operational-insights/workspace:0
params: {
name: 'log-${resourcesName}'
location: location
- skuName: 'PerGB2018'
+ skuName: 'PerGB2018'
dataRetention: 30
diagnosticSettings: [{ useThisWorkspace: true }]
tags: allTags
@@ -141,15 +175,15 @@ module network 'modules/network.bicep' = if (enablePrivateNetworking) {
params: {
resourcesName: resourcesName
logAnalyticsWorkSpaceResourceId: logAnalyticsWorkspace.outputs.resourceId
- vmAdminUsername: vmAdminUsername
- vmAdminPassword: vmAdminPassword
+ vmAdminUsername: vmAdminUsername ?? 'JumpboxAdminUser'
+ vmAdminPassword: vmAdminPassword ?? 'JumpboxAdminP@ssw0rd1234!'
location: location
tags: allTags
enableTelemetry: enableTelemetry
}
}
-module aiServices 'modules/aiServices.bicep' = {
+module aiServices 'modules/ai-foundry/main.bicep' = {
name: take('aiservices-${resourcesName}-deployment', 64)
#disable-next-line no-unnecessary-dependson
dependsOn: [logAnalyticsWorkspace, network] // required due to optional flags that could change dependency
@@ -159,17 +193,14 @@ module aiServices 'modules/aiServices.bicep' = {
sku: 'S0'
kind: 'AIServices'
deployments: [modelDeployment]
+ projectName: 'proj-${resourcesName}'
logAnalyticsWorkspaceResourceId: enableMonitoring ? logAnalyticsWorkspace.outputs.resourceId : ''
- // TODO - add back when container apps can properly access AI Services via Foundry Project over private endpoint
- // Issue: When private endpoint is enabled for OpenAI, the container app cannot access the AI Services endpoint through the Foundry project connection string.
- // Request: POST /api/start-processing
- // Response: ERROR:sql_agents.agents.agent_base:Error creating agent definition: (403) Public access is disabled. Please configure private endpoint.
- // ---------------------
- // privateNetworking: enablePrivateNetworking ? {
- // virtualNetworkResourceId: network.outputs.vnetResourceId
- // subnetResourceId: first(filter(network.outputs.subnets, s => s.name == 'peps')).resourceId
- // } : null
- // ---------------------
+ privateNetworking: enablePrivateNetworking
+ ? {
+ virtualNetworkResourceId: network.outputs.vnetResourceId
+ subnetResourceId: network.outputs.subnetPrivateEndpointsResourceId
+ }
+ : null
roleAssignments: [
{
principalId: appIdentity.outputs.principalId
@@ -177,9 +208,14 @@ module aiServices 'modules/aiServices.bicep' = {
roleDefinitionIdOrName: 'Cognitive Services OpenAI Contributor'
}
{
- principalId: aiFoundryIdentity.outputs.principalId
+ principalId: appIdentity.outputs.principalId
principalType: 'ServicePrincipal'
- roleDefinitionIdOrName: 'Cognitive Services OpenAI Contributor'
+ roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' // Azure AI Developer
+ }
+ {
+ principalId: appIdentity.outputs.principalId
+ principalType: 'ServicePrincipal'
+ roleDefinitionIdOrName: '53ca6127-db72-4b80-b1b0-d745d6d5456d' // Azure AI User
}
]
tags: allTags
@@ -199,18 +235,20 @@ module storageAccount 'modules/storageAccount.bicep' = {
tags: allTags
skuName: enableRedundancy ? 'Standard_GZRS' : 'Standard_LRS'
logAnalyticsWorkspaceResourceId: enableMonitoring ? logAnalyticsWorkspace.outputs.resourceId : ''
- privateNetworking: enablePrivateNetworking ? {
- virtualNetworkResourceId: network.outputs.vnetResourceId
- subnetResourceId: network.outputs.subnetPrivateEndpointsResourceId
- } : null
+ privateNetworking: enablePrivateNetworking
+ ? {
+ virtualNetworkResourceId: network.outputs.vnetResourceId
+ subnetResourceId: network.outputs.subnetPrivateEndpointsResourceId
+ }
+ : null
containers: [
- {
- name: appStorageContainerName
- properties: {
- publicAccess: 'None'
- }
+ {
+ name: appStorageContainerName
+ properties: {
+ publicAccess: 'None'
}
- ]
+ }
+ ]
roleAssignments: [
{
principalId: appIdentity.outputs.principalId
@@ -231,13 +269,15 @@ module keyVault 'modules/keyVault.bicep' = {
location: location
sku: 'standard'
logAnalyticsWorkspaceResourceId: enableMonitoring ? logAnalyticsWorkspace.outputs.resourceId : ''
- privateNetworking: enablePrivateNetworking ? {
- virtualNetworkResourceId: network.outputs.vnetResourceId
- subnetResourceId: network.outputs.subnetPrivateEndpointsResourceId
- } : null
+ privateNetworking: enablePrivateNetworking
+ ? {
+ virtualNetworkResourceId: network.outputs.vnetResourceId
+ subnetResourceId: network.outputs.subnetPrivateEndpointsResourceId
+ }
+ : null
roleAssignments: [
{
- principalId: aiFoundryIdentity.outputs.principalId
+ principalId: aiServices.outputs.?systemAssignedMIPrincipalId ?? ''
principalType: 'ServicePrincipal'
roleDefinitionIdOrName: 'Key Vault Reader'
}
@@ -247,51 +287,23 @@ module keyVault 'modules/keyVault.bicep' = {
}
}
-module azureAifoundry 'modules/aiFoundry.bicep' = {
- name: take('aifoundry-${resourcesName}-deployment', 64)
- #disable-next-line no-unnecessary-dependson
- dependsOn: [logAnalyticsWorkspace, network] // required due to optional flags that could change dependency
- params: {
- location: azureAiServiceLocation
- hubName: 'hub-${resourcesName}'
- hubDescription: 'AI Hub for Modernize Your Code'
- projectName: 'proj-${resourcesName}'
- storageAccountResourceId: storageAccount.outputs.resourceId
- keyVaultResourceId: keyVault.outputs.resourceId
- userAssignedIdentityResourceId: aiFoundryIdentity.outputs.resourceId
- logAnalyticsWorkspaceResourceId: enableMonitoring ? logAnalyticsWorkspace.outputs.resourceId : ''
- aiServicesName: aiServices.outputs.name
- privateNetworking: enablePrivateNetworking ? {
- virtualNetworkResourceId: network.outputs.vnetResourceId
- subnetResourceId: network.outputs.subnetPrivateEndpointsResourceId
- } : null
- roleAssignments: [
- {
- principalId: appIdentity.outputs.principalId
- principalType: 'ServicePrincipal'
- roleDefinitionIdOrName: '64702f94-c441-49e6-a78b-ef80e0188fee' // Azure AI Developer
- }
- ]
- tags: allTags
- enableTelemetry: enableTelemetry
- }
-}
-
module cosmosDb 'modules/cosmosDb.bicep' = {
name: take('cosmos-${resourcesName}-deployment', 64)
#disable-next-line no-unnecessary-dependson
dependsOn: [logAnalyticsWorkspace, network] // required due to optional flags that could change dependency
params: {
- name: 'cosmos-${resourcesName}'
+ name: take('cosmos-${resourcesName}', 44)
location: location
dataAccessIdentityPrincipalId: appIdentity.outputs.principalId
logAnalyticsWorkspaceResourceId: enableMonitoring ? logAnalyticsWorkspace.outputs.resourceId : ''
zoneRedundant: enableRedundancy
secondaryLocation: enableRedundancy && !empty(secondaryLocation) ? secondaryLocation : ''
- privateNetworking: enablePrivateNetworking ? {
- virtualNetworkResourceId: network.outputs.vnetResourceId
- subnetResourceId: network.outputs.subnetPrivateEndpointsResourceId
- } : null
+ privateNetworking: enablePrivateNetworking
+ ? {
+ virtualNetworkResourceId: network.outputs.vnetResourceId
+ subnetResourceId: network.outputs.subnetPrivateEndpointsResourceId
+ }
+ : null
tags: allTags
enableTelemetry: enableTelemetry
}
@@ -316,28 +328,35 @@ module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.11.
]
}
appInsightsConnectionString: enableMonitoring ? applicationInsights.outputs.connectionString : null
- appLogsConfiguration: enableMonitoring ? {
- destination: 'log-analytics'
- logAnalyticsConfiguration: {
- customerId: logAnalyticsWorkspace.outputs.logAnalyticsWorkspaceId
- sharedKey: logAnalyticsWorkspace.outputs.primarySharedKey
- }
- } : {}
- workloadProfiles: enablePrivateNetworking ? [ // NOTE: workload profiles are required for private networking
- {
- name: 'Consumption'
- workloadProfileType: 'Consumption'
- }
- ] : []
+ appLogsConfiguration: enableMonitoring
+ ? {
+ destination: 'log-analytics'
+ logAnalyticsConfiguration: {
+ customerId: logAnalyticsWorkspace.outputs.logAnalyticsWorkspaceId
+ sharedKey: logAnalyticsWorkspace.outputs.primarySharedKey
+ }
+ }
+ : {}
+ workloadProfiles: enablePrivateNetworking
+ ? [
+ // NOTE: workload profiles are required for private networking
+ {
+ name: 'Consumption'
+ workloadProfileType: 'Consumption'
+ }
+ ]
+ : []
tags: allTags
enableTelemetry: enableTelemetry
}
}
-module containerAppFrontend 'br/public:avm/res/app/container-app:0.17.0' = {
- name: take('container-app-frontend-${resourcesName}-deployment', 64)
+module containerAppBackend 'br/public:avm/res/app/container-app:0.17.0' = {
+ name: take('container-app-backend-${resourcesName}-deployment', 64)
+ #disable-next-line no-unnecessary-dependson
+ dependsOn: [applicationInsights] // required due to optional flags that could change dependency
params: {
- name: take('ca-${resourcesName}frontend', 32)
+ name: take('ca-${resourcesName}backend', 32)
location: location
environmentResourceId: containerAppsEnvironment.outputs.resourceId
managedIdentities: {
@@ -347,47 +366,162 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.17.0' = {
}
containers: [
{
- env: [
- {
- name: 'API_URL'
- value: 'https://${containerAppBackend.outputs.fqdn}'
- }
- ]
- image: 'cmsacontainerreg.azurecr.io/cmsafrontend:latest'
- name: 'cmsafrontend'
+ name: 'cmsabackend'
+ image: 'cmsacontainerreg.azurecr.io/cmsabackend:latest'
+ env: concat(
+ [
+ {
+ name: 'COSMOSDB_ENDPOINT'
+ value: cosmosDb.outputs.endpoint
+ }
+ {
+ name: 'COSMOSDB_DATABASE'
+ value: cosmosDb.outputs.databaseName
+ }
+ {
+ name: 'COSMOSDB_BATCH_CONTAINER'
+ value: cosmosDb.outputs.containerNames.batch
+ }
+ {
+ name: 'COSMOSDB_FILE_CONTAINER'
+ value: cosmosDb.outputs.containerNames.file
+ }
+ {
+ name: 'COSMOSDB_LOG_CONTAINER'
+ value: cosmosDb.outputs.containerNames.log
+ }
+ {
+ name: 'AZURE_BLOB_ACCOUNT_NAME'
+ value: storageAccount.outputs.name
+ }
+ {
+ name: 'AZURE_BLOB_CONTAINER_NAME'
+ value: appStorageContainerName
+ }
+ {
+ name: 'AZURE_OPENAI_ENDPOINT'
+ value: 'https://${aiServices.outputs.name}.openai.azure.com/'
+ }
+ {
+ name: 'MIGRATOR_AGENT_MODEL_DEPLOY'
+ value: modelDeployment.name
+ }
+ {
+ name: 'PICKER_AGENT_MODEL_DEPLOY'
+ value: modelDeployment.name
+ }
+ {
+ name: 'FIXER_AGENT_MODEL_DEPLOY'
+ value: modelDeployment.name
+ }
+ {
+ name: 'SEMANTIC_VERIFIER_AGENT_MODEL_DEPLOY'
+ value: modelDeployment.name
+ }
+ {
+ name: 'SYNTAX_CHECKER_AGENT_MODEL_DEPLOY'
+ value: modelDeployment.name
+ }
+ {
+ name: 'SELECTION_MODEL_DEPLOY'
+ value: modelDeployment.name
+ }
+ {
+ name: 'TERMINATION_MODEL_DEPLOY'
+ value: modelDeployment.name
+ }
+ {
+ name: 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME'
+ value: modelDeployment.name
+ }
+ {
+ name: 'AI_PROJECT_ENDPOINT'
+ value: aiServices.outputs.project.apiEndpoint // or equivalent
+ }
+ {
+ name: 'AZURE_AI_AGENT_PROJECT_CONNECTION_STRING' // This was not really used in code.
+ value: aiServices.outputs.project.apiEndpoint
+ }
+ {
+ name: 'AZURE_AI_AGENT_PROJECT_NAME'
+ value: aiServices.outputs.project.name
+ }
+ {
+ name: 'AZURE_AI_AGENT_RESOURCE_GROUP_NAME'
+ value: resourceGroup().name
+ }
+ {
+ name: 'AZURE_AI_AGENT_SUBSCRIPTION_ID'
+ value: subscription().subscriptionId
+ }
+ {
+ name: 'AZURE_AI_AGENT_ENDPOINT'
+ value: aiServices.outputs.project.apiEndpoint
+ }
+ {
+ name: 'AZURE_CLIENT_ID'
+ value: appIdentity.outputs.clientId // NOTE: This is the client ID of the managed identity, not the Entra application, and is needed for the App Service to access the Cosmos DB account.
+ }
+ ],
+ enableMonitoring
+ ? [
+ {
+ name: 'APPLICATIONINSIGHTS_INSTRUMENTATION_KEY'
+ value: applicationInsights.outputs.instrumentationKey
+ }
+ {
+ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
+ value: applicationInsights.outputs.connectionString
+ }
+ ]
+ : []
+ )
resources: {
- cpu: '1'
+ cpu: 1
memory: '2.0Gi'
}
+ probes: enableMonitoring
+ ? [
+ {
+ httpGet: {
+ path: '/health'
+ port: 8000
+ }
+ initialDelaySeconds: 3
+ periodSeconds: 3
+ type: 'Liveness'
+ }
+ ]
+ : []
}
]
- ingressTargetPort: 3000
+ ingressTargetPort: 8000
ingressExternal: true
scaleSettings: {
maxReplicas: enableScaling ? 3 : 1
minReplicas: 1
- rules: enableScaling ? [
- {
- name: 'http-scaler'
- http: {
- metadata: {
- concurrentRequests: 100
+ rules: enableScaling
+ ? [
+ {
+ name: 'http-scaler'
+ http: {
+ metadata: {
+ concurrentRequests: 100
+ }
+ }
}
- }
- }
- ] : []
+ ]
+ : []
}
tags: allTags
enableTelemetry: enableTelemetry
}
}
-module containerAppBackend 'br/public:avm/res/app/container-app:0.17.0' = {
- name: take('container-app-backend-${resourcesName}-deployment', 64)
- #disable-next-line no-unnecessary-dependson
- dependsOn: [applicationInsights] // required due to optional flags that could change dependency
+module containerAppFrontend 'br/public:avm/res/app/container-app:0.17.0' = {
+ name: take('container-app-frontend-${resourcesName}-deployment', 64)
params: {
- name: take('ca-${resourcesName}backend', 32)
+ name: take('ca-${resourcesName}frontend', 32)
location: location
environmentResourceId: containerAppsEnvironment.outputs.resourceId
managedIdentities: {
@@ -397,143 +531,42 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.17.0' = {
}
containers: [
{
- name: 'cmsabackend'
- image: 'cmsacontainerreg.azurecr.io/cmsabackend:latest'
- env: concat([
- {
- name: 'COSMOSDB_ENDPOINT'
- value: cosmosDb.outputs.endpoint
- }
- {
- name: 'COSMOSDB_DATABASE'
- value: cosmosDb.outputs.databaseName
- }
- {
- name: 'COSMOSDB_BATCH_CONTAINER'
- value: cosmosDb.outputs.containers.batch.name
- }
- {
- name: 'COSMOSDB_FILE_CONTAINER'
- value: cosmosDb.outputs.containers.file.name
- }
- {
- name: 'COSMOSDB_LOG_CONTAINER'
- value: cosmosDb.outputs.containers.log.name
- }
- {
- name: 'AZURE_BLOB_ACCOUNT_NAME'
- value: storageAccount.outputs.name
- }
- {
- name: 'AZURE_BLOB_CONTAINER_NAME'
- value: appStorageContainerName
- }
- {
- name: 'AZURE_OPENAI_ENDPOINT'
- value: 'https://${aiServices.outputs.name}.openai.azure.com/'
- }
- {
- name: 'MIGRATOR_AGENT_MODEL_DEPLOY'
- value: modelDeployment.name
- }
- {
- name: 'PICKER_AGENT_MODEL_DEPLOY'
- value: modelDeployment.name
- }
- {
- name: 'FIXER_AGENT_MODEL_DEPLOY'
- value: modelDeployment.name
- }
- {
- name: 'SEMANTIC_VERIFIER_AGENT_MODEL_DEPLOY'
- value: modelDeployment.name
- }
- {
- name: 'SYNTAX_CHECKER_AGENT_MODEL_DEPLOY'
- value: modelDeployment.name
- }
- {
- name: 'SELECTION_MODEL_DEPLOY'
- value: modelDeployment.name
- }
- {
- name: 'TERMINATION_MODEL_DEPLOY'
- value: modelDeployment.name
- }
- {
- name: 'AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME'
- value: modelDeployment.name
- }
- {
- name: 'AZURE_AI_AGENT_PROJECT_NAME'
- value: azureAifoundry.outputs.projectName
- }
- {
- name: 'AZURE_AI_AGENT_RESOURCE_GROUP_NAME'
- value: resourceGroup().name
- }
- {
- name: 'AZURE_AI_AGENT_SUBSCRIPTION_ID'
- value: subscription().subscriptionId
- }
- {
- name: 'AZURE_AI_AGENT_PROJECT_CONNECTION_STRING'
- value: azureAifoundry.outputs.projectConnectionString
- }
- {
- name: 'AZURE_CLIENT_ID'
- value: appIdentity.outputs.clientId // TODO - VERIFY -> NOTE: This is the client ID of the managed identity, not the Entra application, and is needed for the App Service to access the Cosmos DB account.
- }
- ], enableMonitoring ? [
- {
- name: 'APPLICATIONINSIGHTS_INSTRUMENTATION_KEY'
- value: applicationInsights.outputs.instrumentationKey
- }
+ env: [
{
- name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
- value: applicationInsights.outputs.connectionString
+ name: 'API_URL'
+ value: 'https://${containerAppBackend.outputs.fqdn}'
}
- ] : [])
+ ]
+ image: 'cmsacontainerreg.azurecr.io/cmsafrontend:latest'
+ name: 'cmsafrontend'
resources: {
- cpu: 1
+ cpu: '1'
memory: '2.0Gi'
}
- probes: enableMonitoring ? [
- {
- httpGet: {
- path: '/health'
- port: 8000
- }
- initialDelaySeconds: 3
- periodSeconds: 3
- type: 'Liveness'
- }
- ] : []
}
]
- ingressTargetPort: 8000
+ ingressTargetPort: 3000
ingressExternal: true
- // TODO - need way to set this CORS policy after frontend container app is deployed (issue is circular dependency since frontend needs backend to be deployed first)
- // corsPolicy: {
- // allowedOrigins: [
- // 'https://${containerAppFrontend.outputs.fqdn}'
- // ]
- // }
scaleSettings: {
maxReplicas: enableScaling ? 3 : 1
minReplicas: 1
- rules: enableScaling ? [
- {
- name: 'http-scaler'
- http: {
- metadata: {
- concurrentRequests: 100
+ rules: enableScaling
+ ? [
+ {
+ name: 'http-scaler'
+ http: {
+ metadata: {
+ concurrentRequests: 100
+ }
+ }
}
- }
- }
- ] : []
+ ]
+ : []
}
tags: allTags
enableTelemetry: enableTelemetry
}
}
+
+@description('The resource group the resources were deployed into.')
+output resourceGroupName string = resourceGroup().name
diff --git a/infra/main.bicepparam b/infra/main.bicepparam
index 374a70a9..0238f5bc 100644
--- a/infra/main.bicepparam
+++ b/infra/main.bicepparam
@@ -3,14 +3,3 @@ using './main.bicep'
param solutionName = readEnvironmentVariable('AZURE_ENV_NAME')
param location = readEnvironmentVariable('AZURE_LOCATION')
-//*******************************************************************************
-// Uncomment the following lines to enable the WAF-aligned configuration
-//*******************************************************************************
-
-param enableMonitoring = true
-param enableScaling = true
-param enableRedundancy = true
-//param secondaryLocation = 'uksouth' // TODO - test this
-param enablePrivateNetworking = true
-param vmAdminUsername = 'JumpboxAdminUser'
-param vmAdminPassword = 'JumpboxAdminP@ssw0rd1234!'
diff --git a/infra/main.waf-aligned.bicepparam b/infra/main.waf-aligned.bicepparam
new file mode 100644
index 00000000..912262b7
--- /dev/null
+++ b/infra/main.waf-aligned.bicepparam
@@ -0,0 +1,31 @@
+using './main.bicep'
+
+param solutionName = readEnvironmentVariable('AZURE_ENV_NAME')
+param location = readEnvironmentVariable('AZURE_LOCATION')
+
+//**************************************************************************************************
+// WAF-aligned configurations:
+// Monitoring
+// Scaling
+// Redundancy
+// Private networking
+//*************************************************************************************************
+
+param enableMonitoring = true
+param enableScaling = true
+
+//*************************************************************************************************
+// Redundancy, for azure storage and cosmos DB, set to true if you want to enable redundancy
+// !!! Please check capacity and availability for redundancy in your desirable regions first
+// and set it accordingly. We recommend to set this to false if you are not sure.
+//
+param enableRedundancy = false // If true, need to set secondaryLocation
+//param secondaryLocation = 'westus2' // Set the secondary location for redundancy
+//*************************************************************************************************
+
+//*************************************************************************************************
+// Private networking
+param enablePrivateNetworking = true
+param vmAdminUsername = 'JumpboxAdminUser' // update this to your desired admin username
+param vmAdminPassword = 'JumpboxAdminP@ssw0rd1234!' // update this to your desired admin password
+//*************************************************************************************************
diff --git a/infra/modules/ai-foundry/ai-services.bicep b/infra/modules/ai-foundry/ai-services.bicep
new file mode 100644
index 00000000..bb601e0b
--- /dev/null
+++ b/infra/modules/ai-foundry/ai-services.bicep
@@ -0,0 +1,552 @@
+// This module is here solely to provide network injection for Cognitive Services.
+// The AVM Module 'br/public:avm/res/cognitive-services/account:0.11.0' does not support that feature as of version 0.11.0
+metadata name = 'Cognitive Services'
+metadata description = 'This module deploys a Cognitive Service.'
+
+@description('Required. The name of Cognitive Services account.')
+param name string
+
+@description('Required. Kind of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.')
+@allowed([
+ 'AIServices'
+ 'AnomalyDetector'
+ 'CognitiveServices'
+ 'ComputerVision'
+ 'ContentModerator'
+ 'ContentSafety'
+ 'ConversationalLanguageUnderstanding'
+ 'CustomVision.Prediction'
+ 'CustomVision.Training'
+ 'Face'
+ 'FormRecognizer'
+ 'HealthInsights'
+ 'ImmersiveReader'
+ 'Internal.AllInOne'
+ 'LUIS'
+ 'LUIS.Authoring'
+ 'LanguageAuthoring'
+ 'MetricsAdvisor'
+ 'OpenAI'
+ 'Personalizer'
+ 'QnAMaker.v2'
+ 'SpeechServices'
+ 'TextAnalytics'
+ 'TextTranslation'
+])
+param kind string = 'AIServices'
+
+@description('Optional. SKU of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.')
+@allowed([
+ 'C2'
+ 'C3'
+ 'C4'
+ 'F0'
+ 'F1'
+ 'S'
+ 'S0'
+ 'S1'
+ 'S10'
+ 'S2'
+ 'S3'
+ 'S4'
+ 'S5'
+ 'S6'
+ 'S7'
+ 'S8'
+ 'S9'
+])
+param sku string = 'S0'
+
+@description('Optional. Location for all Resources.')
+param location string = resourceGroup().location
+
+import { diagnosticSettingFullType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
+@description('Optional. The diagnostic settings of the service.')
+param diagnosticSettings diagnosticSettingFullType[]?
+
+@description('Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set.')
+@allowed([
+ 'Enabled'
+ 'Disabled'
+])
+param publicNetworkAccess string?
+
+@description('Conditional. Subdomain name used for token-based authentication. Required if \'networkAcls\' or \'privateEndpoints\' are set.')
+param customSubDomainName string?
+
+@description('Optional. A collection of rules governing the accessibility from specific network locations.')
+param networkAcls object?
+
+@description('Optional. The network injection subnet resource Id for the Cognitive Services account. This allows to use the AI Services account with a virtual network.')
+param networkInjectionSubnetResourceId string?
+
+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[]?
+
+import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
+@description('Optional. Array of role assignments to create.')
+param roleAssignments roleAssignmentType[]?
+
+@description('Optional. Tags of the resource.')
+param tags object?
+
+@description('Optional. List of allowed FQDN.')
+param allowedFqdnList array?
+
+@description('Optional. The API properties for special APIs.')
+param apiProperties object?
+
+@description('Optional. Allow only Azure AD authentication. Should be enabled for security reasons.')
+param disableLocalAuth bool = true
+
+@description('Optional. The flag to enable dynamic throttling.')
+param dynamicThrottlingEnabled bool = false
+
+@secure()
+@description('Optional. Resource migration token.')
+param migrationToken string?
+
+@description('Optional. Restore a soft-deleted cognitive service at deployment time. Will fail if no such soft-deleted resource exists.')
+param restore bool = false
+
+@description('Optional. Restrict outbound network access.')
+param restrictOutboundNetworkAccess bool = true
+
+@description('Optional. The storage accounts for this resource.')
+param userOwnedStorage array?
+
+import { managedIdentityAllType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
+@description('Optional. The managed identity definition for this resource.')
+param managedIdentities managedIdentityAllType?
+
+@description('Optional. Array of deployments about cognitive service accounts to create.')
+param deployments deploymentType[]?
+
+var enableReferencedModulesTelemetry = false
+
+var formattedUserAssignedIdentities = reduce(
+ map((managedIdentities.?userAssignedResourceIds ?? []), (id) => { '${id}': {} }),
+ {},
+ (cur, next) => union(cur, next)
+) // Converts the flat array to an object like { '${id1}': {}, '${id2}': {} }
+var identity = !empty(managedIdentities)
+ ? {
+ type: (managedIdentities.?systemAssigned ?? false)
+ ? (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned')
+ : (!empty(managedIdentities.?userAssignedResourceIds ?? {}) ? 'UserAssigned' : null)
+ userAssignedIdentities: !empty(formattedUserAssignedIdentities) ? formattedUserAssignedIdentities : null
+ }
+ : null
+
+var builtInRoleNames = {
+ 'Cognitive Services Contributor': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68'
+ )
+ 'Cognitive Services Custom Vision Contributor': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'c1ff6cc2-c111-46fe-8896-e0ef812ad9f3'
+ )
+ 'Cognitive Services Custom Vision Deployment': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '5c4089e1-6d96-4d2f-b296-c1bc7137275f'
+ )
+ 'Cognitive Services Custom Vision Labeler': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '88424f51-ebe7-446f-bc41-7fa16989e96c'
+ )
+ 'Cognitive Services Custom Vision Reader': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '93586559-c37d-4a6b-ba08-b9f0940c2d73'
+ )
+ 'Cognitive Services Custom Vision Trainer': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '0a5ae4ab-0d65-4eeb-be61-29fc9b54394b'
+ )
+ 'Cognitive Services Data Reader (Preview)': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'b59867f0-fa02-499b-be73-45a86b5b3e1c'
+ )
+ 'Cognitive Services Face Recognizer': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '9894cab4-e18a-44aa-828b-cb588cd6f2d7'
+ )
+ 'Cognitive Services Immersive Reader User': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'b2de6794-95db-4659-8781-7e080d3f2b9d'
+ )
+ 'Cognitive Services Language Owner': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'f07febfe-79bc-46b1-8b37-790e26e6e498'
+ )
+ 'Cognitive Services Language Reader': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '7628b7b8-a8b2-4cdc-b46f-e9b35248918e'
+ )
+ 'Cognitive Services Language Writer': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'f2310ca1-dc64-4889-bb49-c8e0fa3d47a8'
+ )
+ 'Cognitive Services LUIS Owner': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'f72c8140-2111-481c-87ff-72b910f6e3f8'
+ )
+ 'Cognitive Services LUIS Reader': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '18e81cdc-4e98-4e29-a639-e7d10c5a6226'
+ )
+ 'Cognitive Services LUIS Writer': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '6322a993-d5c9-4bed-b113-e49bbea25b27'
+ )
+ 'Cognitive Services Metrics Advisor Administrator': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'cb43c632-a144-4ec5-977c-e80c4affc34a'
+ )
+ 'Cognitive Services Metrics Advisor User': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '3b20f47b-3825-43cb-8114-4bd2201156a8'
+ )
+ 'Cognitive Services OpenAI Contributor': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'a001fd3d-188f-4b5d-821b-7da978bf7442'
+ )
+ 'Cognitive Services OpenAI User': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'
+ )
+ 'Cognitive Services QnA Maker Editor': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'f4cc2bf9-21be-47a1-bdf1-5c5804381025'
+ )
+ 'Cognitive Services QnA Maker Reader': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '466ccd10-b268-4a11-b098-b4849f024126'
+ )
+ 'Cognitive Services Speech Contributor': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '0e75ca1e-0464-4b4d-8b93-68208a576181'
+ )
+ 'Cognitive Services Speech User': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'f2dc8367-1007-4938-bd23-fe263f013447'
+ )
+ 'Cognitive Services User': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'a97b65f3-24c7-4388-baec-2e87135dc908'
+ )
+ Contributor: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
+ Owner: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')
+ Reader: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')
+ 'Role Based Access Control Administrator': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'f58310d9-a9f6-439a-9e8d-f62e7b41a168'
+ )
+ 'User Access Administrator': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9'
+ )
+}
+
+var formattedRoleAssignments = [
+ for (roleAssignment, index) in (roleAssignments ?? []): union(roleAssignment, {
+ roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? (contains(
+ roleAssignment.roleDefinitionIdOrName,
+ '/providers/Microsoft.Authorization/roleDefinitions/'
+ )
+ ? roleAssignment.roleDefinitionIdOrName
+ : subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleAssignment.roleDefinitionIdOrName))
+ })
+]
+
+resource cognitiveService 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = {
+ name: name
+ kind: kind
+ identity: identity
+ location: location
+ tags: tags
+ sku: {
+ name: sku
+ }
+ properties: {
+ customSubDomainName: customSubDomainName
+ allowProjectManagement: true
+ networkAcls: !empty(networkAcls ?? {})
+ ? {
+ defaultAction: networkAcls.?defaultAction
+ virtualNetworkRules: networkAcls.?virtualNetworkRules ?? []
+ ipRules: networkAcls.?ipRules ?? []
+ }
+ : null
+ publicNetworkAccess: publicNetworkAccess != null
+ ? publicNetworkAccess
+ : (!empty(networkAcls) ? 'Enabled' : 'Disabled')
+ allowedFqdnList: allowedFqdnList
+ apiProperties: apiProperties
+ disableLocalAuth: disableLocalAuth
+ #disable-next-line BCP036
+ networkInjections: networkInjectionSubnetResourceId != null
+ ? [
+ {
+ scenario: 'agent'
+ subnetArmId: networkInjectionSubnetResourceId
+ useMicrosoftManagedNetwork: false
+ }
+ ]
+ : null
+ // true is not supported today
+ encryption: null // Customer managed key encryption is used, but the property is required.
+ migrationToken: migrationToken
+ restore: restore
+ restrictOutboundNetworkAccess: restrictOutboundNetworkAccess
+ userOwnedStorage: userOwnedStorage
+ dynamicThrottlingEnabled: dynamicThrottlingEnabled
+ }
+}
+
+@batchSize(1)
+resource cognitiveService_deployments 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = [
+ for (deployment, index) in (deployments ?? []): {
+ parent: cognitiveService
+ name: deployment.?name ?? '${name}-deployments'
+ properties: {
+ model: deployment.model
+ raiPolicyName: deployment.?raiPolicyName
+ versionUpgradeOption: deployment.?versionUpgradeOption
+ }
+ sku: deployment.?sku ?? {
+ name: sku
+ capacity: sku.?capacity
+ tier: sku.?tier
+ size: sku.?size
+ family: sku.?family
+ }
+ }
+]
+
+#disable-next-line use-recent-api-versions
+resource cognitiveService_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
+ }
+ ]
+ marketplacePartnerId: diagnosticSetting.?marketplacePartnerResourceId
+ logAnalyticsDestinationType: diagnosticSetting.?logAnalyticsDestinationType
+ }
+ scope: cognitiveService
+ }
+]
+
+module cognitiveService_privateEndpoints 'br/public:avm/res/network/private-endpoint:0.11.0' = [
+ for (privateEndpoint, index) in (privateEndpoints ?? []): {
+ name: take('${uniqueString(deployment().name, location)}-cognitiveService-PrivateEndpoint-${index}', 64)
+ scope: resourceGroup(
+ split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[2],
+ split(privateEndpoint.?resourceGroupResourceId ?? resourceGroup().id, '/')[4]
+ )
+ params: {
+ name: privateEndpoint.?name ?? 'pep-${last(split(cognitiveService.id, '/'))}-${privateEndpoint.?service ?? 'account'}-${index}'
+ privateLinkServiceConnections: privateEndpoint.?isManualConnection != true
+ ? [
+ {
+ name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(cognitiveService.id, '/'))}-${privateEndpoint.?service ?? 'account'}-${index}'
+ properties: {
+ privateLinkServiceId: cognitiveService.id
+ groupIds: [
+ privateEndpoint.?service ?? 'account'
+ ]
+ }
+ }
+ ]
+ : null
+ manualPrivateLinkServiceConnections: privateEndpoint.?isManualConnection == true
+ ? [
+ {
+ name: privateEndpoint.?privateLinkServiceConnectionName ?? '${last(split(cognitiveService.id, '/'))}-${privateEndpoint.?service ?? 'account'}-${index}'
+ properties: {
+ privateLinkServiceId: cognitiveService.id
+ groupIds: [
+ privateEndpoint.?service ?? 'account'
+ ]
+ requestMessage: privateEndpoint.?manualConnectionRequestMessage ?? 'Manual approval required.'
+ }
+ }
+ ]
+ : null
+ subnetResourceId: privateEndpoint.subnetResourceId
+ enableTelemetry: enableReferencedModulesTelemetry
+ location: privateEndpoint.?location ?? reference(
+ split(privateEndpoint.subnetResourceId, '/subnets/')[0],
+ '2020-06-01',
+ 'Full'
+ ).location
+ privateDnsZoneGroup: privateEndpoint.?privateDnsZoneGroup
+ roleAssignments: privateEndpoint.?roleAssignments
+ tags: privateEndpoint.?tags ?? tags
+ customDnsConfigs: privateEndpoint.?customDnsConfigs
+ ipConfigurations: privateEndpoint.?ipConfigurations
+ applicationSecurityGroupResourceIds: privateEndpoint.?applicationSecurityGroupResourceIds
+ customNetworkInterfaceName: privateEndpoint.?customNetworkInterfaceName
+ }
+ }
+]
+
+resource cognitiveService_roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
+ for (roleAssignment, index) in (formattedRoleAssignments ?? []): {
+ name: roleAssignment.?name ?? guid(cognitiveService.id, roleAssignment.principalId, roleAssignment.roleDefinitionId)
+ properties: {
+ roleDefinitionId: roleAssignment.roleDefinitionId
+ principalId: roleAssignment.principalId
+ description: roleAssignment.?description
+ principalType: roleAssignment.?principalType
+ condition: roleAssignment.?condition
+ conditionVersion: !empty(roleAssignment.?condition) ? (roleAssignment.?conditionVersion ?? '2.0') : null // Must only be set if condtion is set
+ delegatedManagedIdentityResourceId: roleAssignment.?delegatedManagedIdentityResourceId
+ }
+ scope: cognitiveService
+ }
+]
+
+@description('The name of the cognitive services account.')
+output name string = cognitiveService.name
+
+@description('The resource ID of the cognitive services account.')
+output resourceId string = cognitiveService.id
+
+@description('The resource group the cognitive services account was deployed into.')
+output resourceGroupName string = resourceGroup().name
+
+@description('The service endpoint of the cognitive services account.')
+output endpoint string = cognitiveService.properties.endpoint
+
+@description('All endpoints available for the cognitive services account, types depends on the cognitive service kind.')
+output endpoints endpointType = cognitiveService.properties.endpoints
+
+@description('The principal ID of the system assigned identity.')
+output systemAssignedMIPrincipalId string? = cognitiveService.?identity.?principalId
+
+@description('The location the resource was deployed into.')
+output location string = cognitiveService.location
+
+@description('The private endpoints of the congitive services account.')
+output privateEndpoints privateEndpointOutputType[] = [
+ for (pe, index) in (privateEndpoints ?? []): {
+ name: cognitiveService_privateEndpoints[index].outputs.name
+ resourceId: cognitiveService_privateEndpoints[index].outputs.resourceId
+ groupId: cognitiveService_privateEndpoints[index].outputs.?groupId!
+ customDnsConfigs: cognitiveService_privateEndpoints[index].outputs.customDnsConfigs
+ networkInterfaceResourceIds: cognitiveService_privateEndpoints[index].outputs.networkInterfaceResourceIds
+ }
+]
+
+// ================ //
+// Definitions //
+// ================ //
+
+@export()
+@description('The type for the private endpoint output.')
+type privateEndpointOutputType = {
+ @description('The name of the private endpoint.')
+ name: string
+
+ @description('The resource ID of the private endpoint.')
+ resourceId: string
+
+ @description('The group Id for the private endpoint Group.')
+ groupId: string?
+
+ @description('The custom DNS configurations of the private endpoint.')
+ customDnsConfigs: {
+ @description('FQDN that resolves to private endpoint IP address.')
+ fqdn: string?
+
+ @description('A list of private IP addresses of the private endpoint.')
+ ipAddresses: string[]
+ }[]
+
+ @description('The IDs of the network interfaces associated with the private endpoint.')
+ networkInterfaceResourceIds: string[]
+}
+
+@export()
+@description('The type for a cognitive services account deployment.')
+type deploymentType = {
+ @description('Optional. Specify the name of cognitive service account deployment.')
+ name: string?
+
+ @description('Required. Properties of Cognitive Services account deployment model.')
+ model: {
+ @description('Required. The name of Cognitive Services account deployment model.')
+ name: string
+
+ @description('Required. The format of Cognitive Services account deployment model.')
+ format: string
+
+ @description('Required. The version of Cognitive Services account deployment model.')
+ version: string
+ }
+
+ @description('Optional. The resource model definition representing SKU.')
+ sku: {
+ @description('Required. The name of the resource model definition representing SKU.')
+ name: string
+
+ @description('Optional. The capacity of the resource model definition representing SKU.')
+ capacity: int?
+
+ @description('Optional. The tier of the resource model definition representing SKU.')
+ tier: string?
+
+ @description('Optional. The size of the resource model definition representing SKU.')
+ size: string?
+
+ @description('Optional. The family of the resource model definition representing SKU.')
+ family: string?
+ }?
+
+ @description('Optional. The name of RAI policy.')
+ raiPolicyName: string?
+
+ @description('Optional. The version upgrade option.')
+ versionUpgradeOption: string?
+}
+
+@export()
+@description('The type for a cognitive services account endpoint.')
+type endpointType = {
+ @description('Type of the endpoint.')
+ name: string?
+ @description('The endpoint URI.')
+ endpoint: string?
+}
+
+@export()
+@description('The type of the secrets exported to the provided Key Vault.')
+type secretsExportConfigurationType = {
+ @description('Required. The key vault name where to store the keys and connection strings generated by the modules.')
+ keyVaultResourceId: string
+
+ @description('Optional. The name for the accessKey1 secret to create.')
+ accessKey1Name: string?
+
+ @description('Optional. The name for the accessKey2 secret to create.')
+ accessKey2Name: string?
+}
diff --git a/infra/modules/ai-foundry/main.bicep b/infra/modules/ai-foundry/main.bicep
new file mode 100644
index 00000000..1058d2cc
--- /dev/null
+++ b/infra/modules/ai-foundry/main.bicep
@@ -0,0 +1,250 @@
+metadata name = 'AI Services and Project Module'
+metadata description = 'This module creates an AI Services resource and an AI Foundry project within it. It supports private networking, OpenAI deployments, and role assignments.'
+
+@description('Required. Name of the Cognitive Services resource. Must be unique in the resource group.')
+param name string
+
+@description('Optional. The location of the Cognitive Services resource.')
+param location string // this should be passed
+
+@description('Optional. Kind of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.')
+@allowed([
+ 'AIServices'
+ 'AnomalyDetector'
+ 'CognitiveServices'
+ 'ComputerVision'
+ 'ContentModerator'
+ 'ContentSafety'
+ 'ConversationalLanguageUnderstanding'
+ 'CustomVision.Prediction'
+ 'CustomVision.Training'
+ 'Face'
+ 'FormRecognizer'
+ 'HealthInsights'
+ 'ImmersiveReader'
+ 'Internal.AllInOne'
+ 'LUIS'
+ 'LUIS.Authoring'
+ 'LanguageAuthoring'
+ 'MetricsAdvisor'
+ 'OpenAI'
+ 'Personalizer'
+ 'QnAMaker.v2'
+ 'SpeechServices'
+ 'TextAnalytics'
+ 'TextTranslation'
+])
+param kind string = 'AIServices'
+
+@description('Optional. The SKU of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.')
+@allowed([
+ 'S'
+ 'S0'
+ 'S1'
+ 'S2'
+ 'S3'
+ 'S4'
+ 'S5'
+ 'S6'
+ 'S7'
+ 'S8'
+])
+param sku string = 'S0'
+
+@description('Required. The name of the AI Foundry project to create.')
+param projectName string
+
+@description('Optional. The description of the AI Foundry project to create.')
+param projectDescription string = projectName
+
+@description('Optional. The resource ID of the Log Analytics workspace to use for diagnostic settings.')
+param logAnalyticsWorkspaceResourceId string?
+
+import { deploymentType } from 'br/public:avm/res/cognitive-services/account:0.10.2'
+@description('Optional. Specifies the OpenAI deployments to create.')
+param deployments deploymentType[] = []
+
+import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
+@description('Optional. Array of role assignments to create.')
+param roleAssignments roleAssignmentType[] = []
+
+@description('Optional. Values to establish private networking for the AI Services resource.')
+param privateNetworking aiServicesPrivateNetworkingType?
+
+@description('Optional. Tags to be applied to the resources.')
+param tags object = {}
+
+@description('Optional. Enable/Disable usage telemetry for module.')
+param enableTelemetry bool = true
+
+module cognitiveServicesPrivateDnsZone '../privateDnsZone.bicep' = if (privateNetworking != null && empty(privateNetworking.?cogServicesPrivateDnsZoneResourceId)) {
+ name: take('${name}-cognitiveservices-pdns-deployment', 64)
+ params: {
+ name: 'privatelink.cognitiveservices.${toLower(environment().name) == 'azureusgovernment' ? 'azure.us' : 'azure.com'}'
+ virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
+ tags: tags
+ }
+}
+
+module openAiPrivateDnsZone '../privateDnsZone.bicep' = if (privateNetworking != null && empty(privateNetworking.?openAIPrivateDnsZoneResourceId)) {
+ name: take('${name}-openai-pdns-deployment', 64)
+ params: {
+ name: 'privatelink.openai.${toLower(environment().name) == 'azureusgovernment' ? 'azure.us' : 'azure.com'}'
+ virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
+ tags: tags
+ }
+}
+
+module aiServicesPrivateDnsZone '../privateDnsZone.bicep' = if (privateNetworking != null && empty(privateNetworking.?aiServicesPrivateDnsZoneResourceId)) {
+ name: take('${name}-ai-services-pdns-deployment', 64)
+ params: {
+ name: 'privatelink.services.ai.${toLower(environment().name) == 'azureusgovernment' ? 'azure.us' : 'azure.com'}'
+ virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
+ tags: tags
+ }
+}
+
+var cogServicesPrivateDnsZoneResourceId = privateNetworking != null
+ ? (empty(privateNetworking.?cogServicesPrivateDnsZoneResourceId)
+ ? cognitiveServicesPrivateDnsZone.outputs.resourceId ?? ''
+ : privateNetworking.?cogServicesPrivateDnsZoneResourceId)
+ : ''
+var openAIPrivateDnsZoneResourceId = privateNetworking != null
+ ? (empty(privateNetworking.?openAIPrivateDnsZoneResourceId)
+ ? openAiPrivateDnsZone.outputs.resourceId ?? ''
+ : privateNetworking.?openAIPrivateDnsZoneResourceId)
+ : ''
+
+var aiServicesPrivateDnsZoneResourceId = privateNetworking != null
+ ? (empty(privateNetworking.?aiServicesPrivateDnsZoneResourceId)
+ ? aiServicesPrivateDnsZone.outputs.resourceId ?? ''
+ : privateNetworking.?aiServicesPrivateDnsZoneResourceId)
+ : ''
+
+module cognitiveService 'ai-services.bicep' = {
+ name: take('${name}-aiservices-deployment', 64)
+ #disable-next-line no-unnecessary-dependson
+ dependsOn: [cognitiveServicesPrivateDnsZone, openAiPrivateDnsZone, aiServicesPrivateDnsZone] // required due to optional flags that could change dependency
+ params: {
+ name: name
+ location: location
+ tags: tags
+ sku: sku
+ kind: kind
+ managedIdentities: {
+ systemAssigned: true
+ }
+ deployments: deployments
+ customSubDomainName: name
+ disableLocalAuth: false
+ publicNetworkAccess: privateNetworking != null ? 'Disabled' : 'Enabled'
+ // rules to allow firewall and virtual network access
+ networkAcls: {
+ defaultAction: 'Allow'
+ virtualNetworkRules: []
+ ipRules: []
+ }
+ diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId)
+ ? [
+ {
+ workspaceResourceId: logAnalyticsWorkspaceResourceId
+ }
+ ]
+ : []
+ roleAssignments: roleAssignments
+ privateEndpoints: privateNetworking != null
+ ? [
+ {
+ name:'pep-${name}-aiservices' // private endpoint name
+ customNetworkInterfaceName: 'nic-${name}-aiservices'
+ subnetResourceId: privateNetworking.?subnetResourceId ?? ''
+ privateDnsZoneGroup: {
+ privateDnsZoneGroupConfigs: [
+ {
+ privateDnsZoneResourceId: cogServicesPrivateDnsZoneResourceId
+ }
+ {
+ privateDnsZoneResourceId: openAIPrivateDnsZoneResourceId
+ }
+ {
+ privateDnsZoneResourceId: aiServicesPrivateDnsZoneResourceId
+ }
+ ]
+ }
+ }
+ ]
+ : []
+ }
+}
+
+
+module aiProject 'project.bicep' = {
+ name: take('${name}-ai-project-${projectName}-deployment', 64)
+ params: {
+ name: projectName
+ desc: projectDescription
+ aiServicesName: cognitiveService.outputs.name
+ location: location
+ roleAssignments: roleAssignments
+ tags: tags
+ enableTelemetry: enableTelemetry
+ }
+}
+
+@description('The resource group the resources were deployed into.')
+output resourceGroupName string = resourceGroup().name
+
+@description('Name of the Cognitive Services resource.')
+output name string = cognitiveService.outputs.name
+
+@description('Resource ID of the Cognitive Services resource.')
+output resourceId string = cognitiveService.outputs.resourceId
+
+@description('Principal ID of the system assigned managed identity for the Cognitive Services resource. This is only available if the resource has a system assigned managed identity.')
+output systemAssignedMIPrincipalId string? = cognitiveService.outputs.?systemAssignedMIPrincipalId
+
+@description('The endpoint of the Cognitive Services resource.')
+output endpoint string = cognitiveService.outputs.endpoint
+
+import { aiProjectOutputType } from 'project.bicep'
+@description('AI Foundry Project information.')
+output project aiProjectOutputType = {
+ name: aiProject.name
+ resourceId: aiProject.outputs.resourceId
+ apiEndpoint: aiProject.outputs.apiEndpoint
+}
+
+@export()
+@description('A custom AVM-aligned type for a role assignment for AI Services and Project.')
+type aiServicesRoleAssignmentType = {
+ @description('Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated.')
+ name: string?
+
+ @description('Required. The role to assign. You can provide either the role definition GUID or its fully qualified ID in the following format: \'/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11\'.')
+ roleDefinitionId: string
+
+ @description('Required. The principal ID of the principal (user/group/identity) to assign the role to.')
+ principalId: string
+
+ @description('Optional. The principal type of the assigned principal ID.')
+ principalType: ('ServicePrincipal' | 'Group' | 'User' | 'ForeignGroup' | 'Device')?
+}
+
+@export()
+@description('Values to establish private networking for resources that support createing private endpoints.')
+type aiServicesPrivateNetworkingType = {
+ @description('Required. The Resource ID of the virtual network.')
+ virtualNetworkResourceId: string
+
+ @description('Required. The Resource ID of the subnet to establish the Private Endpoint(s).')
+ subnetResourceId: string
+
+ @description('Optional. The Resource ID of an existing "cognitiveservices" Private DNS Zone Resource to link to the virtual network. If not provided, a new "cognitiveservices" Private DNS Zone(s) will be created.')
+ cogServicesPrivateDnsZoneResourceId: string?
+
+ @description('Optional. The Resource ID of an existing "openai" Private DNS Zone Resource to link to the virtual network. If not provided, a new "openai" Private DNS Zone(s) will be created.')
+ openAIPrivateDnsZoneResourceId: string?
+
+ @description('Optional. The Resource ID of an existing "services.ai" Private DNS Zone Resource to link to the virtual network. If not provided, a new "services.ai" Private DNS Zone(s) will be created.')
+ aiServicesPrivateDnsZoneResourceId: string?
+}
diff --git a/infra/modules/ai-foundry/project.bicep b/infra/modules/ai-foundry/project.bicep
new file mode 100644
index 00000000..17b4475f
--- /dev/null
+++ b/infra/modules/ai-foundry/project.bicep
@@ -0,0 +1,105 @@
+@description('Required. Name of the AI Services project.')
+param name string
+
+@description('Required. The location of the Project resource.')
+param location string = resourceGroup().location
+
+@description('Optional. The description of the AI Foundry project to create. Defaults to the project name.')
+param desc string = name
+
+@description('Required. Name of the existing Cognitive Services resource to create the AI Foundry project in.')
+param aiServicesName string
+
+import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
+@description('Optional. Array of role assignments to create.')
+param roleAssignments roleAssignmentType[] = []
+
+@description('Optional. Tags to be applied to the resources.')
+param tags object = {}
+
+@description('Optional. Enable/Disable usage telemetry for module.')
+param enableTelemetry bool = true
+
+// using a few built-in roles here that makes sense for Foundry projects only
+var builtInRoleNames = {
+ 'Cognitive Services OpenAI Contributor': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ 'a001fd3d-188f-4b5d-821b-7da978bf7442'
+ )
+ 'Cognitive Services OpenAI User': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'
+ )
+ 'Azure AI Developer': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '64702f94-c441-49e6-a78b-ef80e0188fee'
+ )
+ 'Azure AI User': subscriptionResourceId(
+ 'Microsoft.Authorization/roleDefinitions',
+ '53ca6127-db72-4b80-b1b0-d745d6d5456d'
+ )
+}
+
+var formattedRoleAssignments = [
+ for (roleAssignment, index) in (roleAssignments ?? []): union(roleAssignment, {
+ roleDefinitionId: builtInRoleNames[?roleAssignment.roleDefinitionIdOrName] ?? (contains(
+ roleAssignment.roleDefinitionIdOrName,
+ '/providers/Microsoft.Authorization/roleDefinitions/'
+ )
+ ? roleAssignment.roleDefinitionIdOrName
+ : subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleAssignment.roleDefinitionIdOrName))
+ })
+]
+
+resource cogServiceReference 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = {
+ name: aiServicesName
+}
+
+resource aiProject 'Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview' = {
+ parent: cogServiceReference
+ name: name
+ tags: tags
+ location: location
+ identity: {
+ type: 'SystemAssigned'
+ }
+ properties: {
+ description: desc
+ displayName: name
+ }
+}
+
+module aiProjectRoleAssignement 'br/public:avm/ptn/authorization/resource-role-assignment:0.1.2' = [
+ for (roleAssignment, i) in formattedRoleAssignments: {
+ name: 'avm.ptn.authorization.resource-role-assignment.${uniqueString(name, roleAssignment.roleDefinitionId, roleAssignment.principalId)}'
+ params: {
+ roleDefinitionId: roleAssignment.roleDefinitionId
+ principalId: roleAssignment.principalId
+ principalType: 'ServicePrincipal'
+ resourceId: aiProject.id
+ enableTelemetry: enableTelemetry
+ }
+ }
+]
+
+@description('Name of the AI Foundry project.')
+output name string = aiProject.name
+
+@description('Resource ID of the AI Foundry project.')
+output resourceId string = aiProject.id
+
+@description('API endpoint for the AI Foundry project.')
+output apiEndpoint string = aiProject.properties.endpoints['AI Foundry API']
+
+@export()
+@description('Output type representing AI project information.')
+type aiProjectOutputType = {
+ @description('Required. Name of the AI project.')
+ name: string
+
+ @description('Required. Resource ID of the AI project.')
+ resourceId: string
+
+ @description('Required. API endpoint for the AI project.')
+ apiEndpoint: string
+}
diff --git a/infra/modules/aiFoundry.bicep b/infra/modules/aiFoundry.bicep
deleted file mode 100644
index 5fd19360..00000000
--- a/infra/modules/aiFoundry.bicep
+++ /dev/null
@@ -1,197 +0,0 @@
-@description('The Azure region where resources will be deployed.')
-param location string
-
-@description('The name of the AI Foundry Project workspace.')
-param projectName string
-
-@description('The name of the AI Foundry Hub workspace.')
-param hubName string
-
-@description('The description of the AI Hub workspace.')
-param hubDescription string = hubName
-
-@description('The Resource Id of an existing storage account to attach to AI Foundry.')
-param storageAccountResourceId string
-
-@description('The resource ID of the Azure Key Vault to associate with AI Foundry.')
-param keyVaultResourceId string
-
-@description('The Resource ID of the managed identity to assign to the AI Foundry Project workspace.')
-param userAssignedIdentityResourceId string
-
-@description('Optional. The resource ID of an existing Log Analytics workspace to associate with AI Foundry for monitoring.')
-param logAnalyticsWorkspaceResourceId string?
-
-@description('Optional. The resource ID of an existing Application Insights resource to associate with AI Foundry for monitoring.')
-param applicationInsightsResourceId string?
-
-@description('The name of an existing Azure Cognitive Services account.')
-param aiServicesName string
-
-@description('Optional. Values to establish private networking for the AI Foundry resources.')
-param privateNetworking machineLearningPrivateNetworkingType?
-
-import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
-@description('Optional. Array of role assignments to create.')
-param roleAssignments roleAssignmentType[]?
-
-@description('Optional. Tags to be applied to the resources.')
-param tags object = {}
-
-@description('Optional. Enable/Disable usage telemetry for module.')
-param enableTelemetry bool = true
-
-module mlApiPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (privateNetworking != null && empty(privateNetworking.?apiPrivateDnsZoneResourceId)) {
- name: take('${hubName}-mlapi-pdns-deployment', 64)
- params: {
- name: 'privatelink.api.${toLower(environment().name) == 'azureusgovernment' ? 'ml.azure.us' : 'azureml.ms'}'
- virtualNetworkLinks: [
- {
- virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
- }
- ]
- tags: tags
- enableTelemetry: enableTelemetry
- }
-}
-
-module mlNotebooksPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (privateNetworking != null && empty(privateNetworking.?notebooksPrivateDnsZoneResourceId)) {
- name: take('${hubName}-mlnotebook-pdns-deployment', 64)
- params: {
- name: 'privatelink.notebooks.${toLower(environment().name) == 'azureusgovernment' ? 'azureml.us' : 'azureml.net'}'
- virtualNetworkLinks: [
- {
- virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
- }
- ]
- tags: tags
- enableTelemetry: enableTelemetry
- }
-}
-
-var apiPrivateDnsZoneResourceId = privateNetworking != null ? (empty(privateNetworking.?apiPrivateDnsZoneResourceId) ? mlApiPrivateDnsZone.outputs.resourceId ?? '' : privateNetworking.?apiPrivateDnsZoneResourceId) : ''
-var notebooksPrivateDnsZoneResourceId = privateNetworking != null ? (empty(privateNetworking.?notebooksPrivateDnsZoneResourceId) ? mlNotebooksPrivateDnsZone.outputs.resourceId ?? '' : privateNetworking.?notebooksPrivateDnsZoneResourceId) : ''
-
-resource aiServices 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = {
- name: aiServicesName
-}
-
-module hub 'br/public:avm/res/machine-learning-services/workspace:0.12.1' = {
- name: take('ai-foundry-${hubName}-deployment', 64)
- #disable-next-line no-unnecessary-dependson
- dependsOn: [mlApiPrivateDnsZone, mlNotebooksPrivateDnsZone] // required due to optional flags that could change dependency
- params: {
- name: hubName
- location: location
- sku: 'Standard'
- kind: 'Hub'
- description: hubDescription
- associatedKeyVaultResourceId: keyVaultResourceId
- associatedStorageAccountResourceId: storageAccountResourceId
- associatedApplicationInsightsResourceId: applicationInsightsResourceId
- publicNetworkAccess: privateNetworking != null ? 'Disabled' : 'Enabled'
- managedNetworkSettings: {
- isolationMode: privateNetworking != null ? 'AllowInternetOutbound' : 'Disabled'
- outboundRules: privateNetworking != null ? {
- cog_services_pep: {
- category: 'UserDefined'
- destination: {
- serviceResourceId: aiServices.id
- sparkEnabled: true
- subresourceTarget: 'account'
- }
- type: 'PrivateEndpoint'
- }
- } : null
- }
- managedIdentities: {
- systemAssigned: true
- }
- systemDatastoresAuthMode: 'Identity'
- diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId) ? [{workspaceResourceId: logAnalyticsWorkspaceResourceId}] : []
- privateEndpoints: privateNetworking != null ? [
- {
- privateDnsZoneGroup: {
- privateDnsZoneGroupConfigs: [
- {
- privateDnsZoneResourceId: apiPrivateDnsZoneResourceId
- }
- {
- privateDnsZoneResourceId: notebooksPrivateDnsZoneResourceId
- }
- ]
- }
- service: 'amlworkspace'
- subnetResourceId: privateNetworking.?subnetResourceId ?? ''
- }
- ] : []
- connections: [
- {
- name: aiServicesName
- value: null
- category: 'AIServices'
- target: aiServices.properties.endpoint
- connectionProperties: {
- authType: 'AAD'
- }
- isSharedToAll: true
- metadata: {
- ApiType: 'Azure'
- Kind: 'AIServices'
- ResourceId: aiServices.id
- }
- }
- ]
- tags: tags
- enableTelemetry: enableTelemetry
- }
-}
-
-module project 'br/public:avm/res/machine-learning-services/workspace:0.12.1' = {
- name: take('ai-foundry-${projectName}-deployment', 64)
- params: {
- name: projectName
- kind: 'Project'
- sku: 'Standard'
- location: location
- hubResourceId: hub.outputs.resourceId
- hbiWorkspace: false
- systemDatastoresAuthMode: 'Identity'
- publicNetworkAccess: privateNetworking != null ? 'Disabled' : 'Enabled'
- managedIdentities: {
- userAssignedResourceIds: [userAssignedIdentityResourceId]
- }
- primaryUserAssignedIdentity: userAssignedIdentityResourceId
- diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId) ? [{workspaceResourceId: logAnalyticsWorkspaceResourceId}] : []
- roleAssignments: roleAssignments
- tags: tags
- enableTelemetry: enableTelemetry
- }
-}
-
-// get reference to the AI Hub project to get access to the discovery URL property (not presently available on AVM)
-// adjust this logic if support on the AVM module is added
-resource projectReference 'Microsoft.MachineLearningServices/workspaces@2024-10-01' existing = {
- name: projectName
- dependsOn: [project]
-}
-
-output projectName string = project.outputs.name
-output hubName string = hub.outputs.name
-output projectConnectionString string = '${split(projectReference.properties.discoveryUrl, '/')[2]};${subscription().subscriptionId};${resourceGroup().name};${projectReference.name}'
-
-@export()
-@description('Values to establish private networking for resources that support createing private endpoints.')
-type machineLearningPrivateNetworkingType = {
- @description('Required. The Resource ID of the virtual network.')
- virtualNetworkResourceId: string
-
- @description('Required. The Resource ID of the subnet to establish the Private Endpoint(s).')
- subnetResourceId: string
-
- @description('Optional. The Resource ID of an existing "api" Private DNS Zone Resource to link to the virtual network. If not provided, a new "api" Private DNS Zone(s) will be created.')
- apiPrivateDnsZoneResourceId: string?
-
- @description('Optional. The Resource ID of an existing "notebooks" Private DNS Zone Resource to link to the virtual network. If not provided, a new "notebooks" Private DNS Zone(s) will be created.')
- notebooksPrivateDnsZoneResourceId: string?
-}
diff --git a/infra/modules/aiServices.bicep b/infra/modules/aiServices.bicep
deleted file mode 100644
index 482bfe5a..00000000
--- a/infra/modules/aiServices.bicep
+++ /dev/null
@@ -1,164 +0,0 @@
-@description('Name of the Cognitive Services resource. Must be unique in the resource group.')
-param name string
-
-@description('The location of the Cognitive Services resource.')
-param location string
-
-@description('Required. Kind of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.')
-@allowed([
- 'AIServices'
- 'AnomalyDetector'
- 'CognitiveServices'
- 'ComputerVision'
- 'ContentModerator'
- 'ContentSafety'
- 'ConversationalLanguageUnderstanding'
- 'CustomVision.Prediction'
- 'CustomVision.Training'
- 'Face'
- 'FormRecognizer'
- 'HealthInsights'
- 'ImmersiveReader'
- 'Internal.AllInOne'
- 'LUIS'
- 'LUIS.Authoring'
- 'LanguageAuthoring'
- 'MetricsAdvisor'
- 'OpenAI'
- 'Personalizer'
- 'QnAMaker.v2'
- 'SpeechServices'
- 'TextAnalytics'
- 'TextTranslation'
-])
-param kind string = 'AIServices'
-
-@description('Required. The SKU of the Cognitive Services account. Use \'Get-AzCognitiveServicesAccountSku\' to determine a valid combinations of \'kind\' and \'SKU\' for your Azure region.')
-@allowed([
- 'S'
- 'S0'
- 'S1'
- 'S2'
- 'S3'
- 'S4'
- 'S5'
- 'S6'
- 'S7'
- 'S8'
-])
-param sku string = 'S0'
-
-@description('Optional. The resource ID of the Log Analytics workspace to use for diagnostic settings.')
-param logAnalyticsWorkspaceResourceId string?
-
-import { deploymentType } from 'br/public:avm/res/cognitive-services/account:0.10.2'
-@description('Optional. Specifies the OpenAI deployments to create.')
-param deployments deploymentType[] = []
-
-import { roleAssignmentType } from 'br/public:avm/utl/types/avm-common-types:0.5.1'
-@description('Optional. Array of role assignments to create.')
-param roleAssignments roleAssignmentType[]?
-
-@description('Optional. Values to establish private networking for the AI Services resource.')
-param privateNetworking aiServicesPrivateNetworkingType?
-
-@description('Optional. Tags to be applied to the resources.')
-param tags object = {}
-
-@description('Optional. Enable/Disable usage telemetry for module.')
-param enableTelemetry bool = true
-
-module cognitiveServicesPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (privateNetworking != null && empty(privateNetworking.?cogServicesPrivateDnsZoneResourceId)) {
- name: take('${name}-cognitiveservices-pdns-deployment', 64)
- params: {
- name: 'privatelink.cognitiveservices.${toLower(environment().name) == 'azureusgovernment' ? 'azure.us' : 'azure.com'}'
- virtualNetworkLinks: [
- {
- virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
- }
- ]
- tags: tags
- enableTelemetry: enableTelemetry
- }
-}
-
-module openAiPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (privateNetworking != null && empty(privateNetworking.?openAIPrivateDnsZoneResourceId)) {
- name: take('${name}-openai-pdns-deployment', 64)
- params: {
- name: 'privatelink.openai.${toLower(environment().name) == 'azureusgovernment' ? 'azure.us' : 'azure.com'}'
- virtualNetworkLinks: [
- {
- virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
- }
- ]
- tags: tags
- enableTelemetry: enableTelemetry
- }
-}
-
-var cogServicesPrivateDnsZoneResourceId = privateNetworking != null ? (empty(privateNetworking.?cogServicesPrivateDnsZoneResourceId) ? cognitiveServicesPrivateDnsZone.outputs.resourceId ?? '' : privateNetworking.?cogServicesPrivateDnsZoneResourceId) : ''
-var openAIPrivateDnsZoneResourceId = privateNetworking != null ? (empty(privateNetworking.?openAIPrivateDnsZoneResourceId) ? openAiPrivateDnsZone.outputs.resourceId ?? '' : privateNetworking.?openAIPrivateDnsZoneResourceId) : ''
-
-module cognitiveService 'br/public:avm/res/cognitive-services/account:0.10.2' = {
- name: take('${name}-aiservices-deployment', 64)
- #disable-next-line no-unnecessary-dependson
- dependsOn: [cognitiveServicesPrivateDnsZone, openAiPrivateDnsZone] // required due to optional flags that could change dependency
- params: {
- name: name
- location: location
- tags: tags
- sku: sku
- kind: kind
- managedIdentities: {
- systemAssigned: true
- }
- deployments: deployments
- customSubDomainName: name
- disableLocalAuth: false
- publicNetworkAccess: privateNetworking != null ? 'Disabled' : 'Enabled'
- diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId) ? [
- {
- workspaceResourceId: logAnalyticsWorkspaceResourceId
- }
- ] : []
- roleAssignments: roleAssignments
- privateEndpoints: privateNetworking != null ? [
- {
- privateDnsZoneGroup: {
- privateDnsZoneGroupConfigs: [
- {
- privateDnsZoneResourceId: cogServicesPrivateDnsZoneResourceId
- }
- {
- privateDnsZoneResourceId: openAIPrivateDnsZoneResourceId
- }
- ]
- }
- subnetResourceId: privateNetworking.?subnetResourceId ?? ''
- }
- ] : []
- enableTelemetry: enableTelemetry
- }
-}
-
-output resourceId string = cognitiveService.outputs.resourceId
-output name string = cognitiveService.outputs.name
-output systemAssignedMIPrincipalId string? = cognitiveService.outputs.?systemAssignedMIPrincipalId
-output endpoint string = cognitiveService.outputs.endpoint
-
-@export()
-@description('Values to establish private networking for resources that support createing private endpoints.')
-type aiServicesPrivateNetworkingType = {
- @description('Required. The Resource ID of the virtual network.')
- virtualNetworkResourceId: string
-
- @description('Required. The Resource ID of the subnet to establish the Private Endpoint(s).')
- subnetResourceId: string
-
- @description('Optional. The Resource ID of an existing "cognitiveservices" Private DNS Zone Resource to link to the virtual network. If not provided, a new "cognitiveservices" Private DNS Zone(s) will be created.')
- cogServicesPrivateDnsZoneResourceId: string?
-
- @description('Optional. The Resource ID of an existing "openai" Private DNS Zone Resource to link to the virtual network. If not provided, a new "openai" Private DNS Zone(s) will be created.')
- openAIPrivateDnsZoneResourceId: string?
-}
-
diff --git a/infra/modules/cosmosDb.bicep b/infra/modules/cosmosDb.bicep
index 3936a08e..2fdb19c0 100644
--- a/infra/modules/cosmosDb.bicep
+++ b/infra/modules/cosmosDb.bicep
@@ -1,19 +1,19 @@
-@description('Name of the Cosmos DB Account.')
+@description('Required. Name of the Cosmos DB Account.')
param name string
-@description('Specifies the location for all the Azure resources.')
+@description('Required. Specifies the location for all the Azure resources.')
param location string
@description('Optional. Tags to be applied to the resources.')
param tags object = {}
-@description('Managed Identity princpial to assign data plane roles for the Cosmos DB Account.')
+@description('Required. Managed Identity princpial to assign data plane roles for the Cosmos DB Account.')
param dataAccessIdentityPrincipalId string
@description('Optional. The resource ID of an existing Log Analytics workspace to associate with AI Foundry for monitoring.')
param logAnalyticsWorkspaceResourceId string?
-@description('Indicates whether the single-region account is zone redundant. This property is ignored for multi-region accounts.')
+@description('Required. Indicates whether the single-region account is zone redundant. This property is ignored for multi-region accounts.')
param zoneRedundant bool
@description('Optional. The secondary location for the Cosmos DB Account for failover and multiple writes.')
@@ -30,30 +30,29 @@ param roleAssignments roleAssignmentType[]?
@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true
-module privateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (privateNetworking != null && empty(privateNetworking.?privateDnsZoneResourceId)) {
+module privateDnsZone 'privateDnsZone.bicep' = if (privateNetworking != null && empty(privateNetworking.?privateDnsZoneResourceId)) {
name: take('${name}-documents-pdns-deployment', 64)
params: {
name: 'privatelink.documents.azure.com'
- virtualNetworkLinks: [
- {
- virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
- }
- ]
+ virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
tags: tags
- enableTelemetry: enableTelemetry
}
}
-var privateDnsZoneResourceId = privateNetworking != null ? (empty(privateNetworking.?privateDnsZoneResourceId) ? privateDnsZone.outputs.resourceId ?? '' : privateNetworking.?privateDnsZoneResourceId ?? '') : ''
+var privateDnsZoneResourceId = privateNetworking != null
+ ? (empty(privateNetworking.?privateDnsZoneResourceId)
+ ? privateDnsZone.outputs.resourceId ?? ''
+ : privateNetworking.?privateDnsZoneResourceId ?? '')
+ : ''
resource sqlContributorRoleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2024-11-15' existing = {
name: '${name}/00000000-0000-0000-0000-000000000002'
}
-var databaseName = 'cmsadb'
-var batchContainerName = 'cmsabatch'
-var fileContainerName = 'cmsafile'
-var logContainerName = 'cmsalog'
+var databaseName = 'cmsadb'
+var batchContainerName = 'cmsabatch'
+var fileContainerName = 'cmsafile'
+var logContainerName = 'cmsalog'
module cosmosAccount 'br/public:avm/res/document-db/database-account:0.15.0' = {
name: take('${name}-account-deployment', 64)
@@ -68,72 +67,78 @@ module cosmosAccount 'br/public:avm/res/document-db/database-account:0.15.0' = {
networkRestrictions: {
networkAclBypass: 'AzureServices'
publicNetworkAccess: privateNetworking != null ? 'Disabled' : 'Enabled'
- ipRules: []
+ ipRules: []
virtualNetworkRules: []
}
zoneRedundant: zoneRedundant
automaticFailover: !empty(secondaryLocation)
- failoverLocations: !empty(secondaryLocation) ? [
- {
- failoverPriority: 0
- isZoneRedundant: zoneRedundant
- locationName: location
- }
- {
- failoverPriority: 0
- isZoneRedundant: zoneRedundant
- locationName: secondaryLocation!
- }
- ] : []
+ failoverLocations: !empty(secondaryLocation)
+ ? [
+ {
+ failoverPriority: 0
+ isZoneRedundant: zoneRedundant
+ locationName: location
+ }
+ {
+ failoverPriority: 1
+ isZoneRedundant: zoneRedundant
+ locationName: secondaryLocation!
+ }
+ ]
+ : []
enableMultipleWriteLocations: !empty(secondaryLocation)
backupPolicyType: !empty(secondaryLocation) ? 'Periodic' : 'Continuous'
backupStorageRedundancy: zoneRedundant ? 'Zone' : 'Local'
disableKeyBasedMetadataWriteAccess: false
disableLocalAuthentication: privateNetworking != null
- diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId) ? [{workspaceResourceId: logAnalyticsWorkspaceResourceId}] : []
- privateEndpoints: privateNetworking != null ? [
- {
- privateDnsZoneGroup: {
- privateDnsZoneGroupConfigs: [
- {
- privateDnsZoneResourceId: privateDnsZoneResourceId
+ diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId)
+ ? [{ workspaceResourceId: logAnalyticsWorkspaceResourceId }]
+ : []
+ privateEndpoints: privateNetworking != null
+ ? [
+ {
+ privateDnsZoneGroup: {
+ privateDnsZoneGroupConfigs: [
+ {
+ privateDnsZoneResourceId: privateDnsZoneResourceId
+ }
+ ]
}
- ]
- }
- service: 'Sql'
- subnetResourceId: privateNetworking.?subnetResourceId ?? ''
- }
- ] : []
+ service: 'Sql'
+ subnetResourceId: privateNetworking.?subnetResourceId ?? ''
+ }
+ ]
+ : []
sqlDatabases: [
{
containers: [
{
- indexingPolicy: {
- automatic: true
+ indexingPolicy: {
+ automatic: true
+ }
+ name: batchContainerName
+ paths: [
+ '/batch_id'
+ ]
}
- name: batchContainerName
- paths:[
- '/batch_id'
- ]
- }
- {
- indexingPolicy: {
- automatic: true
+ {
+ indexingPolicy: {
+ automatic: true
+ }
+ name: fileContainerName
+ paths: [
+ '/file_id'
+ ]
}
- name: fileContainerName
- paths:[
- '/file_id'
- ]
- }
- {
- indexingPolicy: {
- automatic: true
+ {
+ indexingPolicy: {
+ automatic: true
+ }
+ name: logContainerName
+ paths: [
+ '/log_id'
+ ]
}
- name: logContainerName
- paths:[
- '/log_id'
- ]
- }
]
name: databaseName
}
@@ -150,22 +155,21 @@ module cosmosAccount 'br/public:avm/res/document-db/database-account:0.15.0' = {
}
}
-output resourceId string = cosmosAccount.outputs.resourceId
+@description('Name of the Cosmos DB Account resource.')
output name string = cosmosAccount.outputs.name
+
+@description('Resource ID of the Cosmos DB Account.')
+output resourceId string = cosmosAccount.outputs.resourceId
+
+@description('Endpoint of the Cosmos DB Account.')
output endpoint string = cosmosAccount.outputs.endpoint
+
+@description('Name of the Cosmos DB database.')
output databaseName string = databaseName
-output containers object = {
- batch: {
- name: batchContainerName
- resourceId: '${cosmosAccount.outputs.resourceId}/sqlDatabases/${databaseName}/containers/${batchContainerName}'
- }
- file: {
- name: fileContainerName
- resourceId: '${cosmosAccount.outputs.resourceId}/sqlDatabases/${databaseName}/containers/${fileContainerName}'
- }
- log: {
- name: logContainerName
- resourceId: '${cosmosAccount.outputs.resourceId}/sqlDatabases/${databaseName}/containers/${logContainerName}'
- }
+@description('Complex object containing the names of the Cosmos DB containers.')
+output containerNames object = {
+ batch: batchContainerName
+ file: fileContainerName
+ log: logContainerName
}
diff --git a/infra/modules/keyVault.bicep b/infra/modules/keyVault.bicep
index ddd8fdde..880f3d0e 100644
--- a/infra/modules/keyVault.bicep
+++ b/infra/modules/keyVault.bicep
@@ -1,7 +1,7 @@
-@description('Name of the Key Vault.')
+@description('Required. Name of the Key Vault.')
param name string
-@description('Specifies the location for all the Azure resources.')
+@description('Required. Specifies the location for all the Azure resources.')
param location string
@description('Optional. Tags to be applied to the resources.')
@@ -32,21 +32,20 @@ param secrets secretType[]?
@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true
-module privateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (privateNetworking != null && empty(privateNetworking.?privateDnsZoneResourceId)) {
+module privateDnsZone 'privateDnsZone.bicep' = if (privateNetworking != null && empty(privateNetworking.?privateDnsZoneResourceId)) {
name: take('${name}-kv-pdns-deployment', 64)
params: {
name: 'privatelink.${toLower(environment().name) == 'azureusgovernment' ? 'vaultcore.usgovcloudapi.net' : 'vaultcore.azure.net'}'
- virtualNetworkLinks: [
- {
- virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
- }
- ]
+ virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
tags: tags
- enableTelemetry: enableTelemetry
}
}
-var privateDnsZoneResourceId = privateNetworking != null ? (empty(privateNetworking.?privateDnsZoneResourceId) ? privateDnsZone.outputs.resourceId ?? '' : privateNetworking.?privateDnsZoneResourceId ?? '') : ''
+var privateDnsZoneResourceId = privateNetworking != null
+ ? (empty(privateNetworking.?privateDnsZoneResourceId)
+ ? privateDnsZone.outputs.resourceId ?? ''
+ : privateNetworking.?privateDnsZoneResourceId ?? '')
+ : ''
module keyvault 'br/public:avm/res/key-vault/vault:0.12.1' = {
name: take('${name}-kv-deployment', 64)
@@ -58,9 +57,9 @@ module keyvault 'br/public:avm/res/key-vault/vault:0.12.1' = {
tags: tags
createMode: 'default'
sku: sku
- publicNetworkAccess: privateNetworking != null ? 'Disabled' : 'Enabled'
+ publicNetworkAccess: privateNetworking != null ? 'Disabled' : 'Enabled'
networkAcls: {
- defaultAction: 'Allow'
+ defaultAction: 'Allow'
}
enableVaultForDeployment: true
enableVaultForDiskEncryption: true
@@ -69,29 +68,36 @@ module keyvault 'br/public:avm/res/key-vault/vault:0.12.1' = {
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: 7
- diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId) ? [
- {
- workspaceResourceId: logAnalyticsWorkspaceResourceId
- }
- ] : []
- privateEndpoints: privateNetworking != null ? [
- {
- privateDnsZoneGroup: {
- privateDnsZoneGroupConfigs: [
- {
- privateDnsZoneResourceId: privateDnsZoneResourceId
+ diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId)
+ ? [
+ {
+ workspaceResourceId: logAnalyticsWorkspaceResourceId
+ }
+ ]
+ : []
+ privateEndpoints: privateNetworking != null
+ ? [
+ {
+ privateDnsZoneGroup: {
+ privateDnsZoneGroupConfigs: [
+ {
+ privateDnsZoneResourceId: privateDnsZoneResourceId
+ }
+ ]
}
- ]
- }
- service: 'vault'
- subnetResourceId: privateNetworking.?subnetResourceId ?? ''
- }
- ] : []
+ service: 'vault'
+ subnetResourceId: privateNetworking.?subnetResourceId ?? ''
+ }
+ ]
+ : []
roleAssignments: roleAssignments
secrets: secrets
enableTelemetry: enableTelemetry
}
}
-output resourceId string = keyvault.outputs.resourceId
+@description('Name of the Key Vault resource.')
output name string = keyvault.outputs.name
+
+@description('Resource ID of the Key Vault.')
+output resourceId string = keyvault.outputs.resourceId
diff --git a/infra/modules/network.bicep b/infra/modules/network.bicep
index 30c24ab8..b4e252f8 100644
--- a/infra/modules/network.bicep
+++ b/infra/modules/network.bicep
@@ -1,11 +1,11 @@
-@description('Named used for all resource naming.')
+@description('Required. Named used for all resource naming.')
param resourcesName string
-@description('Resource ID of the Log Analytics Workspace for monitoring and diagnostics.')
+@description('Required. Resource ID of the Log Analytics Workspace for monitoring and diagnostics.')
param logAnalyticsWorkSpaceResourceId string
@minLength(3)
-@description('Azure region for all services.')
+@description('Required. Azure region for all services.')
param location string
@description('Optional. Tags to be applied to the resources.')
@@ -14,17 +14,13 @@ param tags object = {}
@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true
-@description('Admin username for the VM.')
+@description('Required. Admin username for the VM.')
@secure()
-param vmAdminUsername string
+param vmAdminUsername string
-@description('Admin password for the VM.')
+@description('Required. Admin password for the VM.')
@secure()
-param vmAdminPassword string
-
-
-
-
+param vmAdminPassword string
// Subnet Classless Inter-Doman Routing (CIDR) Sizing Reference Table (Best Practices)
// | CIDR | # of Addresses | # of /24s | Notes |
@@ -55,7 +51,7 @@ param vmAdminPassword string
// - Document subnet usage and purpose in code comments.
// - For AVM modules, ensure only one delegation per subnet and leave delegations empty if not required.
-module network 'network/main.bicep' = {
+module network 'network/main.bicep' = {
name: take('network-${resourcesName}-create', 64)
params: {
resourcesName: resourcesName
@@ -85,6 +81,32 @@ module network 'network/main.bicep' = {
destinationAddressPrefixes: ['10.0.0.0/23']
}
}
+ {
+ name: 'AllowIntraSubnetTraffic'
+ properties: {
+ access: 'Allow'
+ direction: 'Inbound'
+ priority: 200
+ protocol: '*'
+ sourcePortRange: '*'
+ destinationPortRange: '*'
+ sourceAddressPrefixes: ['10.0.0.0/23'] // From same subnet
+ destinationAddressPrefixes: ['10.0.0.0/23'] // To same subnet
+ }
+ }
+ {
+ name: 'AllowAzureLoadBalancer'
+ properties: {
+ access: 'Allow'
+ direction: 'Inbound'
+ priority: 300
+ protocol: '*'
+ sourcePortRange: '*'
+ destinationPortRange: '*'
+ sourceAddressPrefix: 'AzureLoadBalancer'
+ destinationAddressPrefix: '10.0.0.0/23'
+ }
+ }
]
}
delegation: 'Microsoft.App/environments'
@@ -98,30 +120,30 @@ module network 'network/main.bicep' = {
]
bastionConfiguration: {
name: 'bastion-${resourcesName}'
- subnetAddressPrefixes: ['10.0.10.0/23']
+ subnetAddressPrefixes: ['10.0.10.0/26']
}
jumpboxConfiguration: {
name: 'vm-jumpbox-${resourcesName}'
size: 'Standard_D2s_v3'
username: vmAdminUsername
password: vmAdminPassword
- subnet: {
+ subnet: {
name: 'jumpbox'
addressPrefixes: ['10.0.12.0/23'] // /23 (10.0.12.0 - 10.0.13.255), 512 addresses
networkSecurityGroup: {
name: 'jumpbox-nsg'
securityRules: [
{
- name: 'AllowJumpboxInbound'
+ name: 'AllowRdpFromBastion'
properties: {
access: 'Allow'
direction: 'Inbound'
priority: 100
protocol: 'Tcp'
sourcePortRange: '*'
- destinationPortRange: '22'
+ destinationPortRange: '3389'
sourceAddressPrefixes: [
- '10.0.7.0/24' // Azure Bastion subnet as an example here. You can adjust this as needed by adding more
+ '10.0.10.0/26' // Azure Bastion subnet
]
destinationAddressPrefixes: ['10.0.12.0/23']
}
@@ -134,8 +156,20 @@ module network 'network/main.bicep' = {
}
}
+@description('Name of the Virtual Network resource.')
output vnetName string = network.outputs.vnetName
+
+@description('Resource ID of the Virtual Network.')
output vnetResourceId string = network.outputs.vnetResourceId
+@description('Resource ID of the "web" subnet.')
output subnetWebResourceId string = first(filter(network.outputs.subnets, s => s.name == 'web')).?resourceId ?? ''
+
+@description('Resource ID of the "peps" subnet for Private Endpoints.')
output subnetPrivateEndpointsResourceId string = first(filter(network.outputs.subnets, s => s.name == 'peps')).?resourceId ?? ''
+
+@description('Resource ID of the Bastion Host.')
+output bastionResourceId string = network.outputs.bastionHostId
+
+@description('Resource ID of the Jumpbox VM.')
+output jumpboxResourceId string = network.outputs.jumpboxResourceId
diff --git a/infra/modules/privateDnsZone.bicep b/infra/modules/privateDnsZone.bicep
new file mode 100644
index 00000000..225ab756
--- /dev/null
+++ b/infra/modules/privateDnsZone.bicep
@@ -0,0 +1,44 @@
+// This module is here solely to reduce the size of the main bicep file for meet the 4MB limit
+// The AVM Module 'br/public:avm/res/network/private-dns-zone' should be used if size is available.
+
+@description('Required. Private DNS zone name.')
+param name string
+
+@description('Required. The resource ID of the virtual network to link.')
+param virtualNetworkResourceId string
+
+@description('Optional. Tags of the resource.')
+param tags object?
+
+// Private DNS Zones are global resources and may not support all regions, even if those regions support the underlying services.
+// The Private DNS Zone creation should use 'global' as the location.
+resource privateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = {
+ name: name
+ location: 'global' // Private DNS zones must use 'global' as location
+ tags: tags
+}
+
+resource virtualNetworkLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = {
+ name: '${last(split(virtualNetworkResourceId, '/'))}-vnetlink'
+ parent: privateDnsZone
+ location: 'global' // Virtual Network Links must also use 'global' as location
+ tags: tags
+ properties: {
+ registrationEnabled: false
+ virtualNetwork: {
+ id: virtualNetworkResourceId
+ }
+ }
+}
+
+@description('The resource group the private DNS zone was deployed into.')
+output resourceGroupName string = resourceGroup().name
+
+@description('The name of the private DNS zone.')
+output name string = privateDnsZone.name
+
+@description('The resource ID of the private DNS zone.')
+output resourceId string = privateDnsZone.id
+
+@description('The location the resource was deployed into.')
+output location string = privateDnsZone.location
diff --git a/infra/modules/storageAccount.bicep b/infra/modules/storageAccount.bicep
index 4f63e47f..b109de49 100644
--- a/infra/modules/storageAccount.bicep
+++ b/infra/modules/storageAccount.bicep
@@ -1,7 +1,7 @@
-@description('Name of the Storage Account.')
+@description('Required. Name of the Storage Account.')
param name string
-@description('Specifies the location for all the Azure resources.')
+@description('Required. Specifies the location for all the Azure resources.')
param location string
@allowed([
@@ -14,7 +14,7 @@ param location string
'Standard_GZRS'
'Standard_RAGZRS'
])
-@description('Storage Account Sku Name. Defaults to Standard_LRS.')
+@description('Optional. Storage Account Sku Name. Defaults to Standard_LRS.')
param skuName string = 'Standard_LRS'
@description('Optional. Tags to be applied to the resources.')
@@ -36,36 +36,36 @@ param containers array?
@description('Optional. Enable/Disable usage telemetry for module.')
param enableTelemetry bool = true
-module blobPrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (privateNetworking != null && empty(privateNetworking.?blobPrivateDnsZoneResourceId)) {
- name: take('${name}-blob-pdns-deployment', 64)
+module blobPrivateDnsZone 'privateDnsZone.bicep' = if (privateNetworking != null && empty(privateNetworking.?blobPrivateDnsZoneResourceId)) {
+ name: take('${name}-blob-pdns-deployment', 64)
params: {
name: 'privatelink.blob.${environment().suffixes.storage}'
- virtualNetworkLinks: [
- {
- virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
- }
- ]
+ //location: location
+ virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
tags: tags
- enableTelemetry: enableTelemetry
}
}
-module filePrivateDnsZone 'br/public:avm/res/network/private-dns-zone:0.7.1' = if (privateNetworking != null && empty(privateNetworking.?filePrivateDnsZoneResourceId)) {
- name: take('${name}-file-pdns-deployment', 64)
+module filePrivateDnsZone 'privateDnsZone.bicep' = if (privateNetworking != null && empty(privateNetworking.?filePrivateDnsZoneResourceId)) {
+ name: take('${name}-file-pdns-deployment', 64)
params: {
name: 'privatelink.file.${environment().suffixes.storage}'
- virtualNetworkLinks: [
- {
- virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
- }
- ]
+ //location: location
+ virtualNetworkResourceId: privateNetworking.?virtualNetworkResourceId ?? ''
tags: tags
- enableTelemetry: enableTelemetry
}
}
-var blobPrivateDnsZoneResourceId = privateNetworking != null ? (empty(privateNetworking.?blobPrivateDnsZoneResourceId) ? blobPrivateDnsZone.outputs.resourceId ?? '' : privateNetworking.?blobPrivateDnsZoneResourceId) : ''
-var filePrivateDnsZoneResourceId = privateNetworking != null ? (empty(privateNetworking.?filePrivateDnsZoneResourceId) ? filePrivateDnsZone.outputs.resourceId ?? '' : privateNetworking.?filePrivateDnsZoneResourceId) : ''
+var blobPrivateDnsZoneResourceId = privateNetworking != null
+ ? (empty(privateNetworking.?blobPrivateDnsZoneResourceId)
+ ? blobPrivateDnsZone.outputs.resourceId ?? ''
+ : privateNetworking.?blobPrivateDnsZoneResourceId)
+ : ''
+var filePrivateDnsZoneResourceId = privateNetworking != null
+ ? (empty(privateNetworking.?filePrivateDnsZoneResourceId)
+ ? filePrivateDnsZone.outputs.resourceId ?? ''
+ : privateNetworking.?filePrivateDnsZoneResourceId)
+ : ''
module storageAccount 'br/public:avm/res/storage/storage-account:0.20.0' = {
name: take('${name}-sa-deployment', 64)
@@ -89,48 +89,55 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.20.0' = {
enableNfsV3: false
largeFileSharesState: 'Disabled'
networkAcls: {
- defaultAction: 'Allow'
+ defaultAction: privateNetworking != null ? 'Deny' : 'Allow'
bypass: 'AzureServices'
}
supportsHttpsTrafficOnly: true
- diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId) ? [
- {
- workspaceResourceId: logAnalyticsWorkspaceResourceId
- }
- ] : []
- privateEndpoints: privateNetworking != null ? [
- {
- privateDnsZoneGroup: {
- privateDnsZoneGroupConfigs: [
- {
- privateDnsZoneResourceId: blobPrivateDnsZoneResourceId
+ diagnosticSettings: !empty(logAnalyticsWorkspaceResourceId)
+ ? [
+ {
+ workspaceResourceId: logAnalyticsWorkspaceResourceId
+ }
+ ]
+ : []
+ privateEndpoints: privateNetworking != null
+ ? [
+ {
+ privateDnsZoneGroup: {
+ privateDnsZoneGroupConfigs: [
+ {
+ privateDnsZoneResourceId: blobPrivateDnsZoneResourceId
+ }
+ ]
}
- ]
- }
- service: 'blob'
- subnetResourceId: privateNetworking.?subnetResourceId ?? ''
- }
- {
- privateDnsZoneGroup: {
- privateDnsZoneGroupConfigs: [
- {
- privateDnsZoneResourceId: filePrivateDnsZoneResourceId
+ service: 'blob'
+ subnetResourceId: privateNetworking.?subnetResourceId ?? ''
+ }
+ {
+ privateDnsZoneGroup: {
+ privateDnsZoneGroupConfigs: [
+ {
+ privateDnsZoneResourceId: filePrivateDnsZoneResourceId
+ }
+ ]
}
- ]
- }
- service: 'file'
- subnetResourceId: privateNetworking.?subnetResourceId ?? ''
- }
- ] : []
+ service: 'file'
+ subnetResourceId: privateNetworking.?subnetResourceId ?? ''
+ }
+ ]
+ : []
roleAssignments: roleAssignments
- blobServices: {
+ blobServices: {
containers: containers ?? []
}
enableTelemetry: enableTelemetry
}
}
+@description('Name of the Storage Account.')
output name string = storageAccount.outputs.name
+
+@description('Resource ID of the Storage Account.')
output resourceId string = storageAccount.outputs.resourceId
@export()