diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 318dd04cd..dbefeb159 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,30 +1,29 @@ { "name": "Multi Agent Custom Automation Engine Solution Accelerator", - "image": "mcr.microsoft.com/devcontainers/python:3.10", + "image": "mcr.microsoft.com/devcontainers/javascript-node:20-bullseye", "features": { - "ghcr.io/devcontainers/features/azure-cli:1.0.8": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + }, "ghcr.io/azure/azure-dev/azd:latest": {}, - "ghcr.io/rchaganti/vsc-devcontainer-features/azurebicep:1.0.5": {} + "ghcr.io/devcontainers/features/azure-cli:1": {} }, - - "postCreateCommand": "sudo chmod +x .devcontainer/setupEnv.sh && ./.devcontainer/setupEnv.sh", - "customizations": { "vscode": { "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "GitHub.vscode-github-actions", "ms-azuretools.azure-dev", + "ms-azuretools.vscode-azurefunctions", "ms-azuretools.vscode-bicep", - "ms-python.python" - ] - }, - "codespaces": { - "openFiles": [ - "README.md" + "ms-azuretools.vscode-docker", + "ms-vscode.js-debug", + "ms-vscode.vscode-node-azure-pack" ] } }, - - "remoteUser": "vscode", + "forwardPorts": [3000, 3100], + "remoteUser": "node", "hostRequirements": { "memory": "8gb" } diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml new file mode 100644 index 000000000..47b843c78 --- /dev/null +++ b/.github/workflows/azure-dev.yml @@ -0,0 +1,35 @@ +name: Azure Template Validation +on: + workflow_dispatch: + +permissions: + contents: read + id-token: write + pull-requests: write + +jobs: + template_validation_job: + runs-on: ubuntu-latest + name: Template validation + + steps: + # Step 1: Checkout the code from your repository + - name: Checkout code + uses: actions/checkout@v4 + + # Step 2: Validate the Azure template using microsoft/template-validation-action + - name: Validate Azure Template + uses: microsoft/template-validation-action@v0.3.5 + id: validation + env: + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Step 3: Print the result of the validation + - name: Print result + run: cat ${{ steps.validation.outputs.resultFile }} diff --git a/.gitignore b/.gitignore index 4497c7182..cf66bea81 100644 --- a/.gitignore +++ b/.gitignore @@ -456,4 +456,5 @@ __pycache__/ *.xsd.cs *.whl -!autogen_core-0.3.dev0-py3-none-any.whl \ No newline at end of file +!autogen_core-0.3.dev0-py3-none-any.whl +.azure diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 000000000..226ba7af9 --- /dev/null +++ b/azure.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json +environment: + name: multi-agent-custom-automation-engine-solution-accelerator + location: eastus +name: multi-agent-custom-automation-engine-solution-accelerator +# metadata: +# template: azd-init@1.13.0 +parameters: + baseUrl: + type: string + default: 'https://github.com/TravisHilbert/Modernize-your-code-solution-accelerator' +deployment: + mode: Incremental + template: ./infra/main.bicep # Path to the main.bicep file inside the 'deployment' folder diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 000000000..1533dee56 --- /dev/null +++ b/infra/abbreviations.json @@ -0,0 +1,136 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationStores": "appcs-", + "appManagedEnvironments": "cae-", + "appContainerApps": "ca-", + "authorizationPolicyDefinitions": "policy-", + "automationAutomationAccounts": "aa-", + "blueprintBlueprints": "bp-", + "blueprintBlueprintsArtifacts": "bpa-", + "cacheRedis": "redis-", + "cdnProfiles": "cdnp-", + "cdnProfilesEndpoints": "cdne-", + "cognitiveServicesAccounts": "cog-", + "cognitiveServicesFormRecognizer": "cog-fr-", + "cognitiveServicesTextAnalytics": "cog-ta-", + "computeAvailabilitySets": "avail-", + "computeCloudServices": "cld-", + "computeDiskEncryptionSets": "des", + "computeDisks": "disk", + "computeDisksOs": "osdisk", + "computeGalleries": "gal", + "computeSnapshots": "snap-", + "computeVirtualMachines": "vm", + "computeVirtualMachineScaleSets": "vmss-", + "containerInstanceContainerGroups": "ci", + "containerRegistryRegistries": "cr", + "containerServiceManagedClusters": "aks-", + "databricksWorkspaces": "dbw-", + "dataFactoryFactories": "adf-", + "dataLakeAnalyticsAccounts": "dla", + "dataLakeStoreAccounts": "dls", + "dataMigrationServices": "dms-", + "dBforMySQLServers": "mysql-", + "dBforPostgreSQLServers": "psql-", + "devicesIotHubs": "iot-", + "devicesProvisioningServices": "provs-", + "devicesProvisioningServicesCertificates": "pcert-", + "documentDBDatabaseAccounts": "cosmos-", + "documentDBMongoDatabaseAccounts": "cosmon-", + "eventGridDomains": "evgd-", + "eventGridDomainsTopics": "evgt-", + "eventGridEventSubscriptions": "evgs-", + "eventHubNamespaces": "evhns-", + "eventHubNamespacesEventHubs": "evh-", + "hdInsightClustersHadoop": "hadoop-", + "hdInsightClustersHbase": "hbase-", + "hdInsightClustersKafka": "kafka-", + "hdInsightClustersMl": "mls-", + "hdInsightClustersSpark": "spark-", + "hdInsightClustersStorm": "storm-", + "hybridComputeMachines": "arcs-", + "insightsActionGroups": "ag-", + "insightsComponents": "appi-", + "keyVaultVaults": "kv-", + "kubernetesConnectedClusters": "arck", + "kustoClusters": "dec", + "kustoClustersDatabases": "dedb", + "logicIntegrationAccounts": "ia-", + "logicWorkflows": "logic-", + "machineLearningServicesWorkspaces": "mlw-", + "managedIdentityUserAssignedIdentities": "id-", + "managementManagementGroups": "mg-", + "migrateAssessmentProjects": "migr-", + "networkApplicationGateways": "agw-", + "networkApplicationSecurityGroups": "asg-", + "networkAzureFirewalls": "afw-", + "networkBastionHosts": "bas-", + "networkConnections": "con-", + "networkDnsZones": "dnsz-", + "networkExpressRouteCircuits": "erc-", + "networkFirewallPolicies": "afwp-", + "networkFirewallPoliciesWebApplication": "waf", + "networkFirewallPoliciesRuleGroups": "wafrg", + "networkFrontDoors": "fd-", + "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", + "networkLoadBalancersExternal": "lbe-", + "networkLoadBalancersInternal": "lbi-", + "networkLoadBalancersInboundNatRules": "rule-", + "networkLocalNetworkGateways": "lgw-", + "networkNatGateways": "ng-", + "networkNetworkInterfaces": "nic-", + "networkNetworkSecurityGroups": "nsg-", + "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", + "networkNetworkWatchers": "nw-", + "networkPrivateDnsZones": "pdnsz-", + "networkPrivateLinkServices": "pl-", + "networkPublicIPAddresses": "pip-", + "networkPublicIPPrefixes": "ippre-", + "networkRouteFilters": "rf-", + "networkRouteTables": "rt-", + "networkRouteTablesRoutes": "udr-", + "networkTrafficManagerProfiles": "traf-", + "networkVirtualNetworkGateways": "vgw-", + "networkVirtualNetworks": "vnet-", + "networkVirtualNetworksSubnets": "snet-", + "networkVirtualNetworksVirtualNetworkPeerings": "peer-", + "networkVirtualWans": "vwan-", + "networkVpnGateways": "vpng-", + "networkVpnGatewaysVpnConnections": "vcn-", + "networkVpnGatewaysVpnSites": "vst-", + "notificationHubsNamespaces": "ntfns-", + "notificationHubsNamespacesNotificationHubs": "ntf-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "powerBIDedicatedCapacities": "pbi-", + "purviewAccounts": "pview-", + "recoveryServicesVaults": "rsv-", + "resourcesResourceGroups": "rg-", + "searchSearchServices": "srch-", + "serviceBusNamespaces": "sb-", + "serviceBusNamespacesQueues": "sbq-", + "serviceBusNamespacesTopics": "sbt-", + "serviceEndPointPolicies": "se-", + "serviceFabricClusters": "sf-", + "signalRServiceSignalR": "sigr", + "sqlManagedInstances": "sqlmi-", + "sqlServers": "sql-", + "sqlServersDataWarehouse": "sqldw-", + "sqlServersDatabases": "sqldb-", + "sqlServersDatabasesStretch": "sqlstrdb-", + "storageStorageAccounts": "st", + "storageStorageAccountsVm": "stvm", + "storSimpleManagers": "ssimp", + "streamAnalyticsCluster": "asa-", + "synapseWorkspaces": "syn", + "synapseWorkspacesAnalyticsWorkspaces": "synw", + "synapseWorkspacesSqlPoolsDedicated": "syndp", + "synapseWorkspacesSqlPoolsSpark": "synsp", + "timeSeriesInsightsEnvironments": "tsi-", + "webServerFarms": "plan-", + "webSitesAppService": "app-", + "webSitesAppServiceEnvironment": "ase-", + "webSitesFunctions": "func-", + "webStaticSites": "stapp-" +} diff --git a/infra/deploy_ai_foundry.bicep b/infra/deploy_ai_foundry.bicep new file mode 100644 index 000000000..a38f7d7ea --- /dev/null +++ b/infra/deploy_ai_foundry.bicep @@ -0,0 +1,300 @@ +// Creates Azure dependent resources for Azure AI studio +param solutionName string +param solutionLocation string +param keyVaultName string +param gptModelName string +param gptModelVersion string +param managedIdentityObjectId string +param aiServicesEndpoint string +param aiServicesKey string +param aiServicesId string + +var storageName = '${solutionName}hubstorage' +var storageSkuName = 'Standard_LRS' +var aiServicesName = '${solutionName}-aiservices' +var workspaceName = '${solutionName}-workspace' +var keyvaultName = '${solutionName}-kv' +var location = solutionLocation +var aiHubName = '${solutionName}-aihub' +var aiHubFriendlyName = aiHubName +var aiHubDescription = 'AI Hub for KM template' +var aiProjectName = '${solutionName}-aiproject' +var aiProjectFriendlyName = aiProjectName +var aiSearchName = '${solutionName}-search' + + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: workspaceName + location: location + tags: {} + properties: { + retentionInDays: 30 + sku: { + name: 'PerGB2018' + } + } +} + + +var storageNameCleaned = replace(storageName, '-', '') + + +resource storage 'Microsoft.Storage/storageAccounts@2022-09-01' = { + name: storageNameCleaned + location: location + sku: { + name: storageSkuName + } + kind: 'StorageV2' + identity: { + type: 'SystemAssigned' + } + properties: { + accessTier: 'Hot' + allowBlobPublicAccess: false + allowCrossTenantReplication: false + allowSharedKeyAccess: false + encryption: { + keySource: 'Microsoft.Storage' + requireInfrastructureEncryption: false + services: { + blob: { + enabled: true + keyType: 'Account' + } + file: { + enabled: true + keyType: 'Account' + } + queue: { + enabled: true + keyType: 'Service' + } + table: { + enabled: true + keyType: 'Service' + } + } + } + isHnsEnabled: false + isNfsV3Enabled: false + keyPolicy: { + keyExpirationPeriodInDays: 7 + } + largeFileSharesState: 'Disabled' + minimumTlsVersion: 'TLS1_2' + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Allow' + } + supportsHttpsTrafficOnly: true + } +} + +@description('This is the built-in Storage Blob Data Contributor.') +resource blobDataContributor 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { + scope: subscription() + name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' +} + +resource storageroleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, managedIdentityObjectId, blobDataContributor.id) + scope: storage + properties: { + principalId: managedIdentityObjectId + roleDefinitionId: blobDataContributor.id + principalType: 'ServicePrincipal' + } +} + +resource aiHub 'Microsoft.MachineLearningServices/workspaces@2023-08-01-preview' = { + name: aiHubName + location: location + identity: { + type: 'SystemAssigned' + } + properties: { + // organization + friendlyName: aiHubFriendlyName + description: aiHubDescription + + // dependent resources + keyVault: keyVault.id + storageAccount: storage.id + } + kind: 'hub' + + resource aiServicesConnection 'connections@2024-07-01-preview' = { + name: '${aiHubName}-connection-AzureOpenAI' + properties: { + category: 'AIServices' + target: aiServicesEndpoint + authType: 'ApiKey' + isSharedToAll: true + credentials: { + key: aiServicesKey + } + metadata: { + ApiType: 'Azure' + ResourceId: aiServicesId + } + } + } +} + +resource aiHubProject 'Microsoft.MachineLearningServices/workspaces@2024-01-01-preview' = { + name: aiProjectName + location: location + kind: 'Project' + identity: { + type: 'SystemAssigned' + } + properties: { + friendlyName: aiProjectFriendlyName + hubResourceId: aiHub.id + } +} + +resource tenantIdEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'TENANT-ID' + properties: { + value: subscription().tenantId + } +} + +resource azureOpenAIInferenceEndpoint 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-OPENAI-INFERENCE-ENDPOINT' + properties: { + value:'' + } +} + +resource azureOpenAIInferenceKey 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-OPENAI-INFERENCE-KEY' + properties: { + value:'' + } +} + +resource azureOpenAIApiKeyEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-OPENAI-KEY' + properties: { + value: aiServicesKey //aiServices_m.listKeys().key1 + } +} + +resource azureOpenAIDeploymentModel 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-OPEN-AI-DEPLOYMENT-MODEL' + properties: { + value: gptModelName + } +} + +resource azureOpenAIApiVersionEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-OPENAI-PREVIEW-API-VERSION' + properties: { + value: gptModelVersion //'2024-02-15-preview' + } +} + +resource azureOpenAIEndpointEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-OPENAI-ENDPOINT' + properties: { + value: aiServicesEndpoint//aiServices_m.properties.endpoint + } +} + +resource azureAIProjectConnectionStringEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-AI-PROJECT-CONN-STRING' + properties: { + value: '${split(aiHubProject.properties.discoveryUrl, '/')[2]};${subscription().subscriptionId};${resourceGroup().name};${aiHubProject.name}' + } +} + +resource azureOpenAICUApiVersionEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-OPENAI-CU-VERSION' + properties: { + value: '?api-version=2024-12-01-preview' + } +} + +resource azureSearchIndexEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-SEARCH-INDEX' + properties: { + value: 'transcripts_index' + } +} + +resource cogServiceEndpointEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'COG-SERVICES-ENDPOINT' + properties: { + value: aiServicesEndpoint + } +} + +resource cogServiceKeyEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'COG-SERVICES-KEY' + properties: { + value: aiServicesKey + } +} + +resource cogServiceNameEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'COG-SERVICES-NAME' + properties: { + value: aiServicesName + } +} + +resource azureSubscriptionIdEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-SUBSCRIPTION-ID' + properties: { + value: subscription().subscriptionId + } +} + +resource resourceGroupNameEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-RESOURCE-GROUP' + properties: { + value: resourceGroup().name + } +} + +resource azureLocatioEntry 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = { + parent: keyVault + name: 'AZURE-LOCATION' + properties: { + value: solutionLocation + } +} + +output keyvaultName string = keyvaultName +output keyvaultId string = keyVault.id + +output aiServicesName string = aiServicesName +output aiSearchName string = aiSearchName +output aiProjectName string = aiHubProject.name + +output storageAccountName string = storageNameCleaned + +output logAnalyticsId string = logAnalytics.id +output storageAccountId string = storage.id diff --git a/infra/deploy_keyvault.bicep b/infra/deploy_keyvault.bicep new file mode 100644 index 000000000..5222a9f89 --- /dev/null +++ b/infra/deploy_keyvault.bicep @@ -0,0 +1,67 @@ +@minLength(3) +@maxLength(15) +@description('Solution Name') +param solutionName string +param solutionLocation string +param managedIdentityObjectId string + +var keyvaultName = '${solutionName}-kv' + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: keyvaultName + location: solutionLocation + properties: { + createMode: 'default' + accessPolicies: [ + { + objectId: managedIdentityObjectId + permissions: { + certificates: [ + 'all' + ] + keys: [ + 'all' + ] + secrets: [ + 'all' + ] + storage: [ + 'all' + ] + } + tenantId: subscription().tenantId + } + ] + enabledForDeployment: true + enabledForDiskEncryption: true + enabledForTemplateDeployment: true + enableSoftDelete: false + enableRbacAuthorization: true + enablePurgeProtection: true + publicNetworkAccess: 'enabled' + sku: { + family: 'A' + name: 'standard' + } + softDeleteRetentionInDays: 7 + tenantId: subscription().tenantId + } +} + +@description('This is the built-in Key Vault Administrator role.') +resource kvAdminRole 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { + scope: resourceGroup() + name: '00482a5a-887f-4fb3-b363-3b7fe8e74483' +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, managedIdentityObjectId, kvAdminRole.id) + properties: { + principalId: managedIdentityObjectId + roleDefinitionId:kvAdminRole.id + principalType: 'ServicePrincipal' + } +} + +output keyvaultName string = keyvaultName +output keyvaultId string = keyVault.id diff --git a/infra/deploy_managed_identity.bicep b/infra/deploy_managed_identity.bicep new file mode 100644 index 000000000..08a2b51a8 --- /dev/null +++ b/infra/deploy_managed_identity.bicep @@ -0,0 +1,50 @@ +// ========== Managed Identity ========== // +targetScope = 'resourceGroup' + +@minLength(3) +@maxLength(15) +@description('Solution Name') +param solutionName string + +@description('Solution Location') +//param solutionLocation string +param managedIdentityId string +param managedIdentityPropPrin string +param managedIdentityLocation string +@description('Name') +param miName string = '${ solutionName }-managed-identity' + +// resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { +// name: miName +// location: solutionLocation +// tags: { +// app: solutionName +// location: solutionLocation +// } +// } + +@description('This is the built-in owner role. See https://docs.microsoft.com/azure/role-based-access-control/built-in-roles#owner') +resource ownerRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = { + scope: resourceGroup() + name: '8e3af657-a8ff-443c-a75c-2fe8c4bcb635' +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(resourceGroup().id, managedIdentityId, ownerRoleDefinition.id) + properties: { + principalId: managedIdentityPropPrin + roleDefinitionId: ownerRoleDefinition.id + principalType: 'ServicePrincipal' + } +} + + +output managedIdentityOutput object = { + id: managedIdentityId + objectId: managedIdentityPropPrin + resourceId: managedIdentityId + location: managedIdentityLocation + name: miName +} + +output managedIdentityId string = managedIdentityId diff --git a/infra/macae-continer-oc.json b/infra/macae-continer-oc.json new file mode 100644 index 000000000..40c676ebe --- /dev/null +++ b/infra/macae-continer-oc.json @@ -0,0 +1,458 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "9524414973084491660" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "EastUS2", + "metadata": { + "description": "Location for all resources." + } + }, + "azureOpenAILocation": { + "type": "string", + "defaultValue": "EastUS", + "metadata": { + "description": "Location for OpenAI resources." + } + }, + "prefix": { + "type": "string", + "defaultValue": "macae", + "metadata": { + "description": "A prefix to add to the start of all resource names. Note: A \"unique\" suffix will also be added" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to apply to all deployed resources" + } + }, + "resourceSize": { + "type": "object", + "properties": { + "gpt4oCapacity": { + "type": "int" + }, + "containerAppSize": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "minReplicas": { + "type": "int" + }, + "maxReplicas": { + "type": "int" + } + } + } + }, + "defaultValue": { + "gpt4oCapacity": 50, + "containerAppSize": { + "cpu": "2.0", + "memory": "4.0Gi", + "minReplicas": 1, + "maxReplicas": 1 + } + }, + "metadata": { + "description": "The size of the resources to deploy, defaults to a mini size" + } + } + }, + "variables": { + "appVersion": "latest", + "resgistryName": "biabcontainerreg", + "dockerRegistryUrl": "[format('https://{0}.azurecr.io', variables('resgistryName'))]", + "backendDockerImageURL": "[format('{0}.azurecr.io/macaebackend:{1}', variables('resgistryName'), variables('appVersion'))]", + "frontendDockerImageURL": "[format('{0}.azurecr.io/macaefrontend:{1}', variables('resgistryName'), variables('appVersion'))]", + "uniqueNameFormat": "[format('{0}-{{0}}-{1}', parameters('prefix'), uniqueString(resourceGroup().id, parameters('prefix')))]", + "aoaiApiVersion": "2024-08-01-preview" + }, + "resources": { + "openai::gpt4o": { + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2023-10-01-preview", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'openai'), 'gpt-4o')]", + "sku": { + "name": "GlobalStandard", + "capacity": "[parameters('resourceSize').gpt4oCapacity]" + }, + "properties": { + "model": { + "format": "OpenAI", + "name": "gpt-4o", + "version": "2024-08-06" + }, + "versionUpgradeOption": "NoAutoUpgrade" + }, + "dependsOn": [ + "openai" + ] + }, + "cosmos::autogenDb::memoryContainer": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}/{2}', format(variables('uniqueNameFormat'), 'cosmos'), 'autogen', 'memory')]", + "properties": { + "resource": { + "id": "memory", + "partitionKey": { + "kind": "Hash", + "version": 2, + "paths": [ + "/session_id" + ] + } + } + }, + "dependsOn": [ + "cosmos::autogenDb" + ] + }, + "cosmos::contributorRoleDefinition": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002')]", + "dependsOn": [ + "cosmos" + ] + }, + "cosmos::autogenDb": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'cosmos'), 'autogen')]", + "properties": { + "resource": { + "id": "autogen", + "createMode": "Default" + } + }, + "dependsOn": [ + "cosmos" + ] + }, + "containerAppEnv::aspireDashboard": { + "type": "Microsoft.App/managedEnvironments/dotNetComponents", + "apiVersion": "2024-02-02-preview", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'containerapp'), 'aspire-dashboard')]", + "properties": { + "componentType": "AspireDashboard" + }, + "dependsOn": [ + "containerAppEnv" + ] + }, + "logAnalytics": { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2023-09-01", + "name": "[format(variables('uniqueNameFormat'), 'logs')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "retentionInDays": 30, + "sku": { + "name": "PerGB2018" + } + } + }, + "appInsights": { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02-preview", + "name": "[format(variables('uniqueNameFormat'), 'appins')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', format(variables('uniqueNameFormat'), 'logs'))]" + }, + "dependsOn": [ + "logAnalytics" + ] + }, + "openai": { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2023-10-01-preview", + "name": "[format(variables('uniqueNameFormat'), 'openai')]", + "location": "[parameters('azureOpenAILocation')]", + "tags": "[parameters('tags')]", + "kind": "OpenAI", + "sku": { + "name": "S0" + }, + "properties": { + "customSubDomainName": "[format(variables('uniqueNameFormat'), 'openai')]" + } + }, + "aoaiUserRoleDefinition": { + "existing": true, + "type": "Microsoft.Authorization/roleDefinitions", + "apiVersion": "2022-05-01-preview", + "name": "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" + }, + "acaAoaiRoleAssignment": { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', format(variables('uniqueNameFormat'), 'openai'))]", + "name": "[guid(resourceId('Microsoft.App/containerApps', format('{0}-backend', parameters('prefix'))), resourceId('Microsoft.CognitiveServices/accounts', format(variables('uniqueNameFormat'), 'openai')), resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'))]", + "properties": { + "principalId": "[reference('containerApp', '2024-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "containerApp", + "openai" + ] + }, + "cosmos": { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-05-15", + "name": "[format(variables('uniqueNameFormat'), 'cosmos')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "GlobalDocumentDB", + "properties": { + "databaseAccountOfferType": "Standard", + "enableFreeTier": false, + "locations": [ + { + "failoverPriority": 0, + "locationName": "[parameters('location')]" + } + ], + "capabilities": [ + { + "name": "EnableServerless" + } + ] + } + }, + "pullIdentity": { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-07-31-preview", + "name": "[format(variables('uniqueNameFormat'), 'containerapp-pull')]", + "location": "[parameters('location')]" + }, + "containerAppEnv": { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2024-03-01", + "name": "[format(variables('uniqueNameFormat'), 'containerapp')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "daprAIConnectionString": "[reference('appInsights').ConnectionString]", + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference('logAnalytics').customerId]", + "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', format(variables('uniqueNameFormat'), 'logs')), '2023-09-01').primarySharedKey]" + } + } + }, + "dependsOn": [ + "appInsights", + "logAnalytics" + ] + }, + "acaCosomsRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'cosmos'), guid(resourceId('Microsoft.App/containerApps', format('{0}-backend', parameters('prefix'))), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002')))]", + "properties": { + "principalId": "[reference('containerApp', '2024-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002')]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', format(variables('uniqueNameFormat'), 'cosmos'))]" + }, + "dependsOn": [ + "containerApp", + "cosmos" + ] + }, + "containerApp": { + "type": "Microsoft.App/containerApps", + "apiVersion": "2024-03-01", + "name": "[format('{0}-backend', parameters('prefix'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": { + "type": "SystemAssigned, UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format(variables('uniqueNameFormat'), 'containerapp-pull')))]": {} + } + }, + "properties": { + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', format(variables('uniqueNameFormat'), 'containerapp'))]", + "configuration": { + "ingress": { + "targetPort": 8000, + "external": true, + "corsPolicy": { + "allowedOrigins": [ + "[format('https://{0}.azurewebsites.net', format(variables('uniqueNameFormat'), 'frontend'))]", + "[format('http://{0}.azurewebsites.net', format(variables('uniqueNameFormat'), 'frontend'))]" + ] + } + }, + "activeRevisionsMode": "Single" + }, + "template": { + "scale": { + "minReplicas": "[parameters('resourceSize').containerAppSize.minReplicas]", + "maxReplicas": "[parameters('resourceSize').containerAppSize.maxReplicas]", + "rules": [ + { + "name": "http-scaler", + "http": { + "metadata": { + "concurrentRequests": "100" + } + } + } + ] + }, + "containers": [ + { + "name": "backend", + "image": "[variables('backendDockerImageURL')]", + "resources": { + "cpu": "[json(parameters('resourceSize').containerAppSize.cpu)]", + "memory": "[parameters('resourceSize').containerAppSize.memory]" + }, + "env": [ + { + "name": "COSMOSDB_ENDPOINT", + "value": "[reference('cosmos').documentEndpoint]" + }, + { + "name": "COSMOSDB_DATABASE", + "value": "autogen" + }, + { + "name": "COSMOSDB_CONTAINER", + "value": "memory" + }, + { + "name": "AZURE_OPENAI_ENDPOINT", + "value": "[reference('openai').endpoint]" + }, + { + "name": "AZURE_OPENAI_DEPLOYMENT_NAME", + "value": "gpt-4o" + }, + { + "name": "AZURE_OPENAI_API_VERSION", + "value": "[variables('aoaiApiVersion')]" + }, + { + "name": "FRONTEND_SITE_NAME", + "value": "[format('https://{0}.azurewebsites.net', format(variables('uniqueNameFormat'), 'frontend'))]" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference('appInsights').ConnectionString]" + } + ] + } + ] + } + }, + "dependsOn": [ + "appInsights", + "cosmos::autogenDb", + "containerAppEnv", + "cosmos", + "openai::gpt4o", + "cosmos::autogenDb::memoryContainer", + "openai", + "pullIdentity" + ], + "metadata": { + "description": "" + } + }, + "frontendAppServicePlan": { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2021-02-01", + "name": "[format(variables('uniqueNameFormat'), 'frontend-plan')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "P1v2", + "capacity": 1, + "tier": "PremiumV2" + }, + "properties": { + "reserved": true + }, + "kind": "linux" + }, + "frontendAppService": { + "type": "Microsoft.Web/sites", + "apiVersion": "2021-02-01", + "name": "[format(variables('uniqueNameFormat'), 'frontend')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "app,linux,container", + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format(variables('uniqueNameFormat'), 'frontend-plan'))]", + "reserved": true, + "siteConfig": { + "linuxFxVersion": "[format('DOCKER|{0}', variables('frontendDockerImageURL'))]", + "appSettings": [ + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "[variables('dockerRegistryUrl')]" + }, + { + "name": "WEBSITES_PORT", + "value": "3000" + }, + { + "name": "WEBSITES_CONTAINER_START_TIME_LIMIT", + "value": "1800" + }, + { + "name": "BACKEND_API_URL", + "value": "[format('https://{0}', reference('containerApp').configuration.ingress.fqdn)]" + } + ] + } + }, + "identity": { + "type": "SystemAssigned,UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format(variables('uniqueNameFormat'), 'containerapp-pull')))]": {} + } + }, + "dependsOn": [ + "containerApp", + "frontendAppServicePlan", + "pullIdentity" + ] + } + }, + "outputs": { + "cosmosAssignCli": { + "type": "string", + "value": "[format('az cosmosdb sql role assignment create --resource-group \"{0}\" --account-name \"{1}\" --role-definition-id \"{2}\" --scope \"{3}\" --principal-id \"fill-in\"', resourceGroup().name, format(variables('uniqueNameFormat'), 'cosmos'), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002'), resourceId('Microsoft.DocumentDB/databaseAccounts', format(variables('uniqueNameFormat'), 'cosmos')))]" + } + } +} \ No newline at end of file diff --git a/infra/macae-continer.json b/infra/macae-continer.json new file mode 100644 index 000000000..db8539188 --- /dev/null +++ b/infra/macae-continer.json @@ -0,0 +1,458 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "8201361287909347586" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "EastUS2", + "metadata": { + "description": "Location for all resources." + } + }, + "azureOpenAILocation": { + "type": "string", + "defaultValue": "EastUS", + "metadata": { + "description": "Location for OpenAI resources." + } + }, + "prefix": { + "type": "string", + "defaultValue": "macae", + "metadata": { + "description": "A prefix to add to the start of all resource names. Note: A \"unique\" suffix will also be added" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to apply to all deployed resources" + } + }, + "resourceSize": { + "type": "object", + "properties": { + "gpt4oCapacity": { + "type": "int" + }, + "containerAppSize": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "minReplicas": { + "type": "int" + }, + "maxReplicas": { + "type": "int" + } + } + } + }, + "defaultValue": { + "gpt4oCapacity": 50, + "containerAppSize": { + "cpu": "2.0", + "memory": "4.0Gi", + "minReplicas": 1, + "maxReplicas": 1 + } + }, + "metadata": { + "description": "The size of the resources to deploy, defaults to a mini size" + } + } + }, + "variables": { + "appVersion": "latest", + "resgistryName": "biabcontainerreg", + "dockerRegistryUrl": "[format('https://{0}.azurecr.io', variables('resgistryName'))]", + "backendDockerImageURL": "[format('{0}.azurecr.io/macaebackend:{1}', variables('resgistryName'), variables('appVersion'))]", + "frontendDockerImageURL": "[format('{0}.azurecr.io/macaefrontend:{1}', variables('resgistryName'), variables('appVersion'))]", + "uniqueNameFormat": "[format('{0}-{{0}}-{1}', parameters('prefix'), uniqueString(resourceGroup().id, parameters('prefix')))]", + "aoaiApiVersion": "2024-08-01-preview" + }, + "resources": { + "openai::gpt4o": { + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2023-10-01-preview", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'openai'), 'gpt-4o')]", + "sku": { + "name": "GlobalStandard", + "capacity": "[parameters('resourceSize').gpt4oCapacity]" + }, + "properties": { + "model": { + "format": "OpenAI", + "name": "gpt-4o", + "version": "2024-08-06" + }, + "versionUpgradeOption": "NoAutoUpgrade" + }, + "dependsOn": [ + "openai" + ] + }, + "cosmos::autogenDb::memoryContainer": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}/{2}', format(variables('uniqueNameFormat'), 'cosmos'), 'autogen', 'memory')]", + "properties": { + "resource": { + "id": "memory", + "partitionKey": { + "kind": "Hash", + "version": 2, + "paths": [ + "/session_id" + ] + } + } + }, + "dependsOn": [ + "cosmos::autogenDb" + ] + }, + "cosmos::contributorRoleDefinition": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002')]", + "dependsOn": [ + "cosmos" + ] + }, + "cosmos::autogenDb": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'cosmos'), 'autogen')]", + "properties": { + "resource": { + "id": "autogen", + "createMode": "Default" + } + }, + "dependsOn": [ + "cosmos" + ] + }, + "containerAppEnv::aspireDashboard": { + "type": "Microsoft.App/managedEnvironments/dotNetComponents", + "apiVersion": "2024-02-02-preview", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'containerapp'), 'aspire-dashboard')]", + "properties": { + "componentType": "AspireDashboard" + }, + "dependsOn": [ + "containerAppEnv" + ] + }, + "logAnalytics": { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2023-09-01", + "name": "[format(variables('uniqueNameFormat'), 'logs')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "retentionInDays": 30, + "sku": { + "name": "PerGB2018" + } + } + }, + "appInsights": { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02-preview", + "name": "[format(variables('uniqueNameFormat'), 'appins')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', format(variables('uniqueNameFormat'), 'logs'))]" + }, + "dependsOn": [ + "logAnalytics" + ] + }, + "openai": { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2023-10-01-preview", + "name": "[format(variables('uniqueNameFormat'), 'openai')]", + "location": "[parameters('azureOpenAILocation')]", + "tags": "[parameters('tags')]", + "kind": "OpenAI", + "sku": { + "name": "S0" + }, + "properties": { + "customSubDomainName": "[format(variables('uniqueNameFormat'), 'openai')]" + } + }, + "aoaiUserRoleDefinition": { + "existing": true, + "type": "Microsoft.Authorization/roleDefinitions", + "apiVersion": "2022-05-01-preview", + "name": "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" + }, + "acaAoaiRoleAssignment": { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', format(variables('uniqueNameFormat'), 'openai'))]", + "name": "[guid(resourceId('Microsoft.App/containerApps', format('{0}-backend', parameters('prefix'))), resourceId('Microsoft.CognitiveServices/accounts', format(variables('uniqueNameFormat'), 'openai')), resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'))]", + "properties": { + "principalId": "[reference('containerApp', '2024-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "containerApp", + "openai" + ] + }, + "cosmos": { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-05-15", + "name": "[format(variables('uniqueNameFormat'), 'cosmos')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "GlobalDocumentDB", + "properties": { + "databaseAccountOfferType": "Standard", + "enableFreeTier": false, + "locations": [ + { + "failoverPriority": 0, + "locationName": "[parameters('location')]" + } + ], + "capabilities": [ + { + "name": "EnableServerless" + } + ] + } + }, + "pullIdentity": { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-07-31-preview", + "name": "[format(variables('uniqueNameFormat'), 'containerapp-pull')]", + "location": "[parameters('location')]" + }, + "containerAppEnv": { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2024-03-01", + "name": "[format(variables('uniqueNameFormat'), 'containerapp')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "daprAIConnectionString": "[reference('appInsights').ConnectionString]", + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference('logAnalytics').customerId]", + "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', format(variables('uniqueNameFormat'), 'logs')), '2023-09-01').primarySharedKey]" + } + } + }, + "dependsOn": [ + "appInsights", + "logAnalytics" + ] + }, + "acaCosomsRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'cosmos'), guid(resourceId('Microsoft.App/containerApps', format('{0}-backend', parameters('prefix'))), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002')))]", + "properties": { + "principalId": "[reference('containerApp', '2024-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002')]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', format(variables('uniqueNameFormat'), 'cosmos'))]" + }, + "dependsOn": [ + "containerApp", + "cosmos" + ] + }, + "containerApp": { + "type": "Microsoft.App/containerApps", + "apiVersion": "2024-03-01", + "name": "[format('{0}-backend', parameters('prefix'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": { + "type": "SystemAssigned, UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format(variables('uniqueNameFormat'), 'containerapp-pull')))]": {} + } + }, + "properties": { + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', format(variables('uniqueNameFormat'), 'containerapp'))]", + "configuration": { + "ingress": { + "targetPort": 8000, + "external": true, + "corsPolicy": { + "allowedOrigins": [ + "[format('https://{0}.azurewebsites.net', format(variables('uniqueNameFormat'), 'frontend'))]", + "[format('http://{0}.azurewebsites.net', format(variables('uniqueNameFormat'), 'frontend'))]" + ] + } + }, + "activeRevisionsMode": "Single" + }, + "template": { + "scale": { + "minReplicas": "[parameters('resourceSize').containerAppSize.minReplicas]", + "maxReplicas": "[parameters('resourceSize').containerAppSize.maxReplicas]", + "rules": [ + { + "name": "http-scaler", + "http": { + "metadata": { + "concurrentRequests": "100" + } + } + } + ] + }, + "containers": [ + { + "name": "backend", + "image": "[variables('backendDockerImageURL')]", + "resources": { + "cpu": "[json(parameters('resourceSize').containerAppSize.cpu)]", + "memory": "[parameters('resourceSize').containerAppSize.memory]" + }, + "env": [ + { + "name": "COSMOSDB_ENDPOINT", + "value": "[reference('cosmos').documentEndpoint]" + }, + { + "name": "COSMOSDB_DATABASE", + "value": "autogen" + }, + { + "name": "COSMOSDB_CONTAINER", + "value": "memory" + }, + { + "name": "AZURE_OPENAI_ENDPOINT", + "value": "[reference('openai').endpoint]" + }, + { + "name": "AZURE_OPENAI_DEPLOYMENT_NAME", + "value": "gpt-4o" + }, + { + "name": "AZURE_OPENAI_API_VERSION", + "value": "[variables('aoaiApiVersion')]" + }, + { + "name": "FRONTEND_SITE_NAME", + "value": "[format('https://{0}.azurewebsites.net', format(variables('uniqueNameFormat'), 'frontend'))]" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference('appInsights').ConnectionString]" + } + ] + } + ] + } + }, + "dependsOn": [ + "appInsights", + "containerAppEnv", + "cosmos", + "cosmos::autogenDb", + "cosmos::autogenDb::memoryContainer", + "openai", + "openai::gpt4o", + "pullIdentity" + ], + "metadata": { + "description": "" + } + }, + "frontendAppServicePlan": { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2021-02-01", + "name": "[format(variables('uniqueNameFormat'), 'frontend-plan')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "P1v2", + "capacity": 1, + "tier": "PremiumV2" + }, + "properties": { + "reserved": true + }, + "kind": "linux" + }, + "frontendAppService": { + "type": "Microsoft.Web/sites", + "apiVersion": "2021-02-01", + "name": "[format(variables('uniqueNameFormat'), 'frontend')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "app,linux,container", + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format(variables('uniqueNameFormat'), 'frontend-plan'))]", + "reserved": true, + "siteConfig": { + "linuxFxVersion": "[format('DOCKER|{0}', variables('frontendDockerImageURL'))]", + "appSettings": [ + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "[variables('dockerRegistryUrl')]" + }, + { + "name": "WEBSITES_PORT", + "value": "3000" + }, + { + "name": "WEBSITES_CONTAINER_START_TIME_LIMIT", + "value": "1800" + }, + { + "name": "BACKEND_API_URL", + "value": "[format('https://{0}', reference('containerApp').configuration.ingress.fqdn)]" + } + ] + } + }, + "identity": { + "type": "SystemAssigned,UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format(variables('uniqueNameFormat'), 'containerapp-pull')))]": {} + } + }, + "dependsOn": [ + "containerApp", + "frontendAppServicePlan", + "pullIdentity" + ] + } + }, + "outputs": { + "cosmosAssignCli": { + "type": "string", + "value": "[format('az cosmosdb sql role assignment create --resource-group \"{0}\" --account-name \"{1}\" --role-definition-id \"{2}\" --scope \"{3}\" --principal-id \"fill-in\"', resourceGroup().name, format(variables('uniqueNameFormat'), 'cosmos'), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002'), resourceId('Microsoft.DocumentDB/databaseAccounts', format(variables('uniqueNameFormat'), 'cosmos')))]" + } + } +} \ No newline at end of file diff --git a/infra/macae-dev.bicep b/infra/macae-dev.bicep new file mode 100644 index 000000000..5157fa92f --- /dev/null +++ b/infra/macae-dev.bicep @@ -0,0 +1,131 @@ +@description('Location for all resources.') +param location string = resourceGroup().location + +@description('location for Cosmos DB resources.') +// prompt for this as there is often quota restrictions +param cosmosLocation string + +@description('Location for OpenAI resources.') +// prompt for this as there is often quota restrictions +param azureOpenAILocation string + +@description('A prefix to add to the start of all resource names. Note: A "unique" suffix will also be added') +param prefix string = 'macae' + +@description('Tags to apply to all deployed resources') +param tags object = {} + +@description('Principal ID to assign to the Cosmos DB contributor & Azure OpenAI user role, leave empty to skip role assignment. This is your ObjectID wihtin Entra ID.') +param developerPrincipalId string + +var uniqueNameFormat = '${prefix}-{0}-${uniqueString(resourceGroup().id, prefix)}' +var aoaiApiVersion = '2024-08-01-preview' + +resource openai 'Microsoft.CognitiveServices/accounts@2023-10-01-preview' = { + name: format(uniqueNameFormat, 'openai') + location: azureOpenAILocation + tags: tags + kind: 'OpenAI' + sku: { + name: 'S0' + } + properties: { + customSubDomainName: format(uniqueNameFormat, 'openai') + } + resource gpt4o 'deployments' = { + name: 'gpt-4o' + sku: { + name: 'GlobalStandard' + capacity: 15 + } + properties: { + model: { + format: 'OpenAI' + name: 'gpt-4o' + version: '2024-08-06' + } + versionUpgradeOption: 'NoAutoUpgrade' + } + } +} + +resource aoaiUserRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview' existing = { + name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' //'Cognitive Services OpenAI User' +} + +resource devAoaiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if(!empty(trim(developerPrincipalId))) { + name: guid(developerPrincipalId, openai.id, aoaiUserRoleDefinition.id) + scope: openai + properties: { + principalId: developerPrincipalId + roleDefinitionId: aoaiUserRoleDefinition.id + principalType: 'User' + } +} + +resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = { + name: format(uniqueNameFormat, 'cosmos') + location: cosmosLocation + tags: tags + kind: 'GlobalDocumentDB' + properties: { + databaseAccountOfferType: 'Standard' + enableFreeTier: false + locations: [ + { + failoverPriority: 0 + locationName: cosmosLocation + } + ] + capabilities: [ { name: 'EnableServerless' } ] + } + + resource contributorRoleDefinition 'sqlRoleDefinitions' existing = { + name: '00000000-0000-0000-0000-000000000002' + } + + resource devRoleAssignment 'sqlRoleAssignments' = if(!empty(trim(developerPrincipalId))) { + name: guid(developerPrincipalId, contributorRoleDefinition.id) + properties: { + principalId: developerPrincipalId + roleDefinitionId: contributorRoleDefinition.id + scope: cosmos.id + } + } + + resource autogenDb 'sqlDatabases' = { + name: 'autogen' + properties: { + resource: { + id: 'autogen' + createMode: 'Default' + } + } + + resource memoryContainer 'containers' = { + name: 'memory' + properties: { + resource: { + id: 'memory' + partitionKey: { + kind: 'Hash' + version: 2 + paths: [ + '/session_id' + ] + } + } + } + } + } +} + + + +output COSMOSDB_ENDPOINT string = cosmos.properties.documentEndpoint +output COSMOSDB_DATABASE string = cosmos::autogenDb.name +output COSMOSDB_CONTAINER string = cosmos::autogenDb::memoryContainer.name +output AZURE_OPENAI_ENDPOINT string = openai.properties.endpoint +output AZURE_OPENAI_DEPLOYMENT_NAME string = openai::gpt4o.name +output AZURE_OPENAI_API_VERSION string = aoaiApiVersion + diff --git a/infra/macae-large.bicepparam b/infra/macae-large.bicepparam new file mode 100644 index 000000000..3e88f4452 --- /dev/null +++ b/infra/macae-large.bicepparam @@ -0,0 +1,11 @@ +using './macae.bicep' + +param resourceSize = { + gpt4oCapacity: 50 + containerAppSize: { + cpu: '2.0' + memory: '4.0Gi' + minReplicas: 1 + maxReplicas: 1 + } +} diff --git a/infra/macae-mini.bicepparam b/infra/macae-mini.bicepparam new file mode 100644 index 000000000..ee3d65127 --- /dev/null +++ b/infra/macae-mini.bicepparam @@ -0,0 +1,11 @@ +using './macae.bicep' + +param resourceSize = { + gpt4oCapacity: 15 + containerAppSize: { + cpu: '1.0' + memory: '2.0Gi' + minReplicas: 0 + maxReplicas: 1 + } +} diff --git a/infra/macae.bicep b/infra/macae.bicep new file mode 100644 index 000000000..bfa56c9a1 --- /dev/null +++ b/infra/macae.bicep @@ -0,0 +1,362 @@ +@description('Location for all resources.') +param location string = resourceGroup().location + +@description('location for Cosmos DB resources.') +// prompt for this as there is often quota restrictions +param cosmosLocation string + +@description('Location for OpenAI resources.') +// prompt for this as there is often quota restrictions +param azureOpenAILocation string + +@description('A prefix to add to the start of all resource names. Note: A "unique" suffix will also be added') +param prefix string = 'macae' + +@description('Tags to apply to all deployed resources') +param tags object = {} + +@description('The size of the resources to deploy, defaults to a mini size') +param resourceSize { + gpt4oCapacity: int + containerAppSize: { + cpu: string + memory: string + minReplicas: int + maxReplicas: int + } +} = { + gpt4oCapacity: 50 + containerAppSize: { + cpu: '2.0' + memory: '4.0Gi' + minReplicas: 1 + maxReplicas: 1 + } +} + + +// var appVersion = 'latest' +// var resgistryName = 'biabcontainerreg' +// var dockerRegistryUrl = 'https://${resgistryName}.azurecr.io' +var placeholderImage = 'hello-world:latest' + +var uniqueNameFormat = '${prefix}-{0}-${uniqueString(resourceGroup().id, prefix)}' +var uniqueShortNameFormat = '${toLower(prefix)}{0}${uniqueString(resourceGroup().id, prefix)}' +//var aoaiApiVersion = '2024-08-01-preview' + + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: format(uniqueNameFormat, 'logs') + location: location + tags: tags + properties: { + retentionInDays: 30 + sku: { + name: 'PerGB2018' + } + } +} + +resource appInsights 'Microsoft.Insights/components@2020-02-02-preview' = { + name: format(uniqueNameFormat, 'appins') + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + } +} + +resource openai 'Microsoft.CognitiveServices/accounts@2023-10-01-preview' = { + name: format(uniqueNameFormat, 'openai') + location: azureOpenAILocation + tags: tags + kind: 'OpenAI' + sku: { + name: 'S0' + } + properties: { + customSubDomainName: format(uniqueNameFormat, 'openai') + } + resource gpt4o 'deployments' = { + name: 'gpt-4o' + sku: { + name: 'GlobalStandard' + capacity: resourceSize.gpt4oCapacity + } + properties: { + model: { + format: 'OpenAI' + name: 'gpt-4o' + version: '2024-08-06' + } + versionUpgradeOption: 'NoAutoUpgrade' + } + } +} + +resource aoaiUserRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview' existing = { + name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' //'Cognitive Services OpenAI User' +} + +resource acaAoaiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(containerApp.id, openai.id, aoaiUserRoleDefinition.id) + scope: openai + properties: { + principalId: containerApp.identity.principalId + roleDefinitionId: aoaiUserRoleDefinition.id + principalType: 'ServicePrincipal' + } +} + +resource acr 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = { + name: format(uniqueShortNameFormat, 'acr') + location: location + sku: { + name: 'Standard' + } + properties: { + adminUserEnabled: true // Add this line + } +} + +resource pullIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = { + name: format(uniqueNameFormat, 'containerapp-pull') + location: location +} + +resource acrPullDefinition 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview' existing = { + name: '7f951dda-4ed3-4680-a7ca-43fe172d538d' //'AcrPull' +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(acr.id, pullIdentity.id, acrPullDefinition.id) + properties: { + principalId: pullIdentity.properties.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: acrPullDefinition.id + } +} + +resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = { + name: format(uniqueNameFormat, 'cosmos') + location: cosmosLocation + tags: tags + kind: 'GlobalDocumentDB' + properties: { + databaseAccountOfferType: 'Standard' + enableFreeTier: false + locations: [ + { + failoverPriority: 0 + locationName: cosmosLocation + } + ] + capabilities: [ { name: 'EnableServerless' } ] + } + + resource contributorRoleDefinition 'sqlRoleDefinitions' existing = { + name: '00000000-0000-0000-0000-000000000002' + } + + resource autogenDb 'sqlDatabases' = { + name: 'autogen' + properties: { + resource: { + id: 'autogen' + createMode: 'Default' + } + } + + resource memoryContainer 'containers' = { + name: 'memory' + properties: { + resource: { + id: 'memory' + partitionKey: { + kind: 'Hash' + version: 2 + paths: [ + '/session_id' + ] + } + } + } + } + } +} + +resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: format(uniqueNameFormat, 'containerapp') + location: location + tags: tags + properties: { + daprAIConnectionString: appInsights.properties.ConnectionString + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalytics.properties.customerId + sharedKey: logAnalytics.listKeys().primarySharedKey + } + } + } + resource aspireDashboard 'dotNetComponents@2024-02-02-preview' = { + name: 'aspire-dashboard' + properties: { + componentType: 'AspireDashboard' + } + } +} + +resource acaCosomsRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-05-15' = { + name: guid(containerApp.id, cosmos::contributorRoleDefinition.id) + parent: cosmos + properties: { + principalId: containerApp.identity.principalId + roleDefinitionId: cosmos::contributorRoleDefinition.id + scope: cosmos.id + } +} + +@description('') +resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { + name: '${prefix}-backend' + location: location + tags: tags + identity: { + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${pullIdentity.id}': {} + } + } + properties: { + managedEnvironmentId: containerAppEnv.id + configuration: { + ingress: { + targetPort: 8000 + external: true + corsPolicy: { + allowedOrigins: [ + 'https://${format(uniqueNameFormat, 'frontend')}.azurewebsites.net' + 'http://${format(uniqueNameFormat, 'frontend')}.azurewebsites.net' + ] + } + } + activeRevisionsMode: 'Single' + } + template: { + scale: { + minReplicas: resourceSize.containerAppSize.minReplicas + maxReplicas: resourceSize.containerAppSize.maxReplicas + rules: [ + { + name: 'http-scaler' + http: { + metadata: { + concurrentRequests: '100' + } + } + } + ] + } + containers: [ + { + name: 'backend' + image: placeholderImage + resources: { + cpu: json(resourceSize.containerAppSize.cpu) + memory: resourceSize.containerAppSize.memory + } + } + // env: [ + // { + // name: 'COSMOSDB_ENDPOINT' + // value: cosmos.properties.documentEndpoint + // } + // { + // name: 'COSMOSDB_DATABASE' + // value: cosmos::autogenDb.name + // } + // { + // name: 'COSMOSDB_CONTAINER' + // value: cosmos::autogenDb::memoryContainer.name + // } + // { + // name: 'AZURE_OPENAI_ENDPOINT' + // value: openai.properties.endpoint + // } + // { + // name: 'AZURE_OPENAI_DEPLOYMENT_NAME' + // value: openai::gpt4o.name + // } + // { + // name: 'AZURE_OPENAI_API_VERSION' + // value: aoaiApiVersion + // } + // { + // name: 'FRONTEND_SITE_NAME' + // value: 'https://${format(uniqueNameFormat, 'frontend')}.azurewebsites.net' + // } + // ] + // } + ] + } + + } + + } +resource frontendAppServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = { + name: format(uniqueNameFormat, 'frontend-plan') + location: location + tags: tags + sku: { + name: 'P1v2' + capacity: 1 + tier: 'PremiumV2' + } + properties: { + reserved: true + } + kind: 'linux' // Add this line to support Linux containers +} + +resource frontendAppService 'Microsoft.Web/sites@2021-02-01' = { + name: format(uniqueNameFormat, 'frontend') + location: location + tags: tags + kind: 'app,linux,container' // Add this line + properties: { + serverFarmId: frontendAppServicePlan.id + reserved: true + siteConfig: { + linuxFxVersion:''//'DOCKER|${frontendDockerImageURL}' + appSettings: [ + { + name: 'DOCKER_REGISTRY_SERVER_URL' + value: acr.properties.loginServer + } + { + name: 'WEBSITES_PORT' + value: '3000' + } + { + name: 'WEBSITES_CONTAINER_START_TIME_LIMIT' // Add startup time limit + value: '1800' // 30 minutes, adjust as needed + } + { + name: 'BACKEND_API_URL' + value: 'https://${containerApp.properties.configuration.ingress.fqdn}' + } + ] + } + } + dependsOn: [containerApp] + identity: { + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${pullIdentity.id}': {} + } + } +} + +output cosmosAssignCli string = 'az cosmosdb sql role assignment create --resource-group "${resourceGroup().name}" --account-name "${cosmos.name}" --role-definition-id "${cosmos::contributorRoleDefinition.id}" --scope "${cosmos.id}" --principal-id "fill-in"' diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 000000000..fecb5c751 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,449 @@ +@description('Location for all resources.') +param location string = 'EastUS2' //Fixed for model availability, change back to resourceGroup().location + +@description('Location for OpenAI resources.') +param azureOpenAILocation string = 'japaneast' //Fixed for model availability + + + +@description('A prefix to add to the start of all resource names. Note: A "unique" suffix will also be added') +param prefix string = 'macaeo' + +@description('Tags to apply to all deployed resources') +param tags object = {} + +@description('The size of the resources to deploy, defaults to a mini size') +param resourceSize { + gpt4oCapacity: int + containerAppSize: { + cpu: string + memory: string + minReplicas: int + maxReplicas: int + } +} = { + gpt4oCapacity: 1 + containerAppSize: { + cpu: '2.0' + memory: '4.0Gi' + minReplicas: 1 + maxReplicas: 1 + } +} +param capacity int = 1 + + +var modelVersion = '2024-08-06' +var aiServicesName = '${prefix}-aiservices' +var deploymentType = 'GlobalStandard' +var gptModelVersion = 'gpt-4o' +var appVersion = 'latest' +var resgistryName = 'biabcontainerreg' +var dockerRegistryUrl = 'https://${resgistryName}.azurecr.io' + +@description('URL for frontend docker image') +var backendDockerImageURL = '${resgistryName}.azurecr.io/macaebackend:${appVersion}' +var frontendDockerImageURL = '${resgistryName}.azurecr.io/macaefrontend:${appVersion}' + +var uniqueNameFormat = '${prefix}-{0}-${uniqueString(resourceGroup().id, prefix)}' +var aoaiApiVersion = '2024-08-01-preview' + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: format(uniqueNameFormat, 'logs') + location: location + tags: tags + properties: { + retentionInDays: 30 + sku: { + name: 'PerGB2018' + } + } +} + +resource appInsights 'Microsoft.Insights/components@2020-02-02-preview' = { + name: format(uniqueNameFormat, 'appins') + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + } +} + + +var aiModelDeployments = [ + { + name: gptModelVersion + model: gptModelVersion + version: modelVersion + sku: { + name: deploymentType + capacity: capacity + } + raiPolicyName: 'Microsoft.Default' + } +] + +resource aiServices 'Microsoft.CognitiveServices/accounts@2024-04-01-preview' = { + name: aiServicesName + location: location + sku: { + name: 'S0' + } + kind: 'AIServices' + properties: { + customSubDomainName: aiServicesName + apiProperties: { + statisticsEnabled: false + } + } +} + +resource aiServicesDeployments 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for aiModeldeployment in aiModelDeployments: { + parent: aiServices //aiServices_m + name: aiModeldeployment.name + properties: { + model: { + format: 'OpenAI' + name: aiModeldeployment.model + version: aiModeldeployment.version + } + raiPolicyName: aiModeldeployment.raiPolicyName + } + sku:{ + name: aiModeldeployment.sku.name + capacity: aiModeldeployment.sku.capacity + } +}] + +module kvault 'deploy_keyvault.bicep' = { + name: 'deploy_keyvault' + params: { + solutionName: prefix + solutionLocation: location + managedIdentityObjectId:managedIdentityModule.outputs.managedIdentityOutput.objectId + } + scope: resourceGroup(resourceGroup().name) +} + +module aifoundry 'deploy_ai_foundry.bicep' = { + name: 'deploy_ai_foundry' + params: { + solutionName: prefix + solutionLocation: azureOpenAILocation + keyVaultName: kvault.outputs.keyvaultName + gptModelName: gptModelVersion + gptModelVersion: gptModelVersion + managedIdentityObjectId:managedIdentityModule.outputs.managedIdentityOutput.objectId + aiServicesEndpoint: aiServices.properties.endpoint + aiServicesKey: aiServices.listKeys().key1 + aiServicesId: aiServices.id + } + scope: resourceGroup(resourceGroup().name) +} +// resource openai 'Microsoft.CognitiveServices/accounts@2023-10-01-preview' = { +// name: format(uniqueNameFormat, 'openai') +// location: azureOpenAILocation +// tags: tags +// kind: 'OpenAI' +// sku: { +// name: 'S0' +// } +// properties: { +// customSubDomainName: format(uniqueNameFormat, 'openai') +// } +// resource gpt4o 'deployments' = { +// name: 'gpt-4o' +// sku: { +// name: 'GlobalStandard' +// capacity: resourceSize.gpt4oCapacity +// } +// properties: { +// model: { +// format: 'OpenAI' +// name: gptModelVersion +// version: '2024-08-06' +// } +// versionUpgradeOption: 'NoAutoUpgrade' +// } +// } +// } + +resource aoaiUserRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview' existing = { + name: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' //'Cognitive Services OpenAI User' +} + +resource acaAoaiRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(containerApp.id, aiServices.id, aoaiUserRoleDefinition.id) + scope: aiServices + properties: { + principalId: containerApp.identity.principalId + roleDefinitionId: aoaiUserRoleDefinition.id + principalType: 'ServicePrincipal' + } +} + +resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-05-15' = { + name: format(uniqueNameFormat, 'cosmos') + location: location + tags: tags + kind: 'GlobalDocumentDB' + properties: { + databaseAccountOfferType: 'Standard' + enableFreeTier: false + locations: [ + { + failoverPriority: 0 + locationName: location + } + ] + capabilities: [ { name: 'EnableServerless' } ] + } + + resource contributorRoleDefinition 'sqlRoleDefinitions' existing = { + name: '00000000-0000-0000-0000-000000000002' + } + + resource autogenDb 'sqlDatabases' = { + name: 'autogen' + properties: { + resource: { + id: 'autogen' + createMode: 'Default' + } + } + + resource memoryContainer 'containers' = { + name: 'memory' + properties: { + resource: { + id: 'memory' + partitionKey: { + kind: 'Hash' + version: 2 + paths: [ + '/session_id' + ] + } + } + } + } + } +} +// Define existing ACR resource + + +resource pullIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = { + name: format(uniqueNameFormat, 'containerapp-pull') + location: location +} + + + +resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { + name: format(uniqueNameFormat, 'containerapp') + location: location + tags: tags + properties: { + daprAIConnectionString: appInsights.properties.ConnectionString + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalytics.properties.customerId + sharedKey: logAnalytics.listKeys().primarySharedKey + } + } + } + resource aspireDashboard 'dotNetComponents@2024-02-02-preview' = { + name: 'aspire-dashboard' + properties: { + componentType: 'AspireDashboard' + } + } +} + +resource acaCosomsRoleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-05-15' = { + name: guid(containerApp.id, cosmos::contributorRoleDefinition.id) + parent: cosmos + properties: { + principalId: containerApp.identity.principalId + roleDefinitionId: cosmos::contributorRoleDefinition.id + scope: cosmos.id + } +} + +@description('') +resource containerApp 'Microsoft.App/containerApps@2024-03-01' = { + name: '${prefix}-backend' + location: location + tags: tags + identity: { + type: 'SystemAssigned, UserAssigned' + userAssignedIdentities: { + '${pullIdentity.id}': {} + } + } + properties: { + managedEnvironmentId: containerAppEnv.id + configuration: { + ingress: { + targetPort: 8000 + external: true + corsPolicy: { + allowedOrigins: [ + 'https://${format(uniqueNameFormat, 'frontend')}.azurewebsites.net' + 'http://${format(uniqueNameFormat, 'frontend')}.azurewebsites.net' + ] + } + } + activeRevisionsMode: 'Single' + } + template: { + scale: { + minReplicas: resourceSize.containerAppSize.minReplicas + maxReplicas: resourceSize.containerAppSize.maxReplicas + rules: [ + { + name: 'http-scaler' + http: { + metadata: { + concurrentRequests: '100' + } + } + } + ] + } + containers: [ + { + name: 'backend' + image: backendDockerImageURL + resources: { + cpu: json(resourceSize.containerAppSize.cpu) + memory: resourceSize.containerAppSize.memory + } + env: [ + { + name: 'COSMOSDB_ENDPOINT' + value: cosmos.properties.documentEndpoint + } + { + name: 'COSMOSDB_DATABASE' + value: cosmos::autogenDb.name + } + { + name: 'COSMOSDB_CONTAINER' + value: cosmos::autogenDb::memoryContainer.name + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: aiServices.properties.endpoint + } + { + name: 'AZURE_OPENAI_DEPLOYMENT_NAME' + value: gptModelVersion + } + { + name: 'AZURE_OPENAI_API_VERSION' + value: aoaiApiVersion + } + { + name: 'FRONTEND_SITE_NAME' + value: 'https://${format(uniqueNameFormat, 'frontend')}.azurewebsites.net' + } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: appInsights.properties.ConnectionString + } + ] + } + ] + } + + } + + } +resource frontendAppServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = { + name: format(uniqueNameFormat, 'frontend-plan') + location: location + tags: tags + sku: { + name: 'P1v2' + capacity: 1 + tier: 'PremiumV2' + } + properties: { + reserved: true + } + kind: 'linux' // Add this line to support Linux containers +} + +resource frontendAppService 'Microsoft.Web/sites@2021-02-01' = { + name: format(uniqueNameFormat, 'frontend') + location: location + tags: tags + kind: 'app,linux,container' + properties: { + serverFarmId: frontendAppServicePlan.id + reserved: true + siteConfig: { + linuxFxVersion: 'DOCKER|${frontendDockerImageURL}' + appSettings: [ + { + name: 'DOCKER_REGISTRY_SERVER_URL' + value: dockerRegistryUrl + } + { + name: 'WEBSITES_PORT' + value: '3000' + } + { + name: 'WEBSITES_CONTAINER_START_TIME_LIMIT' + value: '1800' + } + { + name: 'BACKEND_API_URL' + value: 'https://${containerApp.properties.configuration.ingress.fqdn}' + } + ] + } + } + dependsOn: [containerApp] + identity: { + type: 'SystemAssigned,UserAssigned' + userAssignedIdentities: { + '${pullIdentity.id}': {} + } + } +} + +var cosmosAssignCli = 'az cosmosdb sql role assignment create --resource-group "${resourceGroup().name}" --account-name "${cosmos.name}" --role-definition-id "${cosmos::contributorRoleDefinition.id}" --scope "${cosmos.id}" --principal-id "${containerApp.identity.principalId}"' + +module managedIdentityModule 'deploy_managed_identity.bicep' = { + name: 'deploy_managed_identity' + params: { + solutionName: prefix + //solutionLocation: location + managedIdentityId: pullIdentity.id + managedIdentityPropPrin: pullIdentity.properties.principalId + managedIdentityLocation: pullIdentity.location + } + scope: resourceGroup(resourceGroup().name) +} + +module deploymentScriptCLI 'br/public:avm/res/resources/deployment-script:0.5.1' = { + name: 'deploymentScriptCLI' + params: { + // Required parameters + kind: 'AzureCLI' + name: 'rdsmin001' + // Non-required parameters + azCliVersion: '2.69.0' + location: location + managedIdentities: { + userAssignedResourceIds: [ + managedIdentityModule.outputs.managedIdentityId + ] + } + scriptContent: cosmosAssignCli + } +} diff --git a/infra/main.json b/infra/main.json new file mode 100644 index 000000000..9f6864aae --- /dev/null +++ b/infra/main.json @@ -0,0 +1,1663 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "2906892014954666053" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "EastUS2", + "metadata": { + "description": "Location for all resources." + } + }, + "azureOpenAILocation": { + "type": "string", + "defaultValue": "japaneast", + "metadata": { + "description": "Location for OpenAI resources." + } + }, + "prefix": { + "type": "string", + "defaultValue": "macaeo", + "metadata": { + "description": "A prefix to add to the start of all resource names. Note: A \"unique\" suffix will also be added" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to apply to all deployed resources" + } + }, + "resourceSize": { + "type": "object", + "properties": { + "gpt4oCapacity": { + "type": "int" + }, + "containerAppSize": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "minReplicas": { + "type": "int" + }, + "maxReplicas": { + "type": "int" + } + } + } + }, + "defaultValue": { + "gpt4oCapacity": 1, + "containerAppSize": { + "cpu": "2.0", + "memory": "4.0Gi", + "minReplicas": 1, + "maxReplicas": 1 + } + }, + "metadata": { + "description": "The size of the resources to deploy, defaults to a mini size" + } + }, + "capacity": { + "type": "int", + "defaultValue": 1 + } + }, + "variables": { + "modelVersion": "2024-08-06", + "aiServicesName": "[format('{0}-aiservices', parameters('prefix'))]", + "deploymentType": "GlobalStandard", + "gptModelVersion": "gpt-4o", + "appVersion": "latest", + "resgistryName": "biabcontainerreg", + "dockerRegistryUrl": "[format('https://{0}.azurecr.io', variables('resgistryName'))]", + "backendDockerImageURL": "[format('{0}.azurecr.io/macaebackend:{1}', variables('resgistryName'), variables('appVersion'))]", + "frontendDockerImageURL": "[format('{0}.azurecr.io/macaefrontend:{1}', variables('resgistryName'), variables('appVersion'))]", + "uniqueNameFormat": "[format('{0}-{{0}}-{1}', parameters('prefix'), uniqueString(resourceGroup().id, parameters('prefix')))]", + "aoaiApiVersion": "2024-08-01-preview", + "aiModelDeployments": [ + { + "name": "[variables('gptModelVersion')]", + "model": "[variables('gptModelVersion')]", + "version": "[variables('modelVersion')]", + "sku": { + "name": "[variables('deploymentType')]", + "capacity": "[parameters('capacity')]" + }, + "raiPolicyName": "Microsoft.Default" + } + ] + }, + "resources": { + "cosmos::autogenDb::memoryContainer": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}/{2}', format(variables('uniqueNameFormat'), 'cosmos'), 'autogen', 'memory')]", + "properties": { + "resource": { + "id": "memory", + "partitionKey": { + "kind": "Hash", + "version": 2, + "paths": [ + "/session_id" + ] + } + } + }, + "dependsOn": [ + "cosmos::autogenDb" + ] + }, + "cosmos::contributorRoleDefinition": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002')]", + "dependsOn": [ + "cosmos" + ] + }, + "cosmos::autogenDb": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'cosmos'), 'autogen')]", + "properties": { + "resource": { + "id": "autogen", + "createMode": "Default" + } + }, + "dependsOn": [ + "cosmos" + ] + }, + "containerAppEnv::aspireDashboard": { + "type": "Microsoft.App/managedEnvironments/dotNetComponents", + "apiVersion": "2024-02-02-preview", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'containerapp'), 'aspire-dashboard')]", + "properties": { + "componentType": "AspireDashboard" + }, + "dependsOn": [ + "containerAppEnv" + ] + }, + "logAnalytics": { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2023-09-01", + "name": "[format(variables('uniqueNameFormat'), 'logs')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "retentionInDays": 30, + "sku": { + "name": "PerGB2018" + } + } + }, + "appInsights": { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02-preview", + "name": "[format(variables('uniqueNameFormat'), 'appins')]", + "location": "[parameters('location')]", + "kind": "web", + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', format(variables('uniqueNameFormat'), 'logs'))]" + }, + "dependsOn": [ + "logAnalytics" + ] + }, + "aiServices": { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2024-04-01-preview", + "name": "[variables('aiServicesName')]", + "location": "[parameters('location')]", + "sku": { + "name": "S0" + }, + "kind": "AIServices", + "properties": { + "customSubDomainName": "[variables('aiServicesName')]", + "apiProperties": { + "statisticsEnabled": false + } + } + }, + "aiServicesDeployments": { + "copy": { + "name": "aiServicesDeployments", + "count": "[length(variables('aiModelDeployments'))]" + }, + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', variables('aiServicesName'), variables('aiModelDeployments')[copyIndex()].name)]", + "properties": { + "model": { + "format": "OpenAI", + "name": "[variables('aiModelDeployments')[copyIndex()].model]", + "version": "[variables('aiModelDeployments')[copyIndex()].version]" + }, + "raiPolicyName": "[variables('aiModelDeployments')[copyIndex()].raiPolicyName]" + }, + "sku": { + "name": "[variables('aiModelDeployments')[copyIndex()].sku.name]", + "capacity": "[variables('aiModelDeployments')[copyIndex()].sku.capacity]" + }, + "dependsOn": [ + "aiServices" + ] + }, + "aoaiUserRoleDefinition": { + "existing": true, + "type": "Microsoft.Authorization/roleDefinitions", + "apiVersion": "2022-05-01-preview", + "name": "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" + }, + "acaAoaiRoleAssignment": { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', variables('aiServicesName'))]", + "name": "[guid(resourceId('Microsoft.App/containerApps', format('{0}-backend', parameters('prefix'))), resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'))]", + "properties": { + "principalId": "[reference('containerApp', '2024-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "aiServices", + "containerApp" + ] + }, + "cosmos": { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-05-15", + "name": "[format(variables('uniqueNameFormat'), 'cosmos')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "GlobalDocumentDB", + "properties": { + "databaseAccountOfferType": "Standard", + "enableFreeTier": false, + "locations": [ + { + "failoverPriority": 0, + "locationName": "[parameters('location')]" + } + ], + "capabilities": [ + { + "name": "EnableServerless" + } + ] + } + }, + "pullIdentity": { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-07-31-preview", + "name": "[format(variables('uniqueNameFormat'), 'containerapp-pull')]", + "location": "[parameters('location')]" + }, + "containerAppEnv": { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2024-03-01", + "name": "[format(variables('uniqueNameFormat'), 'containerapp')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "daprAIConnectionString": "[reference('appInsights').ConnectionString]", + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference('logAnalytics').customerId]", + "sharedKey": "[listKeys(resourceId('Microsoft.OperationalInsights/workspaces', format(variables('uniqueNameFormat'), 'logs')), '2023-09-01').primarySharedKey]" + } + } + }, + "dependsOn": [ + "appInsights", + "logAnalytics" + ] + }, + "acaCosomsRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2024-05-15", + "name": "[format('{0}/{1}', format(variables('uniqueNameFormat'), 'cosmos'), guid(resourceId('Microsoft.App/containerApps', format('{0}-backend', parameters('prefix'))), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002')))]", + "properties": { + "principalId": "[reference('containerApp', '2024-03-01', 'full').identity.principalId]", + "roleDefinitionId": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002')]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', format(variables('uniqueNameFormat'), 'cosmos'))]" + }, + "dependsOn": [ + "containerApp", + "cosmos" + ] + }, + "containerApp": { + "type": "Microsoft.App/containerApps", + "apiVersion": "2024-03-01", + "name": "[format('{0}-backend', parameters('prefix'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": { + "type": "SystemAssigned, UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format(variables('uniqueNameFormat'), 'containerapp-pull')))]": {} + } + }, + "properties": { + "managedEnvironmentId": "[resourceId('Microsoft.App/managedEnvironments', format(variables('uniqueNameFormat'), 'containerapp'))]", + "configuration": { + "ingress": { + "targetPort": 8000, + "external": true, + "corsPolicy": { + "allowedOrigins": [ + "[format('https://{0}.azurewebsites.net', format(variables('uniqueNameFormat'), 'frontend'))]", + "[format('http://{0}.azurewebsites.net', format(variables('uniqueNameFormat'), 'frontend'))]" + ] + } + }, + "activeRevisionsMode": "Single" + }, + "template": { + "scale": { + "minReplicas": "[parameters('resourceSize').containerAppSize.minReplicas]", + "maxReplicas": "[parameters('resourceSize').containerAppSize.maxReplicas]", + "rules": [ + { + "name": "http-scaler", + "http": { + "metadata": { + "concurrentRequests": "100" + } + } + } + ] + }, + "containers": [ + { + "name": "backend", + "image": "[variables('backendDockerImageURL')]", + "resources": { + "cpu": "[json(parameters('resourceSize').containerAppSize.cpu)]", + "memory": "[parameters('resourceSize').containerAppSize.memory]" + }, + "env": [ + { + "name": "COSMOSDB_ENDPOINT", + "value": "[reference('cosmos').documentEndpoint]" + }, + { + "name": "COSMOSDB_DATABASE", + "value": "autogen" + }, + { + "name": "COSMOSDB_CONTAINER", + "value": "memory" + }, + { + "name": "AZURE_OPENAI_ENDPOINT", + "value": "[reference('aiServices').endpoint]" + }, + { + "name": "AZURE_OPENAI_DEPLOYMENT_NAME", + "value": "[variables('gptModelVersion')]" + }, + { + "name": "AZURE_OPENAI_API_VERSION", + "value": "[variables('aoaiApiVersion')]" + }, + { + "name": "FRONTEND_SITE_NAME", + "value": "[format('https://{0}.azurewebsites.net', format(variables('uniqueNameFormat'), 'frontend'))]" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference('appInsights').ConnectionString]" + } + ] + } + ] + } + }, + "dependsOn": [ + "aiServices", + "appInsights", + "containerAppEnv", + "cosmos", + "cosmos::autogenDb", + "cosmos::autogenDb::memoryContainer", + "pullIdentity" + ], + "metadata": { + "description": "" + } + }, + "frontendAppServicePlan": { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2021-02-01", + "name": "[format(variables('uniqueNameFormat'), 'frontend-plan')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "P1v2", + "capacity": 1, + "tier": "PremiumV2" + }, + "properties": { + "reserved": true + }, + "kind": "linux" + }, + "frontendAppService": { + "type": "Microsoft.Web/sites", + "apiVersion": "2021-02-01", + "name": "[format(variables('uniqueNameFormat'), 'frontend')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "app,linux,container", + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format(variables('uniqueNameFormat'), 'frontend-plan'))]", + "reserved": true, + "siteConfig": { + "linuxFxVersion": "[format('DOCKER|{0}', variables('frontendDockerImageURL'))]", + "appSettings": [ + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "[variables('dockerRegistryUrl')]" + }, + { + "name": "WEBSITES_PORT", + "value": "3000" + }, + { + "name": "WEBSITES_CONTAINER_START_TIME_LIMIT", + "value": "1800" + }, + { + "name": "BACKEND_API_URL", + "value": "[format('https://{0}', reference('containerApp').configuration.ingress.fqdn)]" + } + ] + } + }, + "identity": { + "type": "SystemAssigned,UserAssigned", + "userAssignedIdentities": { + "[format('{0}', resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format(variables('uniqueNameFormat'), 'containerapp-pull')))]": {} + } + }, + "dependsOn": [ + "containerApp", + "frontendAppServicePlan", + "pullIdentity" + ] + }, + "kvault": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "deploy_keyvault", + "resourceGroup": "[resourceGroup().name]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "solutionName": { + "value": "[parameters('prefix')]" + }, + "solutionLocation": { + "value": "[parameters('location')]" + }, + "managedIdentityObjectId": { + "value": "[reference('managedIdentityModule').outputs.managedIdentityOutput.value.objectId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "10664495342911727649" + } + }, + "parameters": { + "solutionName": { + "type": "string", + "minLength": 3, + "maxLength": 15, + "metadata": { + "description": "Solution Name" + } + }, + "solutionLocation": { + "type": "string" + }, + "managedIdentityObjectId": { + "type": "string" + } + }, + "variables": { + "keyvaultName": "[format('{0}-kv', parameters('solutionName'))]" + }, + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2022-07-01", + "name": "[variables('keyvaultName')]", + "location": "[parameters('solutionLocation')]", + "properties": { + "createMode": "default", + "accessPolicies": [ + { + "objectId": "[parameters('managedIdentityObjectId')]", + "permissions": { + "certificates": [ + "all" + ], + "keys": [ + "all" + ], + "secrets": [ + "all" + ], + "storage": [ + "all" + ] + }, + "tenantId": "[subscription().tenantId]" + } + ], + "enabledForDeployment": true, + "enabledForDiskEncryption": true, + "enabledForTemplateDeployment": true, + "enableSoftDelete": false, + "enableRbacAuthorization": true, + "enablePurgeProtection": true, + "publicNetworkAccess": "enabled", + "sku": { + "family": "A", + "name": "standard" + }, + "softDeleteRetentionInDays": 7, + "tenantId": "[subscription().tenantId]" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, parameters('managedIdentityObjectId'), resourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483'))]", + "properties": { + "principalId": "[parameters('managedIdentityObjectId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483')]", + "principalType": "ServicePrincipal" + } + } + ], + "outputs": { + "keyvaultName": { + "type": "string", + "value": "[variables('keyvaultName')]" + }, + "keyvaultId": { + "type": "string", + "value": "[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]" + } + } + } + }, + "dependsOn": [ + "managedIdentityModule" + ] + }, + "aifoundry": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "deploy_ai_foundry", + "resourceGroup": "[resourceGroup().name]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "solutionName": { + "value": "[parameters('prefix')]" + }, + "solutionLocation": { + "value": "[parameters('azureOpenAILocation')]" + }, + "keyVaultName": { + "value": "[reference('kvault').outputs.keyvaultName.value]" + }, + "gptModelName": { + "value": "[variables('gptModelVersion')]" + }, + "gptModelVersion": { + "value": "[variables('gptModelVersion')]" + }, + "managedIdentityObjectId": { + "value": "[reference('managedIdentityModule').outputs.managedIdentityOutput.value.objectId]" + }, + "aiServicesEndpoint": { + "value": "[reference('aiServices').endpoint]" + }, + "aiServicesKey": { + "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2024-04-01-preview').key1]" + }, + "aiServicesId": { + "value": "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12550713338937452696" + } + }, + "parameters": { + "solutionName": { + "type": "string" + }, + "solutionLocation": { + "type": "string" + }, + "keyVaultName": { + "type": "string" + }, + "gptModelName": { + "type": "string" + }, + "gptModelVersion": { + "type": "string" + }, + "managedIdentityObjectId": { + "type": "string" + }, + "aiServicesEndpoint": { + "type": "string" + }, + "aiServicesKey": { + "type": "string" + }, + "aiServicesId": { + "type": "string" + } + }, + "variables": { + "storageName": "[format('{0}hubstorage', parameters('solutionName'))]", + "storageSkuName": "Standard_LRS", + "aiServicesName": "[format('{0}-aiservices', parameters('solutionName'))]", + "workspaceName": "[format('{0}-workspace', parameters('solutionName'))]", + "keyvaultName": "[format('{0}-kv', parameters('solutionName'))]", + "location": "[parameters('solutionLocation')]", + "aiHubName": "[format('{0}-aihub', parameters('solutionName'))]", + "aiHubFriendlyName": "[variables('aiHubName')]", + "aiHubDescription": "AI Hub for KM template", + "aiProjectName": "[format('{0}-aiproject', parameters('solutionName'))]", + "aiProjectFriendlyName": "[variables('aiProjectName')]", + "aiSearchName": "[format('{0}-search', parameters('solutionName'))]", + "storageNameCleaned": "[replace(variables('storageName'), '-', '')]" + }, + "resources": [ + { + "type": "Microsoft.MachineLearningServices/workspaces/connections", + "apiVersion": "2024-07-01-preview", + "name": "[format('{0}/{1}', variables('aiHubName'), format('{0}-connection-AzureOpenAI', variables('aiHubName')))]", + "properties": { + "category": "AIServices", + "target": "[parameters('aiServicesEndpoint')]", + "authType": "ApiKey", + "isSharedToAll": true, + "credentials": { + "key": "[parameters('aiServicesKey')]" + }, + "metadata": { + "ApiType": "Azure", + "ResourceId": "[parameters('aiServicesId')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('aiHubName'))]" + ] + }, + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2023-09-01", + "name": "[variables('workspaceName')]", + "location": "[variables('location')]", + "tags": {}, + "properties": { + "retentionInDays": 30, + "sku": { + "name": "PerGB2018" + } + } + }, + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-09-01", + "name": "[variables('storageNameCleaned')]", + "location": "[variables('location')]", + "sku": { + "name": "[variables('storageSkuName')]" + }, + "kind": "StorageV2", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "accessTier": "Hot", + "allowBlobPublicAccess": false, + "allowCrossTenantReplication": false, + "allowSharedKeyAccess": false, + "encryption": { + "keySource": "Microsoft.Storage", + "requireInfrastructureEncryption": false, + "services": { + "blob": { + "enabled": true, + "keyType": "Account" + }, + "file": { + "enabled": true, + "keyType": "Account" + }, + "queue": { + "enabled": true, + "keyType": "Service" + }, + "table": { + "enabled": true, + "keyType": "Service" + } + } + }, + "isHnsEnabled": false, + "isNfsV3Enabled": false, + "keyPolicy": { + "keyExpirationPeriodInDays": 7 + }, + "largeFileSharesState": "Disabled", + "minimumTlsVersion": "TLS1_2", + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Allow" + }, + "supportsHttpsTrafficOnly": true + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', variables('storageNameCleaned'))]", + "name": "[guid(resourceGroup().id, parameters('managedIdentityObjectId'), subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'))]", + "properties": { + "principalId": "[parameters('managedIdentityObjectId')]", + "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalType": "ServicePrincipal" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2023-08-01-preview", + "name": "[variables('aiHubName')]", + "location": "[variables('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "friendlyName": "[variables('aiHubFriendlyName')]", + "description": "[variables('aiHubDescription')]", + "keyVault": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]", + "storageAccount": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]" + }, + "kind": "hub", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]" + ] + }, + { + "type": "Microsoft.MachineLearningServices/workspaces", + "apiVersion": "2024-01-01-preview", + "name": "[variables('aiProjectName')]", + "location": "[variables('location')]", + "kind": "Project", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "friendlyName": "[variables('aiProjectFriendlyName')]", + "hubResourceId": "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('aiHubName'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('aiHubName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'TENANT-ID')]", + "properties": { + "value": "[subscription().tenantId]" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-INFERENCE-ENDPOINT')]", + "properties": { + "value": "" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-INFERENCE-KEY')]", + "properties": { + "value": "" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-KEY')]", + "properties": { + "value": "[parameters('aiServicesKey')]" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPEN-AI-DEPLOYMENT-MODEL')]", + "properties": { + "value": "[parameters('gptModelName')]" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-PREVIEW-API-VERSION')]", + "properties": { + "value": "[parameters('gptModelVersion')]" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-ENDPOINT')]", + "properties": { + "value": "[parameters('aiServicesEndpoint')]" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-AI-PROJECT-CONN-STRING')]", + "properties": { + "value": "[format('{0};{1};{2};{3}', split(reference(resourceId('Microsoft.MachineLearningServices/workspaces', variables('aiProjectName')), '2024-01-01-preview').discoveryUrl, '/')[2], subscription().subscriptionId, resourceGroup().name, variables('aiProjectName'))]" + }, + "dependsOn": [ + "[resourceId('Microsoft.MachineLearningServices/workspaces', variables('aiProjectName'))]" + ] + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-CU-VERSION')]", + "properties": { + "value": "?api-version=2024-12-01-preview" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-SEARCH-INDEX')]", + "properties": { + "value": "transcripts_index" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'COG-SERVICES-ENDPOINT')]", + "properties": { + "value": "[parameters('aiServicesEndpoint')]" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'COG-SERVICES-KEY')]", + "properties": { + "value": "[parameters('aiServicesKey')]" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'COG-SERVICES-NAME')]", + "properties": { + "value": "[variables('aiServicesName')]" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-SUBSCRIPTION-ID')]", + "properties": { + "value": "[subscription().subscriptionId]" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-RESOURCE-GROUP')]", + "properties": { + "value": "[resourceGroup().name]" + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2021-11-01-preview", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-LOCATION')]", + "properties": { + "value": "[parameters('solutionLocation')]" + } + } + ], + "outputs": { + "keyvaultName": { + "type": "string", + "value": "[variables('keyvaultName')]" + }, + "keyvaultId": { + "type": "string", + "value": "[resourceId('Microsoft.KeyVault/vaults', parameters('keyVaultName'))]" + }, + "aiServicesName": { + "type": "string", + "value": "[variables('aiServicesName')]" + }, + "aiSearchName": { + "type": "string", + "value": "[variables('aiSearchName')]" + }, + "aiProjectName": { + "type": "string", + "value": "[variables('aiProjectName')]" + }, + "storageAccountName": { + "type": "string", + "value": "[variables('storageNameCleaned')]" + }, + "logAnalyticsId": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('workspaceName'))]" + }, + "storageAccountId": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageNameCleaned'))]" + } + } + } + }, + "dependsOn": [ + "aiServices", + "kvault", + "managedIdentityModule" + ] + }, + "managedIdentityModule": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "deploy_managed_identity", + "resourceGroup": "[resourceGroup().name]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "solutionName": { + "value": "[parameters('prefix')]" + }, + "managedIdentityId": { + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', format(variables('uniqueNameFormat'), 'containerapp-pull'))]" + }, + "managedIdentityPropPrin": { + "value": "[reference('pullIdentity').principalId]" + }, + "managedIdentityLocation": { + "value": "[reference('pullIdentity', '2023-07-31-preview', 'full').location]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "11364190519186458619" + } + }, + "parameters": { + "solutionName": { + "type": "string", + "minLength": 3, + "maxLength": 15, + "metadata": { + "description": "Solution Name" + } + }, + "managedIdentityId": { + "type": "string", + "metadata": { + "description": "Solution Location" + } + }, + "managedIdentityPropPrin": { + "type": "string" + }, + "managedIdentityLocation": { + "type": "string" + }, + "miName": { + "type": "string", + "defaultValue": "[format('{0}-managed-identity', parameters('solutionName'))]", + "metadata": { + "description": "Name" + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(resourceGroup().id, parameters('managedIdentityId'), resourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635'))]", + "properties": { + "principalId": "[parameters('managedIdentityPropPrin')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "principalType": "ServicePrincipal" + } + } + ], + "outputs": { + "managedIdentityOutput": { + "type": "object", + "value": { + "id": "[parameters('managedIdentityId')]", + "objectId": "[parameters('managedIdentityPropPrin')]", + "resourceId": "[parameters('managedIdentityId')]", + "location": "[parameters('managedIdentityLocation')]", + "name": "[parameters('miName')]" + } + }, + "managedIdentityId": { + "type": "string", + "value": "[parameters('managedIdentityId')]" + } + } + } + }, + "dependsOn": [ + "pullIdentity" + ] + }, + "deploymentScriptCLI": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "deploymentScriptCLI", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "kind": { + "value": "AzureCLI" + }, + "name": { + "value": "rdsmin001" + }, + "azCliVersion": { + "value": "2.69.0" + }, + "location": { + "value": "[parameters('location')]" + }, + "managedIdentities": { + "value": { + "userAssignedResourceIds": [ + "[reference('managedIdentityModule').outputs.managedIdentityId.value]" + ] + } + }, + "scriptContent": { + "value": "[format('az cosmosdb sql role assignment create --resource-group \"{0}\" --account-name \"{1}\" --role-definition-id \"{2}\" --scope \"{3}\" --principal-id \"{4}\"', resourceGroup().name, format(variables('uniqueNameFormat'), 'cosmos'), resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', format(variables('uniqueNameFormat'), 'cosmos'), '00000000-0000-0000-0000-000000000002'), resourceId('Microsoft.DocumentDB/databaseAccounts', format(variables('uniqueNameFormat'), 'cosmos')), reference('containerApp', '2024-03-01', 'full').identity.principalId)]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.32.4.45862", + "templateHash": "8965217851411422458" + }, + "name": "Deployment Scripts", + "description": "This module deploys Deployment Scripts.", + "owner": "Azure/module-maintainers" + }, + "definitions": { + "environmentVariableType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the environment variable." + } + }, + "secureValue": { + "type": "securestring", + "nullable": true, + "metadata": { + "description": "Conditional. The value of the secure environment variable. Required if `value` is null." + } + }, + "value": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The value of the environment variable. Required if `secureValue` is null." + } + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + }, + "managedIdentityOnlyUserAssignedType": { + "type": "object", + "properties": { + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if only user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.2.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "maxLength": 90, + "metadata": { + "description": "Required. Name of the Deployment Script." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "AzureCLI", + "AzurePowerShell" + ], + "metadata": { + "description": "Required. Specifies the Kind of the Deployment Script." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityOnlyUserAssignedType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Resource tags." + } + }, + "azPowerShellVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Azure PowerShell module version to be used. See a list of supported Azure PowerShell versions: https://mcr.microsoft.com/v2/azuredeploymentscripts-powershell/tags/list." + } + }, + "azCliVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Azure CLI module version to be used. See a list of supported Azure CLI versions: https://mcr.microsoft.com/v2/azure-cli/tags/list." + } + }, + "scriptContent": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Script body. Max length: 32000 characters. To run an external script, use primaryScriptURI instead." + } + }, + "primaryScriptUri": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Uri for the external script. This is the entry point for the external script. To run an internal script, use the scriptContent parameter instead." + } + }, + "environmentVariables": { + "type": "array", + "items": { + "$ref": "#/definitions/environmentVariableType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The environment variables to pass over to the script." + } + }, + "supportingScriptUris": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. List of supporting files for the external script (defined in primaryScriptUri). Does not work with internal scripts (code defined in scriptContent)." + } + }, + "subnetResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. List of subnet IDs to use for the container group. This is required if you want to run the deployment script in a private network. When using a private network, the `Storage File Data Privileged Contributor` role needs to be assigned to the user-assigned managed identity and the deployment principal needs to have permissions to list the storage account keys. Also, Shared-Keys must not be disabled on the used storage account [ref](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/deployment-script-vnet)." + } + }, + "arguments": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Command-line arguments to pass to the script. Arguments are separated by spaces." + } + }, + "retentionInterval": { + "type": "string", + "defaultValue": "P1D", + "metadata": { + "description": "Optional. Interval for which the service retains the script resource after it reaches a terminal state. Resource will be deleted when this duration expires. Duration is based on ISO 8601 pattern (for example P7D means one week)." + } + }, + "baseTime": { + "type": "string", + "defaultValue": "[utcNow('yyyy-MM-dd-HH-mm-ss')]", + "metadata": { + "description": "Generated. Do not provide a value! This date value is used to make sure the script run every time the template is deployed." + } + }, + "runOnce": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. When set to false, script will run every time the template is deployed. When set to true, the script will only run once." + } + }, + "cleanupPreference": { + "type": "string", + "defaultValue": "Always", + "allowedValues": [ + "Always", + "OnSuccess", + "OnExpiration" + ], + "metadata": { + "description": "Optional. The clean up preference when the script execution gets in a terminal state. Specify the preference on when to delete the deployment script resources. The default value is Always, which means the deployment script resources are deleted despite the terminal state (Succeeded, Failed, canceled)." + } + }, + "containerGroupName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Container group name, if not specified then the name will get auto-generated. Not specifying a 'containerGroupName' indicates the system to generate a unique name which might end up flagging an Azure Policy as non-compliant. Use 'containerGroupName' when you have an Azure Policy that expects a specific naming convention or when you want to fully control the name. 'containerGroupName' property must be between 1 and 63 characters long, must contain only lowercase letters, numbers, and dashes and it cannot start or end with a dash and consecutive dashes are not allowed." + } + }, + "storageAccountResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The resource ID of the storage account to use for this deployment script. If none is provided, the deployment script uses a temporary, managed storage account." + } + }, + "timeout": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Maximum allowed script execution time specified in ISO 8601 format. Default value is PT1H - 1 hour; 'PT30M' - 30 minutes; 'P5D' - 5 days; 'P1Y' 1 year." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + }, + { + "name": "subnetIds", + "count": "[length(coalesce(parameters('subnetResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('subnetResourceIds'), createArray())[copyIndex('subnetIds')]]" + } + } + ], + "builtInRoleNames": { + "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')]" + }, + "containerSettings": { + "containerGroupName": "[parameters('containerGroupName')]", + "subnetIds": "[if(not(empty(coalesce(variables('subnetIds'), createArray()))), variables('subnetIds'), null())]" + }, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null()), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]" + }, + "resources": { + "storageAccount": { + "condition": "[not(empty(parameters('storageAccountResourceId')))]", + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2023-05-01", + "subscriptionId": "[split(if(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountResourceId'), '//'), '/')[2]]", + "resourceGroup": "[split(if(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountResourceId'), '////'), '/')[4]]", + "name": "[last(split(if(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountResourceId'), 'dummyAccount'), '/'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.resources-deploymentscript.{0}.{1}', replace('0.5.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "deploymentScript": { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2023-08-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": "[variables('identity')]", + "kind": "[parameters('kind')]", + "properties": { + "azPowerShellVersion": "[if(equals(parameters('kind'), 'AzurePowerShell'), parameters('azPowerShellVersion'), null())]", + "azCliVersion": "[if(equals(parameters('kind'), 'AzureCLI'), parameters('azCliVersion'), null())]", + "containerSettings": "[if(not(empty(variables('containerSettings'))), variables('containerSettings'), null())]", + "storageAccountSettings": "[if(not(empty(parameters('storageAccountResourceId'))), if(not(empty(parameters('storageAccountResourceId'))), createObject('storageAccountKey', if(empty(parameters('subnetResourceIds')), listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(if(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountResourceId'), '//'), '/')[2], split(if(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountResourceId'), '////'), '/')[4]), 'Microsoft.Storage/storageAccounts', last(split(if(not(empty(parameters('storageAccountResourceId'))), parameters('storageAccountResourceId'), 'dummyAccount'), '/'))), '2023-01-01').keys[0].value, null()), 'storageAccountName', last(split(parameters('storageAccountResourceId'), '/'))), null()), null())]", + "arguments": "[parameters('arguments')]", + "environmentVariables": "[parameters('environmentVariables')]", + "scriptContent": "[if(not(empty(parameters('scriptContent'))), parameters('scriptContent'), null())]", + "primaryScriptUri": "[if(not(empty(parameters('primaryScriptUri'))), parameters('primaryScriptUri'), null())]", + "supportingScriptUris": "[if(not(empty(parameters('supportingScriptUris'))), parameters('supportingScriptUris'), null())]", + "cleanupPreference": "[parameters('cleanupPreference')]", + "forceUpdateTag": "[if(parameters('runOnce'), resourceGroup().name, parameters('baseTime'))]", + "retentionInterval": "[parameters('retentionInterval')]", + "timeout": "[parameters('timeout')]" + } + }, + "deploymentScript_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Resources/deploymentScripts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "deploymentScript" + ] + }, + "deploymentScript_roleAssignments": { + "copy": { + "name": "deploymentScript_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Resources/deploymentScripts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Resources/deploymentScripts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "deploymentScript" + ] + }, + "deploymentScriptLogs": { + "existing": true, + "type": "Microsoft.Resources/deploymentScripts/logs", + "apiVersion": "2023-08-01", + "name": "[format('{0}/{1}', parameters('name'), 'default')]", + "dependsOn": [ + "deploymentScript" + ] + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployment script." + }, + "value": "[resourceId('Microsoft.Resources/deploymentScripts', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the deployment script was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployment script." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('deploymentScript', '2023-08-01', 'full').location]" + }, + "outputs": { + "type": "object", + "metadata": { + "description": "The output of the deployment script." + }, + "value": "[coalesce(tryGet(reference('deploymentScript'), 'outputs'), createObject())]" + }, + "deploymentScriptLogs": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The logs of the deployment script." + }, + "value": "[split(reference('deploymentScriptLogs').log, '\n')]" + } + } + } + }, + "dependsOn": [ + "containerApp", + "cosmos", + "managedIdentityModule" + ] + } + } +} \ No newline at end of file diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 000000000..c7fc26a4a --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "backendExists": { + "value": "${SERVICE_BACKEND_RESOURCE_EXISTS=false}" + }, + "backendDefinition": { + "value": { + "settings": [ + { + "name": "", + "value": "${VAR}", + "_comment_name": "The name of the environment variable when running in Azure. If empty, ignored.", + "_comment_value": "The value to provide. This can be a fixed literal, or an expression like ${VAR} to use the value of 'VAR' from the current environment." + }, + { + "name": "", + "value": "${VAR_S}", + "secret": true, + "_comment_name": "The name of the environment variable when running in Azure. If empty, ignored.", + "_comment_value": "The value to provide. This can be a fixed literal, or an expression like ${VAR_S} to use the value of 'VAR_S' from the current environment." + } + ] + } + }, + "frontendExists": { + "value": "${SERVICE_FRONTEND_RESOURCE_EXISTS=false}" + }, + "frontendDefinition": { + "value": { + "settings": [ + { + "name": "", + "value": "${VAR}", + "_comment_name": "The name of the environment variable when running in Azure. If empty, ignored.", + "_comment_value": "The value to provide. This can be a fixed literal, or an expression like ${VAR} to use the value of 'VAR' from the current environment." + }, + { + "name": "", + "value": "${VAR_S}", + "secret": true, + "_comment_name": "The name of the environment variable when running in Azure. If empty, ignored.", + "_comment_value": "The value to provide. This can be a fixed literal, or an expression like ${VAR_S} to use the value of 'VAR_S' from the current environment." + } + ] + } + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + } + } +} diff --git a/infra/main2.bicep b/infra/main2.bicep new file mode 100644 index 000000000..9d9f3f1ca --- /dev/null +++ b/infra/main2.bicep @@ -0,0 +1,54 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the environment that can be used as part of naming resource convention') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +param backendExists bool +@secure() +param backendDefinition object +param frontendExists bool +@secure() +param frontendDefinition object + +@description('Id of the user or app to assign application roles') +param principalId string + +// Tags that should be applied to all resources. +// +// Note that 'azd-service-name' tags should be applied separately to service host resources. +// Example usage: +// tags: union(tags, { 'azd-service-name': }) +var tags = { + 'azd-env-name': environmentName +} + +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: 'rg-${environmentName}' + location: location + tags: tags +} + +module resources 'resources.bicep' = { + scope: rg + name: 'resources' + params: { + location: location + tags: tags + principalId: principalId + backendExists: backendExists + backendDefinition: backendDefinition + frontendExists: frontendExists + frontendDefinition: frontendDefinition + } +} + +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT +output AZURE_RESOURCE_BACKEND_ID string = resources.outputs.AZURE_RESOURCE_BACKEND_ID +output AZURE_RESOURCE_FRONTEND_ID string = resources.outputs.AZURE_RESOURCE_FRONTEND_ID diff --git a/infra/modules/fetch-container-image.bicep b/infra/modules/fetch-container-image.bicep new file mode 100644 index 000000000..78d1e7eeb --- /dev/null +++ b/infra/modules/fetch-container-image.bicep @@ -0,0 +1,8 @@ +param exists bool +param name string + +resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = if (exists) { + name: name +} + +output containers array = exists ? existingApp.properties.template.containers : [] diff --git a/infra/resources.bicep b/infra/resources.bicep new file mode 100644 index 000000000..3c9a580c2 --- /dev/null +++ b/infra/resources.bicep @@ -0,0 +1,242 @@ +@description('The location used for all deployed resources') +param location string = resourceGroup().location + +@description('Tags that will be applied to all resources') +param tags object = {} + + +param backendExists bool +@secure() +param backendDefinition object +param frontendExists bool +@secure() +param frontendDefinition object + +@description('Id of the user or app to assign application roles') +param principalId string + +var abbrs = loadJsonContent('./abbreviations.json') +var resourceToken = uniqueString(subscription().id, resourceGroup().id, location) + +// Monitor application with Azure Monitor +module monitoring 'br/public:avm/ptn/azd/monitoring:0.1.0' = { + name: 'monitoring' + params: { + logAnalyticsName: '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + applicationInsightsName: '${abbrs.insightsComponents}${resourceToken}' + applicationInsightsDashboardName: '${abbrs.portalDashboards}${resourceToken}' + location: location + tags: tags + } +} + +// Container registry +module containerRegistry 'br/public:avm/res/container-registry/registry:0.1.1' = { + name: 'registry' + params: { + name: '${abbrs.containerRegistryRegistries}${resourceToken}' + location: location + tags: tags + publicNetworkAccess: 'Enabled' + roleAssignments:[ + { + principalId: backendIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + } + { + principalId: frontendIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionIdOrName: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + } + ] + } +} + +// Container apps environment +module containerAppsEnvironment 'br/public:avm/res/app/managed-environment:0.4.5' = { + name: 'container-apps-environment' + params: { + logAnalyticsWorkspaceResourceId: monitoring.outputs.logAnalyticsWorkspaceResourceId + name: '${abbrs.appManagedEnvironments}${resourceToken}' + location: location + zoneRedundant: false + } +} + +module backendIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'backendidentity' + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}backend-${resourceToken}' + location: location + } +} + +module backendFetchLatestImage './modules/fetch-container-image.bicep' = { + name: 'backend-fetch-image' + params: { + exists: backendExists + name: 'backend' + } +} + +var backendAppSettingsArray = filter(array(backendDefinition.settings), i => i.name != '') +var backendSecrets = map(filter(backendAppSettingsArray, i => i.?secret != null), i => { + name: i.name + value: i.value + secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) +}) +var backendEnv = map(filter(backendAppSettingsArray, i => i.?secret == null), i => { + name: i.name + value: i.value +}) + +module backend 'br/public:avm/res/app/container-app:0.8.0' = { + name: 'backend' + params: { + name: 'backend' + ingressTargetPort: 8000 + scaleMinReplicas: 1 + scaleMaxReplicas: 10 + secrets: { + secureList: union([ + ], + map(backendSecrets, secret => { + name: secret.secretRef + value: secret.value + })) + } + containers: [ + { + image: backendFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'main' + resources: { + cpu: json('0.5') + memory: '1.0Gi' + } + env: union([ + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: monitoring.outputs.applicationInsightsConnectionString + } + { + name: 'AZURE_CLIENT_ID' + value: backendIdentity.outputs.clientId + } + { + name: 'PORT' + value: '8000' + } + ], + backendEnv, + map(backendSecrets, secret => { + name: secret.name + secretRef: secret.secretRef + })) + } + ] + managedIdentities:{ + systemAssigned: false + userAssignedResourceIds: [backendIdentity.outputs.resourceId] + } + registries:[ + { + server: containerRegistry.outputs.loginServer + identity: backendIdentity.outputs.resourceId + } + ] + environmentResourceId: containerAppsEnvironment.outputs.resourceId + location: location + tags: union(tags, { 'azd-service-name': 'backend' }) + } +} + +module frontendIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.2.1' = { + name: 'frontendidentity' + params: { + name: '${abbrs.managedIdentityUserAssignedIdentities}frontend-${resourceToken}' + location: location + } +} + +module frontendFetchLatestImage './modules/fetch-container-image.bicep' = { + name: 'frontend-fetch-image' + params: { + exists: frontendExists + name: 'frontend' + } +} + +var frontendAppSettingsArray = filter(array(frontendDefinition.settings), i => i.name != '') +var frontendSecrets = map(filter(frontendAppSettingsArray, i => i.?secret != null), i => { + name: i.name + value: i.value + secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32) +}) +var frontendEnv = map(filter(frontendAppSettingsArray, i => i.?secret == null), i => { + name: i.name + value: i.value +}) + +module frontend 'br/public:avm/res/app/container-app:0.8.0' = { + name: 'frontend' + params: { + name: 'frontend' + ingressTargetPort: 3000 + scaleMinReplicas: 1 + scaleMaxReplicas: 10 + secrets: { + secureList: union([ + ], + map(frontendSecrets, secret => { + name: secret.secretRef + value: secret.value + })) + } + containers: [ + { + image: frontendFetchLatestImage.outputs.?containers[?0].?image ?? 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: 'main' + resources: { + cpu: json('0.5') + memory: '1.0Gi' + } + env: union([ + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: monitoring.outputs.applicationInsightsConnectionString + } + { + name: 'AZURE_CLIENT_ID' + value: frontendIdentity.outputs.clientId + } + { + name: 'PORT' + value: '3000' + } + ], + frontendEnv, + map(frontendSecrets, secret => { + name: secret.name + secretRef: secret.secretRef + })) + } + ] + managedIdentities:{ + systemAssigned: false + userAssignedResourceIds: [frontendIdentity.outputs.resourceId] + } + registries:[ + { + server: containerRegistry.outputs.loginServer + identity: frontendIdentity.outputs.resourceId + } + ] + environmentResourceId: containerAppsEnvironment.outputs.resourceId + location: location + tags: union(tags, { 'azd-service-name': 'frontend' }) + } +} +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_RESOURCE_BACKEND_ID string = backend.outputs.resourceId +output AZURE_RESOURCE_FRONTEND_ID string = frontend.outputs.resourceId diff --git a/infra/scripts/checkquota.sh b/infra/scripts/checkquota.sh new file mode 100644 index 000000000..afc340378 --- /dev/null +++ b/infra/scripts/checkquota.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# List of Azure regions to check for quota (update as needed) +IFS=', ' read -ra REGIONS <<< "$AZURE_REGIONS" + +SUBSCRIPTION_ID="${AZURE_SUBSCRIPTION_ID}" +GPT_MIN_CAPACITY="${GPT_MIN_CAPACITY}" +AZURE_CLIENT_ID="${AZURE_CLIENT_ID}" +AZURE_TENANT_ID="${AZURE_TENANT_ID}" +AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET}" + +# Authenticate using Managed Identity +echo "Authentication using Managed Identity..." +if ! az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID"; then + echo "❌ Error: Failed to login using Managed Identity." + exit 1 +fi + +echo "🔄 Validating required environment variables..." +if [[ -z "$SUBSCRIPTION_ID" || -z "$GPT_MIN_CAPACITY" || -z "$REGIONS" ]]; then + echo "❌ ERROR: Missing required environment variables." + exit 1 +fi + +echo "🔄 Setting Azure subscription..." +if ! az account set --subscription "$SUBSCRIPTION_ID"; then + echo "❌ ERROR: Invalid subscription ID or insufficient permissions." + exit 1 +fi +echo "✅ Azure subscription set successfully." + +# Define models and their minimum required capacities +declare -A MIN_CAPACITY=( + ["OpenAI.Standard.gpt-4o"]=$GPT_MIN_CAPACITY +) + +VALID_REGION="" +for REGION in "${REGIONS[@]}"; do + echo "----------------------------------------" + echo "🔍 Checking region: $REGION" + + QUOTA_INFO=$(az cognitiveservices usage list --location "$REGION" --output json) + if [ -z "$QUOTA_INFO" ]; then + echo "⚠️ WARNING: Failed to retrieve quota for region $REGION. Skipping." + continue + fi + + INSUFFICIENT_QUOTA=false + for MODEL in "${!MIN_CAPACITY[@]}"; do + MODEL_INFO=$(echo "$QUOTA_INFO" | awk -v model="\"value\": \"$MODEL\"" ' + BEGIN { RS="},"; FS="," } + $0 ~ model { print $0 } + ') + + if [ -z "$MODEL_INFO" ]; then + echo "⚠️ WARNING: No quota information found for model: $MODEL in $REGION. Skipping." + continue + fi + + CURRENT_VALUE=$(echo "$MODEL_INFO" | awk -F': ' '/"currentValue"/ {print $2}' | tr -d ',' | tr -d ' ') + LIMIT=$(echo "$MODEL_INFO" | awk -F': ' '/"limit"/ {print $2}' | tr -d ',' | tr -d ' ') + + CURRENT_VALUE=${CURRENT_VALUE:-0} + LIMIT=${LIMIT:-0} + + CURRENT_VALUE=$(echo "$CURRENT_VALUE" | cut -d'.' -f1) + LIMIT=$(echo "$LIMIT" | cut -d'.' -f1) + + AVAILABLE=$((LIMIT - CURRENT_VALUE)) + + echo "✅ Model: $MODEL | Used: $CURRENT_VALUE | Limit: $LIMIT | Available: $AVAILABLE" + + if [ "$AVAILABLE" -lt "${MIN_CAPACITY[$MODEL]}" ]; then + echo "❌ ERROR: $MODEL in $REGION has insufficient quota." + INSUFFICIENT_QUOTA=true + break + fi + done + + if [ "$INSUFFICIENT_QUOTA" = false ]; then + VALID_REGION="$REGION" + break + fi + +done + +if [ -z "$VALID_REGION" ]; then + echo "❌ No region with sufficient quota found. Blocking deployment." + echo "QUOTA_FAILED=true" >> "$GITHUB_ENV" + exit 0 +else + echo "✅ Final Region: $VALID_REGION" + echo "VALID_REGION=$VALID_REGION" >> "$GITHUB_ENV" + exit 0 +fi diff --git a/next-steps.md b/next-steps.md new file mode 100644 index 000000000..3203dfccc --- /dev/null +++ b/next-steps.md @@ -0,0 +1,93 @@ +# Next Steps after `azd init` + +## Table of Contents + +1. [Next Steps](#next-steps) +2. [What was added](#what-was-added) +3. [Billing](#billing) +4. [Troubleshooting](#troubleshooting) + +## Next Steps + +### Provision infrastructure and deploy application code + +Run `azd up` to provision your infrastructure and deploy to Azure (or run `azd provision` then `azd deploy` to accomplish the tasks separately). Visit the service endpoints listed to see your application up-and-running! + +To troubleshoot any issues, see [troubleshooting](#troubleshooting). + +### Configure environment variables for running services + +Configure environment variables for running services by updating `settings` in [main.parameters.json](./infra/main.parameters.json). + +### Configure CI/CD pipeline + +Run `azd pipeline config` to configure the deployment pipeline to connect securely to Azure. + +- Deploying with `GitHub Actions`: Select `GitHub` when prompted for a provider. If your project lacks the `azure-dev.yml` file, accept the prompt to add it and proceed with pipeline configuration. + +- Deploying with `Azure DevOps Pipeline`: Select `Azure DevOps` when prompted for a provider. If your project lacks the `azure-dev.yml` file, accept the prompt to add it and proceed with pipeline configuration. + +## What was added + +### Infrastructure configuration + +To describe the infrastructure and application, `azure.yaml` along with Infrastructure as Code files using Bicep were added with the following directory structure: + +```yaml +- azure.yaml # azd project configuration +- infra/ # Infrastructure-as-code Bicep files + - main.bicep # Subscription level resources + - resources.bicep # Primary resource group resources + - modules/ # Library modules +``` + +The resources declared in [resources.bicep](./infra/resources.bicep) are provisioned when running `azd up` or `azd provision`. +This includes: + + +- Azure Container App to host the 'backend' service. +- Azure Container App to host the 'frontend' service. + +More information about [Bicep](https://aka.ms/bicep) language. + +### Build from source (no Dockerfile) + +#### Build with Buildpacks using Oryx + +If your project does not contain a Dockerfile, we will use [Buildpacks](https://buildpacks.io/) using [Oryx](https://github.com/microsoft/Oryx/blob/main/doc/README.md) to create an image for the services in `azure.yaml` and get your containerized app onto Azure. + +To produce and run the docker image locally: + +1. Run `azd package` to build the image. +2. Copy the *Image Tag* shown. +3. Run `docker run -it ` to run the image locally. + +#### Exposed port + +Oryx will automatically set `PORT` to a default value of `80` (port `8080` for Java). Additionally, it will auto-configure supported web servers such as `gunicorn` and `ASP .NET Core` to listen to the target `PORT`. If your application already listens to the port specified by the `PORT` variable, the application will work out-of-the-box. Otherwise, you may need to perform one of the steps below: + +1. Update your application code or configuration to listen to the port specified by the `PORT` variable +1. (Alternatively) Search for `targetPort` in a .bicep file under the `infra/app` folder, and update the variable to match the port used by the application. + +## Billing + +Visit the *Cost Management + Billing* page in Azure Portal to track current spend. For more information about how you're billed, and how you can monitor the costs incurred in your Azure subscriptions, visit [billing overview](https://learn.microsoft.com/azure/developer/intro/azure-developer-billing). + +## Troubleshooting + +Q: I visited the service endpoint listed, and I'm seeing a blank page, a generic welcome page, or an error page. + +A: Your service may have failed to start, or it may be missing some configuration settings. To investigate further: + +1. Run `azd show`. Click on the link under "View in Azure Portal" to open the resource group in Azure Portal. +2. Navigate to the specific Container App service that is failing to deploy. +3. Click on the failing revision under "Revisions with Issues". +4. Review "Status details" for more information about the type of failure. +5. Observe the log outputs from Console log stream and System log stream to identify any errors. +6. If logs are written to disk, use *Console* in the navigation to connect to a shell within the running container. + +For more troubleshooting information, visit [Container Apps troubleshooting](https://learn.microsoft.com/azure/container-apps/troubleshooting). + +### Additional information + +For additional information about setting up your `azd` project, visit our official [docs](https://learn.microsoft.com/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-convert).