From 80f8ea187160fafbd353085edd82d9fcb4bed5c2 Mon Sep 17 00:00:00 2001 From: Natalia Venditto Date: Wed, 8 May 2024 20:51:04 +0200 Subject: [PATCH] infra: add iac and config --- .gitignore | 4 + azure.yaml | 19 +++ infra/abbreviations.json | 135 +++++++++++++++++ infra/core/ai/cognitiveservices.bicep | 55 +++++++ infra/core/host/staticwebapp.bicep | 22 +++ infra/core/search/search-services.bicep | 68 +++++++++ infra/core/security/role.bicep | 21 +++ infra/core/storage/storage-account.bicep | 64 ++++++++ infra/main.bicep | 177 +++++++++++++++++++++++ infra/main.parameters.json | 42 ++++++ swa-cli.config.json | 2 +- vite.config.js | 5 +- 12 files changed, 612 insertions(+), 2 deletions(-) create mode 100644 azure.yaml create mode 100644 infra/abbreviations.json create mode 100644 infra/core/ai/cognitiveservices.bicep create mode 100644 infra/core/host/staticwebapp.bicep create mode 100644 infra/core/search/search-services.bicep create mode 100644 infra/core/security/role.bicep create mode 100644 infra/core/storage/storage-account.bicep create mode 100644 infra/main.bicep create mode 100644 infra/main.parameters.json diff --git a/.gitignore b/.gitignore index a5b4e88..0b18e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ ## ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +## Azure Developer CLI output +.azure/ +.azure/** + # User-specific files *.rsuser *.suo diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..ce4c2fc --- /dev/null +++ b/azure.yaml @@ -0,0 +1,19 @@ +name: azure-openai-assistant-javascript@1.0.0 +services: + webapp: + project: ./ + dist: dist + language: js + host: staticwebapp + hooks: + predeploy: + windows: + shell: pwsh + run: npm run build + interactive: false + continueOnError: false + posix: + shell: sh + run: npm run build + interactive: false + continueOnError: false \ No newline at end of file diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 0000000..703e503 --- /dev/null +++ b/infra/abbreviations.json @@ -0,0 +1,135 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationConfigurationStores": "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-", + "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/core/ai/cognitiveservices.bicep b/infra/core/ai/cognitiveservices.bicep new file mode 100644 index 0000000..83e7515 --- /dev/null +++ b/infra/core/ai/cognitiveservices.bicep @@ -0,0 +1,55 @@ +metadata description = 'Creates an Azure Cognitive Services instance.' +param name string +param location string = resourceGroup().location +param tags object = {} +@description('The custom subdomain name used to access the API. Defaults to the value of the name parameter.') +param customSubDomainName string = name +param deployments array = [] +param kind string = 'OpenAI' + +@allowed([ 'Enabled', 'Disabled' ]) +param publicNetworkAccess string = 'Enabled' +param sku object = { + name: 'S0' +} + +param allowedIpRules array = [] +param networkAcls object = empty(allowedIpRules) ? { + defaultAction: 'Allow' +} : { + ipRules: allowedIpRules + defaultAction: 'Deny' +} +param disableLocalAuth bool = false + +resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { + name: name + location: location + tags: tags + kind: kind + properties: { + customSubDomainName: customSubDomainName + publicNetworkAccess: publicNetworkAccess + networkAcls: networkAcls + disableLocalAuth: disableLocalAuth + } + 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 diff --git a/infra/core/host/staticwebapp.bicep b/infra/core/host/staticwebapp.bicep new file mode 100644 index 0000000..7bd695e --- /dev/null +++ b/infra/core/host/staticwebapp.bicep @@ -0,0 +1,22 @@ +metadata description = 'Creates an Azure Static Web Apps instance.' +param name string +param location string = resourceGroup().location +param tags object = {} + +param sku object = { + name: 'Free' + tier: 'Free' +} + +resource web 'Microsoft.Web/staticSites@2023-01-01' = { + name: name + location: location + tags: tags + sku: sku + properties: { + provider: 'Custom' + } +} + +output name string = web.name +output uri string = 'https://${web.properties.defaultHostname}' diff --git a/infra/core/search/search-services.bicep b/infra/core/search/search-services.bicep new file mode 100644 index 0000000..33fd83e --- /dev/null +++ b/infra/core/search/search-services.bicep @@ -0,0 +1,68 @@ +metadata description = 'Creates an Azure AI Search instance.' +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' + +var searchIdentityProvider = (sku.name == 'free') ? null : { + type: 'SystemAssigned' +} + +resource search 'Microsoft.Search/searchServices@2021-04-01-preview' = { + name: name + location: location + tags: tags + // The free tier does not support managed identity + identity: searchIdentityProvider + properties: { + authOptions: disableLocalAuth ? null : 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 +output principalId string = !empty(searchIdentityProvider) ? search.identity.principalId : '' + diff --git a/infra/core/security/role.bicep b/infra/core/security/role.bicep new file mode 100644 index 0000000..0b30cfd --- /dev/null +++ b/infra/core/security/role.bicep @@ -0,0 +1,21 @@ +metadata description = 'Creates a role assignment for a service principal.' +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/infra/core/storage/storage-account.bicep b/infra/core/storage/storage-account.bicep new file mode 100644 index 0000000..4b6febb --- /dev/null +++ b/infra/core/storage/storage-account.bicep @@ -0,0 +1,64 @@ +metadata description = 'Creates an Azure storage account.' +param name string +param location string = resourceGroup().location +param tags object = {} + +@allowed([ + 'Cool' + 'Hot' + 'Premium' ]) +param accessTier string = 'Hot' +param allowBlobPublicAccess bool = true +param allowCrossTenantReplication bool = true +param allowSharedKeyAccess bool = true +param containers array = [] +param defaultToOAuthAuthentication bool = false +param deleteRetentionPolicy object = {} +@allowed([ 'AzureDnsZone', 'Standard' ]) +param dnsEndpointType string = 'Standard' +param kind string = 'StorageV2' +param minimumTlsVersion string = 'TLS1_2' +param supportsHttpsTrafficOnly bool = true +param networkAcls object = { + bypass: 'AzureServices' + defaultAction: 'Allow' +} +@allowed([ 'Enabled', 'Disabled' ]) +param publicNetworkAccess string = 'Enabled' +param sku object = { name: 'Standard_LRS' } + +resource storage 'Microsoft.Storage/storageAccounts@2022-05-01' = { + name: name + location: location + tags: tags + kind: kind + sku: sku + properties: { + accessTier: accessTier + allowBlobPublicAccess: allowBlobPublicAccess + allowCrossTenantReplication: allowCrossTenantReplication + allowSharedKeyAccess: allowSharedKeyAccess + defaultToOAuthAuthentication: defaultToOAuthAuthentication + dnsEndpointType: dnsEndpointType + minimumTlsVersion: minimumTlsVersion + networkAcls: networkAcls + publicNetworkAccess: publicNetworkAccess + supportsHttpsTrafficOnly: supportsHttpsTrafficOnly + } + + resource blobServices 'blobServices' = if (!empty(containers)) { + name: 'default' + properties: { + deleteRetentionPolicy: deleteRetentionPolicy + } + resource container 'containers' = [for container in containers: { + name: container.name + properties: { + publicAccess: contains(container, 'publicAccess') ? container.publicAccess : 'None' + } + }] + } +} + +output name string = storage.name +output primaryEndpoints object = storage.properties.primaryEndpoints diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 0000000..c21b590 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,177 @@ +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 resourceGroupName string = '' +param webappName string = 'webapp' +param storageAccountName string = '' +var abbrs = loadJsonContent('abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } +param webappLocation string // Set in main.parameters.json + +param searchServiceName string = '' + +// OpenAI Cognitive Services +param chatModelName string // Set in main.parameters.json +param chatDeploymentName string = chatModelName +param chatModelVersion string // Set in main.parameters.json +param chatDeploymentCapacity int = 30 +param embeddingsModelName string // Set in main.parameters.json +param embeddingsModelVersion string // Set in main.parameters.json +param embeddingsDeploymentName string = embeddingsModelName +param embeddingsDeploymentCapacity int = 30 + +var finalOpenAiUrl = empty(openAiUrl) ? 'https://${openAi.outputs.name}.openai.azure.com' : openAiUrl +var storageUrl = 'https://${storage.outputs.name}.blob.${environment().suffixes.storage}' +var searchUrl = 'https://${search.outputs.name}.search.windows.net' + +// The free tier does not support managed identity (required) or semantic search (optional) +@allowed(['basic', 'standard', 'standard2', 'standard3', 'storage_optimized_l1', 'storage_optimized_l2']) +param searchServiceSkuName string + +param openAiLocation string // Set in main.parameters.json +param openAiSkuName string = 'S0' +param openAiUrl string = '' + +param blobContainerName string = 'files' + +// Id of the user or app to assign application roles +param principalId string = '' + +// Set automated deployment flag +param isContinuousDeployment bool // Set in main.parameters.json + +// Organize resources in a resource group +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +// The application frontend webapp +module webapp './core/host/staticwebapp.bicep' = { + name: 'webapp' + scope: resourceGroup + params: { + name: !empty(webappName) ? webappName : '${abbrs.webStaticSites}web-${resourceToken}' + location: webappLocation + tags: union(tags, { 'azd-service-name': webappName }) + } +} + +module search 'core/search/search-services.bicep' = { + name: 'search' + scope: resourceGroup + params: { + name: !empty(searchServiceName) ? searchServiceName : '${abbrs.searchSearchServices}${resourceToken}' + location: location + tags: tags + disableLocalAuth: true + authOptions: null + sku: { + name: searchServiceSkuName + } + } +} +// Storage for Azure Functions API and Blob storage +module storage './core/storage/storage-account.bicep' = { + name: 'storage' + scope: resourceGroup + params: { + name: !empty(storageAccountName) ? storageAccountName : '${abbrs.storageStorageAccounts}${resourceToken}' + location: location + tags: tags + allowBlobPublicAccess: false + containers: [ + { + name: blobContainerName + publicAccess: 'None' + } + ] + } +} + +module openAi 'core/ai/cognitiveservices.bicep' = if (empty(openAiUrl)) { + name: 'openai' + scope: resourceGroup + params: { + name: '${abbrs.cognitiveServicesAccounts}${resourceToken}' + location: openAiLocation + tags: tags + sku: { + name: openAiSkuName + } + disableLocalAuth: true + deployments: [ + { + name: chatDeploymentName + model: { + format: 'OpenAI' + name: chatModelName + version: chatModelVersion + } + sku: { + name: 'Standard' + capacity: chatDeploymentCapacity + } + } + { + name: embeddingsDeploymentName + model: { + format: 'OpenAI' + name: embeddingsModelName + version: embeddingsModelVersion + } + capacity: embeddingsDeploymentCapacity + } + ] + } +} + +// User roles +module openAiRoleUser 'core/security/role.bicep' = if (!isContinuousDeployment) { + scope: resourceGroup + name: 'openai-role-user' + params: { + principalId: principalId + // Cognitive Services OpenAI User + roleDefinitionId: '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' + principalType: 'User' + } +} + +module storageRoleUser 'core/security/role.bicep' = if (!isContinuousDeployment) { + scope: resourceGroup + name: 'storage-contrib-role-user' + params: { + principalId: principalId + // Storage Blob Data Contributor + roleDefinitionId: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' + principalType: 'User' + } +} + +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +output AZURE_RESOURCE_GROUP string = resourceGroup.name + +output AZURE_OPENAI_API_ENDPOINT string = finalOpenAiUrl +output AZURE_OPENAI_API_DEPLOYMENT_NAME string = chatDeploymentName +output AZURE_OPENAI_API_MODEL string = chatModelName +output AZURE_OPENAI_API_MODEL_VERSION string = chatModelVersion +output AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME string = embeddingsDeploymentName +output AZURE_OPENAI_API_EMBEDDINGS_MODEL string = embeddingsModelName +output AZURE_OPENAI_API_EMBEDDINGS_MODEL_VERSION string = embeddingsModelVersion +output AZURE_STORAGE_URL string = storageUrl +output AZURE_STORAGE_CONTAINER_NAME string = blobContainerName +output AZURE_AISEARCH_ENDPOINT string = searchUrl + +output WEBAPP_URL string = webapp.outputs.uri diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 0000000..bfee8af --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "resourceGroupName": { + "value": "${AZURE_RESOURCE_GROUP}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "openAiLocation": { + "value": "${AZURE_OPENAI_LOCATION=swedencentral}" + }, + "chatModelName": { + "value": "${AZURE_OPENAI_API_MODEL=gpt-35-turbo}" + }, + "chatModelVersion": { + "value": "${AZURE_OPENAI_API_MODEL_VERSION=1106}" + }, + "embeddingsModelName": { + "value": "${AZURE_OPENAI_API_EMBEDDINGS_MODEL=text-embedding-ada-002}" + }, + "embeddingsModelVersion": { + "value": "${AZURE_OPENAI_API_EMBEDDINGS_MODEL_VERSION=2}" + }, + "webappLocation": { + "value": "${AZURE_WEBAPP_LOCATION=eastus2}" + }, + "searchServiceSkuName": { + "value": "${AZURE_SEARCH_SERVICE_SKU=basic}" + }, + "isContinuousDeployment": { + "value": "${CI=false}" + } + } +} diff --git a/swa-cli.config.json b/swa-cli.config.json index 949c179..a696da6 100644 --- a/swa-cli.config.json +++ b/swa-cli.config.json @@ -4,7 +4,7 @@ "azure-openai-assistant-javascript": { "appLocation": "src", "apiLocation": "api", - "outputLocation": "src/dist", + "outputLocation": "/dist", "apiLanguage": "node", "apiVersion": "18", "appBuildCommand": "npm run build", diff --git a/vite.config.js b/vite.config.js index 13c52f6..313935d 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,4 +1,7 @@ export default { - root: "src", + root: "./src", + build: { + outDir: '../dist' + }, envPrefix: ["ASSISTANT_ID", "AZURE_", "OPENAI_"], };