diff --git a/.gitignore b/.gitignore index 0712879..56d9d2f 100644 --- a/.gitignore +++ b/.gitignore @@ -171,3 +171,5 @@ studio/dist .env-dev .env-prod dump.sql +*.parameters.json +*.bicepparam \ No newline at end of file diff --git a/deployments/bicep/README.md b/deployments/bicep/README.md new file mode 100644 index 0000000..00f073f --- /dev/null +++ b/deployments/bicep/README.md @@ -0,0 +1,5 @@ +```bash +az deployment group create --name ExampleDeployment --resource-group jb-studio-test --parameters storage.bicepparam + +az deployment group create --resource-group jbstudiotest1 --template-file ./main.bicep --parameters main.bicepparam +``` \ No newline at end of file diff --git a/deployments/bicep/main.bicep b/deployments/bicep/main.bicep new file mode 100644 index 0000000..7d37c7a --- /dev/null +++ b/deployments/bicep/main.bicep @@ -0,0 +1,160 @@ +// create bicep version of the stuff above + +param resourceNamePrefix string +param location string +param postgresAdminUser string + +@secure() +param postgresAdminPassword string + +param postgresDatabaseName string +param cpu string = '0.5' +param memory string = '1Gi' + +@secure() +param AZURE_OPENAI_API_KEY string +param AZURE_OPENAI_API_VERSION string +param AZURE_OPENAI_ENDPOINT string +param FAST_MODEL string = 'gpt-4-turbo' +param SLOW_MODEL string = 'gpt-4-turbo' + +param pwrEngineImageName string +param pwrServerImageName string +@secure() +param imagePassword string +param imageRegistryLoginServer string +param imageUsername string + +param AAD_APP_CLIENT_ID string +param AAD_APP_TENANT_ID string +param ISSUER string + +param SERVER_HOST string + +param keyVaultName string + +@description('The secret url for the certificate in Azure Key Vault.') +@secure() +param keyVaultSecretId string + +// deploy ./vnet.bicep + +module vnet './modules/vnet.bicep' = { + name: '${resourceNamePrefix}-vnet' + params: { + vnetName: '${resourceNamePrefix}-vnet' + } +} + +// deploy the files ./eventhub.bicep and postgres.bicep + +module eventhub './modules/eventhub.bicep' = { + name: '${resourceNamePrefix}-eventhub' + params: { + eventHubNamespace: '${resourceNamePrefix}-eventhub-namespace' + location: location + } +} + +module postgres './modules/postgres.bicep' = { + name: '${resourceNamePrefix}-postgres' + params: { + resourceNamePrefix: resourceNamePrefix + location: location + postgresAdminUser: postgresAdminUser + postgresAdminPassword: postgresAdminPassword + postgresDatabaseName: postgresDatabaseName + } +} + +module storage './modules/storage.bicep' = { + name: '${resourceNamePrefix}-storage' + params: { + location: location + resourceNamePrefix: resourceNamePrefix + } +} + + +// create a public ip that will later be used for a load balancer + +resource publicIp 'Microsoft.Network/publicIPAddresses@2020-11-01' = { + name: '${resourceNamePrefix}-public-ip' + location: location + properties: { + publicIPAllocationMethod: 'Dynamic' + } +} + +// deploy the files ./server.bicep and ./engine.bicep + +module engine './modules/containers/engine.bicep' = { + name: '${resourceNamePrefix}-engine' + params: { + location: location + AZURE_OPENAI_API_KEY: AZURE_OPENAI_API_KEY + AZURE_OPENAI_API_VERSION: AZURE_OPENAI_API_VERSION + AZURE_OPENAI_ENDPOINT: AZURE_OPENAI_ENDPOINT + FAST_MODEL: FAST_MODEL + SLOW_MODEL: SLOW_MODEL + + containerName: '${resourceNamePrefix}-pwr-engine' + imageName: pwrEngineImageName + imagePassword: imagePassword + imageRegistryLoginServer: imageRegistryLoginServer + imageUsername: imageUsername + KAFKA_BROKER: eventhub.outputs.kafkaBroker + KAFKA_CONSUMER_PASSWORD: eventhub.outputs.kafkaConnectionPassword + KAFKA_CONSUMER_USERNAME: eventhub.outputs.kafkaConnectionUsername + memory: memory + numberCpuCores: cpu + } +} + + + +module server './modules/containers/server.bicep' = { + name: '${resourceNamePrefix}-server' + params: { + location: location + + containerName: '${resourceNamePrefix}-pwr-server' + AAD_APP_CLIENT_ID: AAD_APP_CLIENT_ID + AAD_APP_TENANT_ID: AAD_APP_TENANT_ID + ISSUER: ISSUER + // construct a full db string using the postgress params and the output server ip + dbConnectionString: 'postgresql://${postgresAdminUser}:${postgresAdminPassword}@${postgres.outputs.postgresqlServerIP}:5432/${postgresDatabaseName}' + SERVER_HOST: SERVER_HOST + + imageName: pwrServerImageName + imagePassword: imagePassword + imageRegistryLoginServer: imageRegistryLoginServer + imageUsername: imageUsername + KAFKA_BROKER: eventhub.outputs.kafkaBroker + KAFKA_PRODUCER_PASSWORD: eventhub.outputs.kafkaConnectionPassword + KAFKA_PRODUCER_USERNAME: eventhub.outputs.kafkaConnectionUsername + memory: memory + numberCpuCores: cpu + subnetId: vnet.outputs.defaultSubnetId + + } +} + + +// create a load balancer that will be used to route traffic to the containers +// module gateway './modules/gateway.bicep' = { +// name: '${resourceNamePrefix}-gateway' +// params: { +// resourceNamePrefix: resourceNamePrefix +// location: location +// subnetId: vnet.outputs.gatewaySubnetId +// publicIpId: publicIp.id +// backendIPAddress: server.outputs.containerIP +// keyVaultName: keyVaultName +// keyVaultSecretId: keyVaultSecretId +// } +// } + +output eventhubNamespace string = eventhub.outputs.kafkaBroker +output postgresqlServerName string = postgres.outputs.postgresqlServerIP + diff --git a/deployments/bicep/modules/containers/engine.bicep b/deployments/bicep/modules/containers/engine.bicep new file mode 100644 index 0000000..11b725e --- /dev/null +++ b/deployments/bicep/modules/containers/engine.bicep @@ -0,0 +1,99 @@ +param location string +param containerName string + +param numberCpuCores string +param memory string + +param imageRegistryLoginServer string +param imageUsername string +@secure() +param imagePassword string +param imageName string + +param KAFKA_BROKER string +param KAFKA_CONSUMER_USERNAME string + +@secure() +param KAFKA_CONSUMER_PASSWORD string + +@secure() +param AZURE_OPENAI_API_KEY string +param AZURE_OPENAI_API_VERSION string +param AZURE_OPENAI_ENDPOINT string +param FAST_MODEL string = 'gpt-4-turbo' +param SLOW_MODEL string = 'gpt-4-turbo' + +resource container 'Microsoft.ContainerInstance/containerGroups@2022-10-01-preview' = { + location: location + name: containerName + properties: { + containers: [ + { + name: containerName + properties: { + image: imageName + resources: { + requests: { + cpu: int(numberCpuCores) + memoryInGB: json(memory) + } + } + environmentVariables: [ + { + name: 'KAFKA_BROKER' + value: KAFKA_BROKER + } + { + name: 'KAFKA_USE_SASL' + value: 'true' + } + { + name: 'KAFKA_CONSUMER_USERNAME' + value: KAFKA_CONSUMER_USERNAME + } + { + name: 'KAFKA_CONSUMER_PASSWORD' + secureValue: KAFKA_CONSUMER_PASSWORD + } + { + name: 'KAFKA_ENGINE_TOPIC' + value: 'pwr_engine' + } + { + name: 'AZURE_OPENAI_API_KEY' + value: AZURE_OPENAI_API_KEY + } + { + name: 'AZURE_OPENAI_API_VERSION' + value: AZURE_OPENAI_API_VERSION + } + { + name: 'AZURE_OPENAI_ENDPOINT' + value: AZURE_OPENAI_ENDPOINT + } + { + name: 'FAST_MODEL' + value: FAST_MODEL + } + { + name: 'SLOW_MODEL' + value: SLOW_MODEL + } + ] + ports: [{port: 80, protocol: 'TCP'}] + } + } + ] + restartPolicy: 'OnFailure' + osType: 'Linux' + sku: 'Standard' + imageRegistryCredentials: [ + { + server: imageRegistryLoginServer + username: imageUsername + password: imagePassword + } + ] + } + tags: {} +} diff --git a/deployments/bicep/modules/containers/server.bicep b/deployments/bicep/modules/containers/server.bicep new file mode 100644 index 0000000..c74c5be --- /dev/null +++ b/deployments/bicep/modules/containers/server.bicep @@ -0,0 +1,118 @@ +param location string +param containerName string +param subnetId string + +param numberCpuCores string +param memory string + +param imageRegistryLoginServer string +param imageName string +param imageUsername string +@secure() +param imagePassword string + +param SERVER_HOST string +param dbConnectionString string + +param AAD_APP_CLIENT_ID string +param AAD_APP_TENANT_ID string +param ISSUER string + +param KAFKA_BROKER string +param KAFKA_PRODUCER_USERNAME string +@secure() +param KAFKA_PRODUCER_PASSWORD string + + +resource container 'Microsoft.ContainerInstance/containerGroups@2022-10-01-preview' = { + location: location + name: containerName + properties: { + containers: [ + { + name: containerName + properties: { + image: imageName + resources: { + requests: { + cpu: int(numberCpuCores) + memoryInGB: json(memory) + } + } + environmentVariables: [ + { + name: 'SERVER_HOST' + value: SERVER_HOST + } + { + name: 'DB_CONNECTION_STRING' + value: dbConnectionString + } + { + name: 'AAD_APP_CLIENT_ID' + value: AAD_APP_CLIENT_ID + } + { + name: 'AAD_APP_TENANT_ID' + value: AAD_APP_TENANT_ID + } + { + name: 'ISSUER' + value: ISSUER + } + { + name: 'KAFKA_BROKER' + value: KAFKA_BROKER + } + { + name: 'KAFKA_USE_SASL' + value: 'true' + } + { + name: 'KAFKA_ENGINE_TOPIC' + value: 'pwr_engine' + } + { + name: 'KAFKA_PRODUCER_USERNAME' + value: KAFKA_PRODUCER_USERNAME + } + { + name: 'KAFKA_PRODUCER_PASSWORD' + secureValue: KAFKA_PRODUCER_PASSWORD + } + ] + command: [ + 'uvicorn' + 'app.main:app' + '--workers' + '1' + '--host' + '0.0.0.0' + '--port' + '80' + ] + ports: [{port: 80, protocol: 'TCP'}, {port: 3000, protocol: 'TCP'}] + } + } + ] + restartPolicy: 'OnFailure' + osType: 'Linux' + sku: 'Standard' + imageRegistryCredentials: [ + { + server: imageRegistryLoginServer + username: imageUsername + password: imagePassword + } + ] + subnetIds: [ + {id: subnetId} + ] + } + tags: {} +} + + +// output the private IP adress assigned to the container group from the subnet + +output containerIP string = container.properties.ipAddress.ip diff --git a/deployments/bicep/modules/eventhub.bicep b/deployments/bicep/modules/eventhub.bicep new file mode 100644 index 0000000..ac2ec0d --- /dev/null +++ b/deployments/bicep/modules/eventhub.bicep @@ -0,0 +1,65 @@ +@description('The name of the Event Hub namespace.') +param eventHubNamespace string + +@description('The location for the resources.') +param location string + +resource eventhubNamespace_resource 'Microsoft.EventHub/namespaces@2024-01-01' = { + name: eventHubNamespace + location: location + sku: { + name: 'Standard' + tier: 'Standard' + capacity: 1 + } + properties: { + minimumTlsVersion: '1.2' + publicNetworkAccess: 'Enabled' + disableLocalAuth: false + zoneRedundant: true + isAutoInflateEnabled: true + maximumThroughputUnits: 5 + kafkaEnabled: true + } +} + +resource eventhubNamespace_sendlisten 'Microsoft.EventHub/namespaces/authorizationrules@2024-01-01' = { + parent: eventhubNamespace_resource + name: 'sendlisten' + properties: { + rights: [ + 'Send' + 'Listen' + ] + } +} + +resource eventhubNamespace_pwr 'Microsoft.EventHub/namespaces/eventhubs@2024-01-01' = { + parent: eventhubNamespace_resource + name: 'pwr_engine' + properties: { + retentionDescription: { + cleanupPolicy: 'Delete' + retentionTimeInHours: 1 + } + messageRetentionInDays: 1 + partitionCount: 3 + status: 'Active' + } +} + +resource eventhubNamespace_pwr_Default 'Microsoft.EventHub/namespaces/eventhubs/consumergroups@2024-01-01' = { + parent: eventhubNamespace_pwr + name: '$Default' + properties: {} +} + +resource eventhubNamespace_pwr_cooler_group_id 'Microsoft.EventHub/namespaces/eventhubs/consumergroups@2024-01-01' = { + parent: eventhubNamespace_pwr + name: 'cooler_group_id' + properties: {} +} + +output kafkaConnectionUsername string = '$ConnectionString' +output kafkaConnectionPassword string = eventhubNamespace_sendlisten.listKeys().primaryConnectionString +output kafkaBroker string = '${eventHubNamespace}.servicebus.windows.net:9093' diff --git a/deployments/bicep/modules/gateway.bicep b/deployments/bicep/modules/gateway.bicep new file mode 100644 index 0000000..494cf10 --- /dev/null +++ b/deployments/bicep/modules/gateway.bicep @@ -0,0 +1,162 @@ +@description('The resource ID of the subnet to which the Application Gateway should be connected.') +param subnetId string + +@description('The private IP address of the backend server.') +param backendIPAddress string + +@description('The secret url for the certificate in Azure Key Vault.') +@secure() +param keyVaultSecretId string + +param location string = resourceGroup().location +param resourceNamePrefix string +param publicIpId string + +param keyVaultName string + +var managedIdentityName = '${resourceNamePrefix}-gateway-identity' + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: managedIdentityName + location: location +} + +// assign the identity to keyvault with name +resource keyVaultAccess 'Microsoft.KeyVault/vaults/accessPolicies@2021-06-01-preview' = { + name: '${keyVaultName}/add' + properties: { + accessPolicies: [ + { + tenantId: subscription().tenantId + objectId: managedIdentity.properties.principalId + permissions: { + keys: ['get', 'list'] + secrets: ['get', 'list'] + } + } + ] + } +} + + +resource appGateway 'Microsoft.Network/applicationGateways@2020-06-01' = { + name: '${resourceNamePrefix}-appGateway' + location: location + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + // add the reference to the managed identity + '${managedIdentity.id}': {} + } + + } + properties: { + sku: { + name: 'Standard_v2' + tier: 'Standard_v2' + capacity: 2 + } + gatewayIPConfigurations: [ + { + name: 'appGatewayIpConfig' + properties: { + subnet: { + id: subnetId + } + } + } + ] + frontendIPConfigurations: [ + { + name: 'appGatewayFrontendIP' + properties: { + publicIPAddress: { + id: publicIpId + } + } + } + ] + frontendPorts: [ + { + name: 'frontendPortHttps' + properties: { + port: 443 + } + } + ] + backendAddressPools: [ + { + name: 'backendPool' + properties: { + backendAddresses: [ + { + ipAddress: backendIPAddress + } + ] + } + } + ] + backendHttpSettingsCollection: [ + { + name: 'backendHttpSettings' + properties: { + port: 80 + protocol: 'Http' + cookieBasedAffinity: 'Disabled' + pickHostNameFromBackendAddress: false + requestTimeout: 20 + } + } + ] + httpListeners: [ + { + name: 'httpsListener' + properties: { + frontendIPConfiguration: { + id: resourceId( + 'Microsoft.Network/applicationGateways/frontendIPConfigurations', + 'appGateway', + 'appGatewayFrontendIP' + ) + } + frontendPort: { + id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', 'appGateway', 'frontendPortHttps') + } + protocol: 'Https' + sslCertificate: { + id: resourceId('Microsoft.Network/applicationGateways/sslCertificates', 'appGateway', 'sslCertificate') + } + } + } + ] + requestRoutingRules: [ + { + name: 'routingRule' + properties: { + ruleType: 'Basic' + httpListener: { + id: resourceId('Microsoft.Network/applicationGateways/httpListeners', 'appGateway', 'httpsListener') + } + backendAddressPool: { + id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', 'appGateway', 'backendPool') + } + backendHttpSettings: { + id: resourceId( + 'Microsoft.Network/applicationGateways/backendHttpSettingsCollection', + 'appGateway', + 'backendHttpSettings' + ) + } + } + } + ] + sslCertificates: [ + { + name: 'sslCertificate' + properties: { + keyVaultSecretId: keyVaultSecretId + } + } + ] + } +} diff --git a/deployments/bicep/modules/postgres.bicep b/deployments/bicep/modules/postgres.bicep new file mode 100644 index 0000000..c162622 --- /dev/null +++ b/deployments/bicep/modules/postgres.bicep @@ -0,0 +1,85 @@ +@description('The prefix used by all resources created by this template.') +param resourceNamePrefix string + +@description('The location for the resources.') +param location string + +@description('The administrator username for the PostgreSQL Flexible Server.') +param postgresAdminUser string + +@description('The administrator password for the PostgreSQL Flexible Server.') +@secure() +param postgresAdminPassword string + +@description('The name of the database to create in the PostgreSQL Flexible Server.') +param postgresDatabaseName string + +var postgresqlServerName = '${resourceNamePrefix}-postgresql-server' + +resource postgresqlServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-12-01-preview' = { + name: postgresqlServerName + location: location + properties: { + replica: { + role: 'Primary' + } + storage: { + iops: 120 + tier: 'P4' + storageSizeGB: 32 + autoGrow: 'Enabled' + } + network: { + publicNetworkAccess: 'Enabled' + } + dataEncryption: { + type: 'SystemManaged' + } + authConfig: { + activeDirectoryAuth: 'Disabled' + passwordAuth: 'Enabled' + } + version: '16' + administratorLogin: postgresAdminUser + administratorLoginPassword: postgresAdminPassword + availabilityZone: '1' + backup: { + backupRetentionDays: 7 + geoRedundantBackup: 'Disabled' + } + highAvailability: { + mode: 'Disabled' + } + maintenanceWindow: { + customWindow: 'Disabled' + dayOfWeek: 0 + startHour: 0 + startMinute: 0 + } + replicationRole: 'Primary' + } + sku: { + name: 'Standard_D2ds_v4' + tier: 'GeneralPurpose' + } +} + +resource postgresAllowAllAzureIPs 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-12-01-preview' = { + parent: postgresqlServer + name: 'AllowAllAzureIPs' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } +} + +resource postgresqlServerName_postgresDatabase 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-12-01-preview' = { + parent: postgresqlServer + name: postgresDatabaseName + properties: { + charset: 'UTF8' + collation: 'en_US.utf8' + } +} + +output postgresqlServerIP string = postgresqlServer.properties.fullyQualifiedDomainName diff --git a/deployments/bicep/modules/storage.bicep b/deployments/bicep/modules/storage.bicep new file mode 100644 index 0000000..db6f0ed --- /dev/null +++ b/deployments/bicep/modules/storage.bicep @@ -0,0 +1,107 @@ +@description('The prefix used by all resources created by this template.') +param resourceNamePrefix string + +@description('The location for the resources.') +param location string + +var storageAccountName = '${resourceNamePrefix}storageaccount' +var storageContainerName = '${resourceNamePrefix}-storage-container' + +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { + name: storageAccountName + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + dnsEndpointType: 'Standard' + defaultToOAuthAuthentication: false + publicNetworkAccess: 'Enabled' + allowCrossTenantReplication: false + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: true + allowSharedKeyAccess: true + networkAcls: { + bypass: 'AzureServices' + virtualNetworkRules: [] + ipRules: [] + defaultAction: 'Allow' + } + supportsHttpsTrafficOnly: true + encryption: { + requireInfrastructureEncryption: false + services: { + file: { + keyType: 'Account' + enabled: true + } + blob: { + keyType: 'Account' + enabled: true + } + } + keySource: 'Microsoft.Storage' + } + accessTier: 'Hot' + } +} + +resource storageAccountName_default 'Microsoft.Storage/storageAccounts/blobServices@2023-05-01' = { + parent: storageAccount + name: 'default' + properties: { + changeFeed: { + enabled: false + } + restorePolicy: { + enabled: false + } + containerDeleteRetentionPolicy: { + enabled: true + days: 7 + } + cors: { + corsRules: [] + } + deleteRetentionPolicy: { + allowPermanentDelete: false + enabled: true + days: 7 + } + isVersioningEnabled: false + } +} + +resource storageAccountName_default_storageContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = { + parent: storageAccountName_default + name: storageContainerName + properties: { + immutableStorageWithVersioning: { + enabled: false + } + defaultEncryptionScope: '$account-encryption-key' + denyEncryptionScopeOverride: false + publicAccess: 'None' + } +} + +resource storageAccountName_default_web 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = { + parent: storageAccountName_default + name: '$web' + properties: { + immutableStorageWithVersioning: { + enabled: false + } + defaultEncryptionScope: '$account-encryption-key' + denyEncryptionScopeOverride: false + publicAccess: 'Container' + } + dependsOn: [ + storageAccountName_default_storageContainer + ] +} + +output AZURE_STORAGE_ACCOUNT_URL string = storageAccount.properties.primaryEndpoints.blob +output AZURE_STORAGE_ACCOUNT_KEY string = listKeys(storageAccountName, '2023-05-01').keys[0].value +output AZURE_STORAGE_CONTAINER string = storageContainerName diff --git a/deployments/bicep/modules/vnet.bicep b/deployments/bicep/modules/vnet.bicep new file mode 100644 index 0000000..ad1f0ce --- /dev/null +++ b/deployments/bicep/modules/vnet.bicep @@ -0,0 +1,47 @@ +@description('The name of the virtual network.') +param vnetName string + + +// create a virtual network with two subnets +resource pwrStudioVnet 'Microsoft.Network/virtualNetworks@2023-11-01' = { + name: vnetName + location: 'centralindia' + properties: { + addressSpace: { + addressPrefixes: [ + '10.0.0.0/16' + ] + } + subnets: [ + { + name: 'default' + properties: { + addressPrefix: '10.0.0.0/24' + delegations: [ + { + name: 'Microsoft.ContainerInstance/containerGroups' + properties: { + serviceName: 'Microsoft.ContainerInstance/containerGroups' + } + } + ] + } + } + { + name: 'gateway' + properties: { + addressPrefix: '10.0.1.0/24' + } + } + ] + } +} + + +// output the subnets as well as network info + +output defaultSubnetId string = pwrStudioVnet.properties.subnets[0].id +output gatewaySubnetId string = pwrStudioVnet.properties.subnets[1].id + +// output network info too +output vnetId string = pwrStudioVnet.id diff --git a/docs/assets/pwr-studio-hld.drawio b/docs/assets/pwr-studio-hld.drawio new file mode 100644 index 0000000..53ba6bb --- /dev/null +++ b/docs/assets/pwr-studio-hld.drawio @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +