diff --git a/application-workloads/chatgpt-base-v2/azuredeploy.parameters.json b/application-workloads/chatgpt-base-v2/azuredeploy.parameters.json new file mode 100644 index 0000000..0442949 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/azuredeploy.parameters.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "name": { + "value": "azinsider-chatgpt" + }, + "location": { + "value": "eastus" + }, + "principalId": { + "value": "e8cace21-41c9-4995-9ef2-aa4694cb3d8a" + }, + "openAiResourceName": { + "value": "azinsider-OpenAI" + }, + "openAiResourceGroupName": { + "value": "openai" + }, + "openAiResourceGroupLocation": { + "value": "eastus" + }, + "openAiSkuName": { + "value": "S0" + }, + "createRoleForUser": { + "value": true + }, + "acaExists": { + "value": false + } + } +} \ No newline at end of file diff --git a/application-workloads/chatgpt-base-v2/container-apps.bicep b/application-workloads/chatgpt-base-v2/container-apps.bicep new file mode 100644 index 0000000..039eeae --- /dev/null +++ b/application-workloads/chatgpt-base-v2/container-apps.bicep @@ -0,0 +1,54 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param identityName string +param containerAppsEnvironmentName string +param containerRegistryName string +param serviceName string = 'aca' +param exists bool +param openAiDeploymentName string +param openAiEndpoint string + +resource acaIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: identityName + location: location +} + + +module app 'modules/container-app-upsert.bicep' = { + name: '${serviceName}-container-app-module' + params: { + name: name + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + identityName: acaIdentity.name + exists: exists + containerAppsEnvironmentName: containerAppsEnvironmentName + containerRegistryName: containerRegistryName + env: [ + { + name: 'AZURE_OPENAI_CHATGPT_DEPLOYMENT' + value: openAiDeploymentName + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: openAiEndpoint + } + { + name: 'RUNNING_IN_PRODUCTION' + value: 'true' + } + { + name: 'AZURE_OPENAI_CLIENT_ID' + value: acaIdentity.properties.clientId + } + ] + targetPort: 50505 + } +} + +output SERVICE_ACA_IDENTITY_PRINCIPAL_ID string = acaIdentity.properties.principalId +output SERVICE_ACA_NAME string = app.outputs.name +output SERVICE_ACA_URI string = app.outputs.uri +output SERVICE_ACA_IMAGE_NAME string = app.outputs.imageName diff --git a/application-workloads/chatgpt-base-v2/diagram.png b/application-workloads/chatgpt-base-v2/diagram.png new file mode 100644 index 0000000..3e6b062 Binary files /dev/null and b/application-workloads/chatgpt-base-v2/diagram.png differ diff --git a/application-workloads/chatgpt-base-v2/main.bicep b/application-workloads/chatgpt-base-v2/main.bicep new file mode 100644 index 0000000..2344e02 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/main.bicep @@ -0,0 +1,141 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name which is used to generate a short unique hash for each resource') +param name string + +@minLength(1) +@description('Primary location for all resources') +param location string + +param modelName string = 'gpt-35-turbo' + +@description('Id of the user or app to assign application roles') +param principalId string = 'e8cace21-41c9-4995-9ef2-aa4694cb3d8a' + +@description('Flag to decide where to create OpenAI role for current user') +param createRoleForUser bool = false + +param acaExists bool = false + +param openAiResourceName string = '' +param openAiResourceGroupName string = '' +param openAiResourceGroupLocation string = '' +param openAiSkuName string = '' +param openAiDeploymentCapacity int = 30 + +var resourceToken = toLower(uniqueString(subscription().id, name, location)) +var tags = { 'azd-env-name': name } + +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: '${name}-rg' + location: location + tags: tags +} + +resource openAiResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' existing = if (!empty(openAiResourceGroupName)) { + name: !empty(openAiResourceGroupName) ? openAiResourceGroupName : resourceGroup.name +} + +var prefix = '${name}-${resourceToken}' + +var openAiDeploymentName = 'chatgpt' +module openAi 'modules/cognitive-services.bicep' = { + name: 'openai' + scope: openAiResourceGroup + params: { + name: !empty(openAiResourceName) ? openAiResourceName : '${resourceToken}-cog' + location: !empty(openAiResourceGroupLocation) ? openAiResourceGroupLocation : location + tags: tags + sku: { + name: !empty(openAiSkuName) ? openAiSkuName : 'S0' + } + deployments: [ + { + name: openAiDeploymentName + model: { + format: 'OpenAI' + name: modelName + version: '0613' + } + sku: { + name: 'Standard' + capacity: 30 + } + } + ] + } +} + + +// Container apps host (including container registry) +module containerApps 'modules/container-apps.bicep' = { + name: 'container-apps' + scope: resourceGroup + params: { + name: 'app' + location: location + tags: tags + containerAppsEnvironmentName: '${prefix}-containerapps-env' + containerRegistryName: '${replace(prefix, '-', '')}registry' + } +} + +// Container app frontend +module aca 'container-apps.bicep' = { + name: 'aca' + scope: resourceGroup + params: { + name: replace('${take(prefix,19)}-ca', '--', '-') + location: location + tags: tags + identityName: '${prefix}-id-aca' + containerAppsEnvironmentName: containerApps.outputs.environmentName + containerRegistryName: containerApps.outputs.registryName + openAiDeploymentName: openAiDeploymentName + openAiEndpoint: openAi.outputs.endpoint + exists: acaExists + } +} + + +module openAiRoleUser 'modules/role.bicep' = if (createRoleForUser) { + scope: openAiResourceGroup + name: 'openai-role-user' + params: { + principalId: principalId + roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + principalType: 'ServicePrincipal' + } +} + + +module openAiRoleBackend 'modules/role.bicep' = { + scope: openAiResourceGroup + name: 'openai-role-backend' + params: { + principalId: aca.outputs.SERVICE_ACA_IDENTITY_PRINCIPAL_ID + roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + principalType: 'ServicePrincipal' + } +} + +output AZURE_LOCATION string = location + +output AZURE_OPENAI_CHATGPT_DEPLOYMENT string = openAiDeploymentName +output AZURE_OPENAI_ENDPOINT string = openAi.outputs.endpoint +output AZURE_OPENAI_KEY string = openAi.outputs.key +output AZURE_OPENAI_RESOURCE string = openAi.outputs.name +output AZURE_OPENAI_RESOURCE_GROUP string = openAiResourceGroup.name +output AZURE_OPENAI_SKU_NAME string = openAi.outputs.skuName +output AZURE_OPENAI_RESOURCE_GROUP_LOCATION string = openAiResourceGroup.location + +output SERVICE_ACA_IDENTITY_PRINCIPAL_ID string = aca.outputs.SERVICE_ACA_IDENTITY_PRINCIPAL_ID +output SERVICE_ACA_NAME string = aca.outputs.SERVICE_ACA_NAME +output SERVICE_ACA_URI string = aca.outputs.SERVICE_ACA_URI +output SERVICE_ACA_IMAGE_NAME string = aca.outputs.SERVICE_ACA_IMAGE_NAME + +output AZURE_CONTAINER_ENVIRONMENT_NAME string = containerApps.outputs.environmentName +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerApps.outputs.registryLoginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerApps.outputs.registryName diff --git a/application-workloads/chatgpt-base-v2/main.bicepparam b/application-workloads/chatgpt-base-v2/main.bicepparam new file mode 100644 index 0000000..3309931 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/main.bicepparam @@ -0,0 +1,20 @@ +using 'main.bicep' + +param name = 'azinsider-chatgpt' +param modelName = 'gpt-35-turbo' + +param location = 'eastus' + +param principalId = 'e8cace21-41c9-4995-9ef2-aa4694cb3d8a' + +param openAiResourceName = 'azinsider-OpenAI' + +param openAiResourceGroupName = 'openai' + +param openAiResourceGroupLocation = 'eastus' + +param openAiSkuName = 'S0' + +param createRoleForUser = true + +param acaExists = false diff --git a/application-workloads/chatgpt-base-v2/modules/cognitive-services.bicep b/application-workloads/chatgpt-base-v2/modules/cognitive-services.bicep new file mode 100644 index 0000000..749cbde --- /dev/null +++ b/application-workloads/chatgpt-base-v2/modules/cognitive-services.bicep @@ -0,0 +1,46 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param customSubDomainName string = name +param deployments array = [] +param kind string = 'OpenAI' +param publicNetworkAccess string = 'Enabled' +param sku object = { + name: 'S0' +} + +resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { + name: name + location: location + tags: tags + kind: kind + properties: { + customSubDomainName: customSubDomainName + publicNetworkAccess: publicNetworkAccess + networkAcls: { + defaultAction: 'Allow' + } + } + sku: sku +} + +@batchSize(1) +resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { + parent: account + name: deployment.name + properties: { + model: deployment.model + raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null + } + sku: contains(deployment, 'sku') ? deployment.sku : { + name: 'Standard' + capacity: 20 + } +}] + +output endpoint string = account.properties.endpoint +output id string = account.id +output name string = account.name +output skuName string = account.sku.name +output key string = account.listKeys().key1 diff --git a/application-workloads/chatgpt-base-v2/modules/container-app-upsert.bicep b/application-workloads/chatgpt-base-v2/modules/container-app-upsert.bicep new file mode 100644 index 0000000..c6398c7 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/modules/container-app-upsert.bicep @@ -0,0 +1,76 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param containerAppsEnvironmentName string +param containerName string = 'main' +param containerRegistryName string + +@description('Minimum number of replicas to run') +@minValue(1) +param containerMinReplicas int = 1 +@description('Maximum number of replicas to run') +@minValue(1) +param containerMaxReplicas int = 10 + +param secrets array = [] +param env array = [] +param external bool = true +param targetPort int = 80 +param exists bool + +@description('User assigned identity name') +param identityName string + +@description('Enabled Ingress for container app') +param ingressEnabled bool = true + +// Dapr Options +@description('Enable Dapr') +param daprEnabled bool = false +@description('Dapr app ID') +param daprAppId string = containerName +@allowed([ 'http', 'grpc' ]) +@description('Protocol used by Dapr to connect to the app, e.g. http or grpc') +param daprAppProtocol string = 'http' + +@description('CPU cores allocated to a single container instance, e.g. 0.5') +param containerCpuCoreCount string = '0.5' + +@description('Memory allocated to a single container instance, e.g. 1Gi') +param containerMemory string = '1.0Gi' + +resource existingApp 'Microsoft.App/containerApps@2022-03-01' existing = if (exists) { + name: name +} + +module app 'container-app.bicep' = { + name: '${deployment().name}-update' + params: { + name: name + location: location + tags: tags + identityName: identityName + ingressEnabled: ingressEnabled + containerName: containerName + containerAppsEnvironmentName: containerAppsEnvironmentName + containerRegistryName: containerRegistryName + containerCpuCoreCount: containerCpuCoreCount + containerMemory: containerMemory + containerMinReplicas: containerMinReplicas + containerMaxReplicas: containerMaxReplicas + daprEnabled: daprEnabled + daprAppId: daprAppId + daprAppProtocol: daprAppProtocol + secrets: secrets + external: external + env: env + imageName: exists ? existingApp.properties.template.containers[0].image : '' + targetPort: targetPort + } +} + +output defaultDomain string = app.outputs.defaultDomain +output imageName string = app.outputs.imageName +output name string = app.outputs.name +output uri string = app.outputs.uri diff --git a/application-workloads/chatgpt-base-v2/modules/container-app.bicep b/application-workloads/chatgpt-base-v2/modules/container-app.bicep new file mode 100644 index 0000000..6680eb5 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/modules/container-app.bicep @@ -0,0 +1,123 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param containerAppsEnvironmentName string +param containerName string = 'main' +param containerRegistryName string + +@description('Minimum number of replicas to run') +@minValue(1) +param containerMinReplicas int = 1 +@description('Maximum number of replicas to run') +@minValue(1) +param containerMaxReplicas int = 10 + +param secrets array = [] +param env array = [] +param external bool = true +param imageName string +param targetPort int = 80 + +@description('User assigned identity name') +param identityName string + +@description('Enabled Ingress for container app') +param ingressEnabled bool = true + +// Dapr Options +@description('Enable Dapr') +param daprEnabled bool = false +@description('Dapr app ID') +param daprAppId string = containerName +@allowed([ 'http', 'grpc' ]) +@description('Protocol used by Dapr to connect to the app, e.g. http or grpc') +param daprAppProtocol string = 'http' + +@description('CPU cores allocated to a single container instance, e.g. 0.5') +param containerCpuCoreCount string = '0.5' + +@description('Memory allocated to a single container instance, e.g. 1Gi') +param containerMemory string = '1.0Gi' + +resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = { + name: identityName +} + +module containerRegistryAccess 'registry-access.bicep' = { + name: '${deployment().name}-registry-access' + params: { + containerRegistryName: containerRegistryName + principalId: userIdentity.properties.principalId + } +} + +resource app 'Microsoft.App/containerApps@2022-03-01' = { + 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: [ containerRegistryAccess ] + identity: { + type: 'UserAssigned' + userAssignedIdentities: { '${userIdentity.id}': {} } + } + properties: { + managedEnvironmentId: containerAppsEnvironment.id + configuration: { + activeRevisionsMode: 'single' + ingress: ingressEnabled ? { + external: external + targetPort: targetPort + transport: 'auto' + } : null + dapr: daprEnabled ? { + enabled: true + appId: daprAppId + appProtocol: daprAppProtocol + appPort: ingressEnabled ? targetPort : 0 + } : { enabled: false } + secrets: secrets + registries: [ + { + server: '${containerRegistry.name}.azurecr.io' + identity: userIdentity.id + } + ] + } + template: { + containers: [ + { + image: !empty(imageName) ? imageName : 'daverendon/chatgpt-base:latest' + name: containerName + env: env + resources: { + cpu: json(containerCpuCoreCount) + memory: containerMemory + } + } + ] + scale: { + minReplicas: containerMinReplicas + maxReplicas: containerMaxReplicas + } + } + } +} + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' existing = { + name: containerAppsEnvironmentName +} + +// 2022-02-01-preview needed for anonymousPullEnabled +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { + name: containerRegistryName +} + +output defaultDomain string = containerAppsEnvironment.properties.defaultDomain +output imageName string = imageName +output name string = app.name +output uri string = 'https://${app.properties.configuration.ingress.fqdn}' diff --git a/application-workloads/chatgpt-base-v2/modules/container-apps-environment.bicep b/application-workloads/chatgpt-base-v2/modules/container-apps-environment.bicep new file mode 100644 index 0000000..4bfd397 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/modules/container-apps-environment.bicep @@ -0,0 +1,15 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' = { + name: name + location: location + tags: tags + properties: { + + } +} + +output defaultDomain string = containerAppsEnvironment.properties.defaultDomain +output name string = containerAppsEnvironment.name diff --git a/application-workloads/chatgpt-base-v2/modules/container-apps.bicep b/application-workloads/chatgpt-base-v2/modules/container-apps.bicep new file mode 100644 index 0000000..87e45f9 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/modules/container-apps.bicep @@ -0,0 +1,30 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param containerAppsEnvironmentName string +param containerRegistryName string +param applicationInsightsName string = '' + +module containerAppsEnvironment 'container-apps-environment.bicep' = { + name: '${name}-container-apps-environment' + params: { + name: containerAppsEnvironmentName + location: location + tags: tags + } +} + +module containerRegistry 'container-registry.bicep' = { + name: '${name}-container-registry' + params: { + name: containerRegistryName + location: location + tags: tags + } +} + +output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain +output environmentName string = containerAppsEnvironment.outputs.name +output registryLoginServer string = containerRegistry.outputs.loginServer +output registryName string = containerRegistry.outputs.name diff --git a/application-workloads/chatgpt-base-v2/modules/container-registry.bicep b/application-workloads/chatgpt-base-v2/modules/container-registry.bicep new file mode 100644 index 0000000..8045eb0 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/modules/container-registry.bicep @@ -0,0 +1,39 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param adminUserEnabled bool = true +param anonymousPullEnabled bool = false +param dataEndpointEnabled bool = false +param encryption object = { + status: 'disabled' +} +param networkRuleBypassOptions string = 'AzureServices' +param publicNetworkAccess string = 'Enabled' +param sku object = { + name: 'Basic' +} +param zoneRedundancy string = 'Disabled' + + +// 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 + } +} + + + +output loginServer string = containerRegistry.properties.loginServer +output name string = containerRegistry.name diff --git a/application-workloads/chatgpt-base-v2/modules/registry-access.bicep b/application-workloads/chatgpt-base-v2/modules/registry-access.bicep new file mode 100644 index 0000000..e17e404 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/modules/registry-access.bicep @@ -0,0 +1,18 @@ +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/application-workloads/chatgpt-base-v2/modules/role.bicep b/application-workloads/chatgpt-base-v2/modules/role.bicep new file mode 100644 index 0000000..dca01e1 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/modules/role.bicep @@ -0,0 +1,20 @@ +param principalId string + +@allowed([ + 'Device' + 'ForeignGroup' + 'Group' + 'ServicePrincipal' + 'User' +]) +param principalType string = 'ServicePrincipal' +param roleDefinitionId string + +resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) + properties: { + principalId: principalId + principalType: principalType + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) + } +} diff --git a/application-workloads/chatgpt-base-v2/readme.MD b/application-workloads/chatgpt-base-v2/readme.MD new file mode 100644 index 0000000..0960260 --- /dev/null +++ b/application-workloads/chatgpt-base-v2/readme.MD @@ -0,0 +1,5 @@ +# Deploy a Chat App with OpenAI and Bicep Language v2.0 + +This will deploy the application directly to Azure Container Apps + +👉 https://blog.azinsider.net/f42823f4d44d?source=friends_link&sk=90e5b0b1bd25c012f53113cdde70fa0a