diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 746588a..1034fb6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -58,7 +58,8 @@ }, "ghcr.io/jlaundry/devcontainer-features/azure-functions-core-tools:1": { "version": "latest" - } + }, + "ghcr.io/azure/azure-dev/azd:0": {} } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 92f0497..af6d2af 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ *.userosscache *.sln.docstates +.azure + # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..04e345c --- /dev/null +++ b/azure.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json +name: intro-intelligent-apps +services: + frontend: + project: ./labs/04-deploy-ai/02-frontend-ui/chainlitagent-ui + language: python + host: containerapp + api-python: + project: ./labs/04-deploy-ai/01-backend-api/acs-lc-python-api/acs-lc-python + language: python + host: containerapp + api-csharp: + project: ./labs/04-deploy-ai/01-backend-api/acs-sk-csharp-api/acs-sk-csharp + language: csharp + host: containerapp \ No newline at end of file diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 0000000..141ded1 --- /dev/null +++ b/infra/abbreviations.json @@ -0,0 +1,16 @@ +{ + "appManagedEnvironments": "cae-", + "containerRegistryRegistries": "cr", + "insightsComponents": "appi-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "resourcesResourceGroups": "rg-", + "storageStorageAccounts": "st", + "webServerFarms": "plan-", + "webSitesFunctions": "func-", + "kustoCluster": "kc-", + "managedIdentityUserAssignedIdentities": "id-", + "appContainerApps":"ca-", + "postgresServiceName": "pg-", + "searchService":"acs-" +} \ No newline at end of file diff --git a/infra/app/api-csharp.bicep b/infra/app/api-csharp.bicep new file mode 100644 index 0000000..13c1537 --- /dev/null +++ b/infra/app/api-csharp.bicep @@ -0,0 +1,114 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param applicationInsightsName string = '' +param identityName string +param serviceName string = 'api-csharp' + +param containerAppsEnvironmentName string +param containerRegistryName string +param openAICompletionDeploymentName string +param openAIEmbeddingDeploymentName string +param openAICompletionModel string +@secure() +param openAIKey string +param openAIEndpoint string +param searchServiceName string +param searchIndexName string +param searchServiceEndpoint string + + +resource apiGraphIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: identityName + location: location +} + +resource search 'Microsoft.Search/searchServices@2021-04-01-preview' existing = { + name: searchServiceName +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: applicationInsightsName +} + +module app '../core/host/container-app.bicep' = { + name: '${serviceName}-api-csharp' + params: { + name: name + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + identityType: 'UserAssigned' + identityName: apiGraphIdentity.name + containerAppsEnvironmentName: containerAppsEnvironmentName + containerRegistryName: containerRegistryName + containerCpuCoreCount: '1.0' + containerMemory: '2.0Gi' + env: [ + { + name: 'AZURE_CLIENT_ID' + value: apiGraphIdentity.properties.clientId + } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: applicationInsights.properties.ConnectionString + } + { + name: 'AZURE_OPENAI_COMPLETION_DEPLOYMENT_NAME' + value: openAICompletionDeploymentName + } + { + name: 'AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME' + value: openAIEmbeddingDeploymentName + } + { + name: 'AZURE_COGNITIVE_SEARCH_SERVICE_NAME' + value: searchServiceName + } + { + name: 'AZURE_COGNITIVE_SEARCH_ENDPOINT_NAME' + value: searchServiceEndpoint + } + { + name: 'AZURE_COGNITIVE_SEARCH_INDEX_NAME' + value: searchIndexName + } + { + name: 'AZURE_COGNITIVE_SEARCH_API_KEY' + value: search.listAdminKeys().primaryKey + } + { + name: 'AZURE_TENANT_ID' + value: apiGraphIdentity.properties.tenantId + } + { + name: 'OPENAI_COMPLETION_MODEL' + value: openAICompletionModel + } + { + name: 'OPENAI_API_VERSION' + value: '2023-05-15' + } + { + name: 'OPENAI_API_BASE' + value: openAIEndpoint + } + { + name: 'OPENAI_API_KEY' + value: openAIKey + } + { + name: 'OPENAI_API_TYPE' + value: 'azure' + } + ] + targetPort: 5291 + } +} + + + +output SERVICE_API_CSHARP_IDENTITY_PRINCIPAL_ID string = apiGraphIdentity.properties.principalId +output SERVICE_API_CSHARP_NAME string = app.outputs.name +output SERVICE_API_CSHARP_URI string = app.outputs.uri +output SERVICE_API_CSHARP_IMAGE_NAME string = app.outputs.imageName diff --git a/infra/app/api-python.bicep b/infra/app/api-python.bicep new file mode 100644 index 0000000..86a5e6a --- /dev/null +++ b/infra/app/api-python.bicep @@ -0,0 +1,113 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param applicationInsightsName string = '' +param identityName string +param serviceName string = 'api-python' + +param containerAppsEnvironmentName string +param containerRegistryName string + +param openAICompletionDeploymentName string +param openAIEmbeddingDeploymentName string +param openAICompletionModel string +@secure() +param openAIKey string +param openAIEndpoint string +param searchServiceName string +param searchIndexName string +param searchServiceEndpoint string + +resource apiGraphIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: identityName + location: location +} + +resource search 'Microsoft.Search/searchServices@2021-04-01-preview' existing = { + name: searchServiceName +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: applicationInsightsName +} + +module app '../core/host/container-app.bicep' = { + name: '${serviceName}-api-python' + params: { + name: name + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + identityType: 'UserAssigned' + identityName: apiGraphIdentity.name + containerAppsEnvironmentName: containerAppsEnvironmentName + containerRegistryName: containerRegistryName + containerCpuCoreCount: '1.0' + containerMemory: '2.0Gi' + env: [ + { + name: 'AZURE_CLIENT_ID' + value: apiGraphIdentity.properties.clientId + } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: applicationInsights.properties.ConnectionString + } + { + name: 'AZURE_OPENAI_COMPLETION_DEPLOYMENT_NAME' + value: openAICompletionDeploymentName + } + { + name: 'AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME' + value: openAIEmbeddingDeploymentName + } + { + name: 'AZURE_COGNITIVE_SEARCH_SERVICE_NAME' + value: searchServiceName + } + { + name: 'AZURE_COGNITIVE_SEARCH_ENDPOINT_NAME' + value: searchServiceEndpoint + } + { + name: 'AZURE_COGNITIVE_SEARCH_INDEX_NAME' + value: searchIndexName + } + { + name: 'AZURE_COGNITIVE_SEARCH_API_KEY' + value: search.listAdminKeys().primaryKey + } + { + name: 'AZURE_TENANT_ID' + value: apiGraphIdentity.properties.tenantId + } + { + name: 'OPENAI_COMPLETION_MODEL' + value: openAICompletionModel + } + { + name: 'OPENAI_API_VERSION' + value: '2023-05-15' + } + { + name: 'OPENAI_API_BASE' + value: openAIEndpoint + } + { + name: 'OPENAI_API_KEY' + value: openAIKey + } + { + name: 'OPENAI_API_TYPE' + value: 'azure' + } + ] + targetPort: 5291 + } +} + + +output SERVICE_API_PYTHON_IDENTITY_PRINCIPAL_ID string = apiGraphIdentity.properties.principalId +output SERVICE_API_PYTHON_NAME string = app.outputs.name +output SERVICE_API_PYTHON_URI string = app.outputs.uri +output SERVICE_API_PYTHON_IMAGE_NAME string = app.outputs.imageName diff --git a/infra/app/frontend.bicep b/infra/app/frontend.bicep new file mode 100644 index 0000000..642866f --- /dev/null +++ b/infra/app/frontend.bicep @@ -0,0 +1,56 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param applicationInsightsName string = '' +param identityName string +param serviceName string = 'frontend' + +param containerAppsEnvironmentName string +param containerRegistryName string + +param backendApiUrl string + +resource frontendIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: identityName + location: location +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: applicationInsightsName +} + +module app '../core/host/container-app.bicep' = { + name: '${serviceName}-frontend' + params: { + name: name + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + identityType: 'UserAssigned' + identityName: frontendIdentity.name + containerAppsEnvironmentName: containerAppsEnvironmentName + containerRegistryName: containerRegistryName + containerCpuCoreCount: '1.0' + containerMemory: '2.0Gi' + env: [ + { + name: 'AZURE_CLIENT_ID' + value: frontendIdentity.properties.clientId + } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: applicationInsights.properties.ConnectionString + } + { + name: 'BACKEND_API_BASE' + value: backendApiUrl + } + ] + targetPort: 8000 + } +} + +output SERVICE_FRONTEND_API_IDENTITY_PRINCIPAL_ID string = frontendIdentity.properties.principalId +output SERVICE_FRONTEND_API_NAME string = app.outputs.name +output SERVICE_FRONTEND_API_URI string = app.outputs.uri +output SERVICE_FRONTEND_API_IMAGE_NAME string = app.outputs.imageName diff --git a/infra/core/host/container-app.bicep b/infra/core/host/container-app.bicep new file mode 100644 index 0000000..0ed8701 --- /dev/null +++ b/infra/core/host/container-app.bicep @@ -0,0 +1,165 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +@description('Allowed origins') +param allowedOrigins array = [] + +param probes array = [] + +@description('Name of the environment for container apps') +param containerAppsEnvironmentName string + +@description('CPU cores allocated to a single container instance, e.g., 0.5') +param containerCpuCoreCount string = '0.5' + +@description('The maximum number of replicas to run. Must be at least 1.') +@minValue(1) +param containerMaxReplicas int = 10 + +@description('Memory allocated to a single container instance, e.g., 1Gi') +param containerMemory string = '1.0Gi' + +@description('The minimum number of replicas to run. Must be at least 1.') +param containerMinReplicas int = 1 + +@description('The name of the container') +param containerName string = 'main' + +@description('The name of the container registry') +param containerRegistryName string = '' + +@description('The protocol used by Dapr to connect to the app, e.g., http or grpc') +@allowed([ 'http', 'grpc' ]) +param daprAppProtocol string = 'http' + +@description('The Dapr app ID') +param daprAppId string = containerName + +@description('Enable Dapr') +param daprEnabled bool = false + +@description('The environment variables for the container') +param env array = [] + +@description('Specifies if the resource ingress is exposed externally') +param external bool = true + +@description('The name of the user-assigned identity') +param identityName string = '' + +@description('The type of identity for the resource') +@allowed([ 'None', 'SystemAssigned', 'UserAssigned' ]) +param identityType string = 'None' + +@description('The name of the container image') +param imageName string = '' + +@description('Specifies if Ingress is enabled for the container app') +param ingressEnabled bool = true + +param revisionMode string = 'Single' + +@description('The secrets required for the container') +param secrets array = [] + +@description('The service binds associated with the container') +param serviceBinds array = [] + +@description('The name of the container apps add-on to use. e.g. redis') +param serviceType string = '' + +@description('The target port for the container') +param targetPort int = 80 + +resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = if (!empty(identityName)) { + name: identityName +} + +// Private registry support requires both an ACR name and a User Assigned managed identity +var usePrivateRegistry = !empty(identityName) && !empty(containerRegistryName) + +// Automatically set to `UserAssigned` when an `identityName` has been set +var normalizedIdentityType = !empty(identityName) ? 'UserAssigned' : identityType + +module containerRegistryAccess '../security/registry-access.bicep' = if (usePrivateRegistry) { + name: '${deployment().name}-registry-access' + params: { + containerRegistryName: containerRegistryName + principalId: usePrivateRegistry ? userIdentity.properties.principalId : '' + } +} + +resource app 'Microsoft.App/containerApps@2023-04-01-preview' = { + name: name + location: location + tags: tags + // It is critical that the identity is granted ACR pull access before the app is created + // otherwise the container app will throw a provision error + // This also forces us to use an user assigned managed identity since there would no way to + // provide the system assigned identity with the ACR pull access before the app is created + dependsOn: usePrivateRegistry ? [ containerRegistryAccess ] : [] + identity: { + type: normalizedIdentityType + userAssignedIdentities: !empty(identityName) && normalizedIdentityType == 'UserAssigned' ? { '${userIdentity.id}': {} } : null + } + properties: { + environmentId: containerAppsEnvironment.id + configuration: { + activeRevisionsMode: revisionMode + ingress: ingressEnabled ? { + external: external + targetPort: targetPort + transport: 'auto' + corsPolicy: { + allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) + } + } : null + dapr: daprEnabled ? { + enabled: true + appId: daprAppId + appProtocol: daprAppProtocol + appPort: ingressEnabled ? targetPort : 0 + } : { enabled: false } + secrets: secrets + service: !empty(serviceType) ? { type: serviceType } : null + registries: usePrivateRegistry ? [ + { + server: '${containerRegistryName}.azurecr.io' + identity: userIdentity.id + } + ] : [] + } + template: { + serviceBinds: !empty(serviceBinds) ? serviceBinds : null + containers: [ + { + image: !empty(imageName) ? imageName : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + name: containerName + env: env + resources: { + cpu: json(containerCpuCoreCount) + memory: containerMemory + } + probes: probes + } + ] + scale: { + minReplicas: containerMinReplicas + maxReplicas: containerMaxReplicas + } + + } + } +} + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-04-01-preview' existing = { + name: containerAppsEnvironmentName +} + +output defaultDomain string = containerAppsEnvironment.properties.defaultDomain +output identityPrincipalId string = normalizedIdentityType == 'None' ? '' : (empty(identityName) ? app.identity.principalId : userIdentity.properties.principalId) +output imageName string = imageName +output name string = app.name +output serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {} +output uri string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : '' diff --git a/infra/core/host/container-apps-environment.bicep b/infra/core/host/container-apps-environment.bicep new file mode 100644 index 0000000..f29079a --- /dev/null +++ b/infra/core/host/container-apps-environment.bicep @@ -0,0 +1,40 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +@description('Name of the Application Insights resource') +param applicationInsightsName string = '' + +@description('Specifies if Dapr is enabled') +param daprEnabled bool = false + +@description('Name of the Log Analytics workspace') +param logAnalyticsWorkspaceName string + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-04-01-preview' = { + name: name + location: location + tags: tags + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalyticsWorkspace.properties.customerId + sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey + } + } + daprAIInstrumentationKey: daprEnabled && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : '' + } +} + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { + name: logAnalyticsWorkspaceName +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (daprEnabled && !empty(applicationInsightsName)) { + name: applicationInsightsName +} + +output defaultDomain string = containerAppsEnvironment.properties.defaultDomain +output id string = containerAppsEnvironment.id +output name string = containerAppsEnvironment.name diff --git a/infra/core/host/container-apps.bicep b/infra/core/host/container-apps.bicep new file mode 100644 index 0000000..38f47e0 --- /dev/null +++ b/infra/core/host/container-apps.bicep @@ -0,0 +1,37 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param containerAppsEnvironmentName string +param containerRegistryName string +param containerRegistryResourceGroupName string = '' +param logAnalyticsWorkspaceName string +param applicationInsightsName string = '' + +module containerAppsEnvironment 'container-apps-environment.bicep' = { + name: '${name}-container-apps-environment' + params: { + name: containerAppsEnvironmentName + location: location + tags: tags + logAnalyticsWorkspaceName: logAnalyticsWorkspaceName + applicationInsightsName: applicationInsightsName + } +} + +module containerRegistry 'container-registry.bicep' = { + name: '${name}-container-registry' + scope: !empty(containerRegistryResourceGroupName) ? resourceGroup(containerRegistryResourceGroupName) : resourceGroup() + params: { + name: containerRegistryName + location: location + tags: tags + } +} + +output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain +output environmentName string = containerAppsEnvironment.outputs.name +output environmentId string = containerAppsEnvironment.outputs.id + +output registryLoginServer string = containerRegistry.outputs.loginServer +output registryName string = containerRegistry.outputs.name diff --git a/infra/core/host/container-registry.bicep b/infra/core/host/container-registry.bicep new file mode 100644 index 0000000..02af299 --- /dev/null +++ b/infra/core/host/container-registry.bicep @@ -0,0 +1,82 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +@description('Indicates whether admin user is enabled') +param adminUserEnabled bool = false + +@description('Indicates whether anonymous pull is enabled') +param anonymousPullEnabled bool = false + +@description('Indicates whether data endpoint is enabled') +param dataEndpointEnabled bool = false + +@description('Encryption settings') +param encryption object = { + status: 'disabled' +} + +@description('Options for bypassing network rules') +param networkRuleBypassOptions string = 'AzureServices' + +@description('Public network access setting') +param publicNetworkAccess string = 'Enabled' + +@description('SKU settings') +param sku object = { + name: 'Basic' +} + +@description('Zone redundancy setting') +param zoneRedundancy string = 'Disabled' + +@description('The log analytics workspace ID used for logging and monitoring') +param workspaceId string = '' + +// 2022-02-01-preview needed for anonymousPullEnabled +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = { + name: name + location: location + tags: tags + sku: sku + properties: { + adminUserEnabled: adminUserEnabled + anonymousPullEnabled: anonymousPullEnabled + dataEndpointEnabled: dataEndpointEnabled + encryption: encryption + networkRuleBypassOptions: networkRuleBypassOptions + publicNetworkAccess: publicNetworkAccess + zoneRedundancy: zoneRedundancy + } +} + +// TODO: Update diagnostics to be its own module +// Blocking issue: https://github.com/Azure/bicep/issues/622 +// Unable to pass in a `resource` scope or unable to use string interpolation in resource types +resource diagnostics 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (!empty(workspaceId)) { + name: 'registry-diagnostics' + scope: containerRegistry + properties: { + workspaceId: workspaceId + logs: [ + { + category: 'ContainerRegistryRepositoryEvents' + enabled: true + } + { + category: 'ContainerRegistryLoginEvents' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + timeGrain: 'PT1M' + } + ] + } +} + +output loginServer string = containerRegistry.properties.loginServer +output name string = containerRegistry.name diff --git a/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep new file mode 100644 index 0000000..5d35cc5 --- /dev/null +++ b/infra/core/monitor/applicationinsights.bicep @@ -0,0 +1,19 @@ +param name string +param location string = resourceGroup().location +param tags object = {} +param logAnalyticsWorkspaceId string + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: name + location: location + tags: tags + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspaceId + } +} + +output connectionString string = applicationInsights.properties.ConnectionString +output instrumentationKey string = applicationInsights.properties.InstrumentationKey +output name string = applicationInsights.name diff --git a/infra/core/monitor/loganalytics.bicep b/infra/core/monitor/loganalytics.bicep new file mode 100644 index 0000000..770544c --- /dev/null +++ b/infra/core/monitor/loganalytics.bicep @@ -0,0 +1,21 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { + name: name + location: location + tags: tags + properties: any({ + retentionInDays: 30 + features: { + searchVersion: 1 + } + sku: { + name: 'PerGB2018' + } + }) +} + +output id string = logAnalytics.id +output name string = logAnalytics.name diff --git a/infra/core/monitor/monitoring.bicep b/infra/core/monitor/monitoring.bicep new file mode 100644 index 0000000..bee800d --- /dev/null +++ b/infra/core/monitor/monitoring.bicep @@ -0,0 +1,29 @@ +param logAnalyticsName string +param applicationInsightsName string +param location string = resourceGroup().location +param tags object = {} + +module logAnalytics 'loganalytics.bicep' = { + name: 'loganalytics' + params: { + name: logAnalyticsName + location: location + tags: tags + } +} + +module applicationInsights 'applicationinsights.bicep' = { + name: 'applicationinsights' + params: { + name: applicationInsightsName + location: location + tags: tags + logAnalyticsWorkspaceId: logAnalytics.outputs.id + } +} + +output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString +output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey +output applicationInsightsName string = applicationInsights.outputs.name +output logAnalyticsWorkspaceId string = logAnalytics.outputs.id +output logAnalyticsWorkspaceName string = logAnalytics.outputs.name diff --git a/infra/core/search/search-services.bicep b/infra/core/search/search-services.bicep new file mode 100644 index 0000000..399a8f3 --- /dev/null +++ b/infra/core/search/search-services.bicep @@ -0,0 +1,62 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param sku object = { + name: 'standard' +} + +param authOptions object = {} +param disableLocalAuth bool = false +param disabledDataExfiltrationOptions array = [] +param encryptionWithCmk object = { + enforcement: 'Unspecified' +} +@allowed([ + 'default' + 'highDensity' +]) +param hostingMode string = 'default' +param networkRuleSet object = { + bypass: 'None' + ipRules: [] +} +param partitionCount int = 1 +@allowed([ + 'enabled' + 'disabled' +]) +param publicNetworkAccess string = 'enabled' +param replicaCount int = 1 +@allowed([ + 'disabled' + 'free' + 'standard' +]) +param semanticSearch string = 'disabled' + +resource search 'Microsoft.Search/searchServices@2021-04-01-preview' = { + name: name + location: location + tags: tags + identity: { + type: 'SystemAssigned' + } + properties: { + authOptions: authOptions + disableLocalAuth: disableLocalAuth + disabledDataExfiltrationOptions: disabledDataExfiltrationOptions + encryptionWithCmk: encryptionWithCmk + hostingMode: hostingMode + networkRuleSet: networkRuleSet + partitionCount: partitionCount + publicNetworkAccess: publicNetworkAccess + replicaCount: replicaCount + semanticSearch: semanticSearch + } + sku: sku +} + +output id string = search.id +output endpoint string = 'https://${name}.search.windows.net/' +output name string = search.name diff --git a/infra/core/security/registry-access.bicep b/infra/core/security/registry-access.bicep new file mode 100644 index 0000000..5335efa --- /dev/null +++ b/infra/core/security/registry-access.bicep @@ -0,0 +1,19 @@ +metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.' +param containerRegistryName string +param principalId string + +var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') + +resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: containerRegistry // Use when specifying a scope that is different than the deployment scope + name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole) + properties: { + roleDefinitionId: acrPullRole + principalType: 'ServicePrincipal' + principalId: principalId + } +} + +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { + name: containerRegistryName +} diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 0000000..ff4f845 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,157 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@minLength(1) +@description('Primary location for all resources') +param location string + +param openAIServiceId string +param openAIDeploymentId string +param openAIEmbeddingId string +param openAIEndpoint string +@secure() +param openAIKey string +@description('Flag that picks between csharp and python') +@allowed([ + 'csharp' + 'python' +]) +param backend string + +param resourceGroupName string = '' +param containerAppsEnvironmentName string = '' +param containerRegistryName string = '' +param applicationInsightsName string = '' +param logAnalyticsName string = '' +param searchServiceName string = '' +param apiCsharpName string = '' +param apiPythonName string = '' +param frontendName string = '' + +var abbrs = loadJsonContent('./abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +module monitoring './core/monitor/monitoring.bicep' = { + name: 'monitoring' + scope: rg + params: { + location: location + tags: tags + logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + } +} + +// Container apps host (including container registry) +module containerApps './core/host/container-apps.bicep' = { + name: 'container-apps' + scope: rg + params: { + name: 'app' + location: location + tags: tags + containerAppsEnvironmentName: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}' + containerRegistryName: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' + logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName + applicationInsightsName: monitoring.outputs.applicationInsightsName + } +} + +module searchService 'core/search/search-services.bicep' = { + name: 'search-service' + scope: rg + params: { + name: !empty(searchServiceName) ? searchServiceName : '${abbrs.searchService}${resourceToken}' + location: location + tags: tags + authOptions: { + aadOrApiKey: { + aadAuthFailureMode: 'http401WithBearerChallenge' + } + } + sku: { + name: 'standard' + } + semanticSearch: 'free' + } +} + +var indexName = 'cognitive-search-index' +module frontend 'app/frontend.bicep' = { + name: 'frontend' + scope: rg + params: { + name: !empty(frontendName) ? frontendName : '${abbrs.appContainerApps}frontend-${resourceToken}' + location: location + tags: tags + identityName: '${abbrs.managedIdentityUserAssignedIdentities}backstage-${resourceToken}' + applicationInsightsName: monitoring.outputs.applicationInsightsName + containerAppsEnvironmentName: containerApps.outputs.environmentName + containerRegistryName: containerApps.outputs.registryName + backendApiUrl: (backend == 'csharp') ? apicsharp.outputs.SERVICE_API_CSHARP_URI : apipython.outputs.SERVICE_API_PYTHON_URI + } +} + +module apicsharp 'app/api-csharp.bicep' = if (backend == 'csharp') { + name: 'api-csharp' + scope: rg + params: { + name: !empty(apiCsharpName) ? apiCsharpName : '${abbrs.appContainerApps}apicsharp-${resourceToken}' + location: location + tags: tags + identityName: '${abbrs.managedIdentityUserAssignedIdentities}apicsharp-${resourceToken}' + applicationInsightsName: monitoring.outputs.applicationInsightsName + containerAppsEnvironmentName: containerApps.outputs.environmentName + containerRegistryName: containerApps.outputs.registryName + openAICompletionDeploymentName: openAIDeploymentId + openAICompletionModel: openAIServiceId + openAIEmbeddingDeploymentName: openAIEmbeddingId + openAIEndpoint: openAIEndpoint + openAIKey: openAIKey + searchIndexName: indexName + searchServiceEndpoint: searchService.outputs.endpoint + searchServiceName: searchService.outputs.name + } +} + +module apipython 'app/api-python.bicep' = if (backend == 'python') { + name: 'api-python' + scope: rg + params: { + name: !empty(apiPythonName) ? apiPythonName : '${abbrs.appContainerApps}apipython-${resourceToken}' + location: location + tags: tags + identityName: '${abbrs.managedIdentityUserAssignedIdentities}backstage-${resourceToken}' + applicationInsightsName: monitoring.outputs.applicationInsightsName + containerAppsEnvironmentName: containerApps.outputs.environmentName + containerRegistryName: containerApps.outputs.registryName + openAICompletionDeploymentName: openAIDeploymentId + openAICompletionModel: openAIServiceId + openAIEmbeddingDeploymentName: openAIEmbeddingId + openAIEndpoint: openAIEndpoint + openAIKey: openAIKey + searchIndexName: indexName + searchServiceEndpoint: searchService.outputs.endpoint + searchServiceName: searchService.outputs.name + } +} + +// App outputs +output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId + +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerApps.outputs.registryLoginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerApps.outputs.registryName diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 0000000..4dc1e1b --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,31 @@ +{ + "$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}" + }, + "openAIServiceId": { + "value": "${OAI_SERVICE_ID}" + }, + "openAIDeploymentId": { + "value": "${OAI_DEPLOYMENT_ID}" + }, + "openAIEmbeddingId": { + "value": "${OAI_EMBEDDING_ID}" + }, + "openAIEndpoint": { + "value": "${OAI_ENDPOINT}" + }, + "openAIKey": { + "value": "${OAI_KEY}" + }, + "backend": { + "value": "${BACKEND_LANGUAGE}" + } + } + } + \ No newline at end of file