diff --git a/Results/AppSpainCentral.png b/Results/AppSpainCentral.png new file mode 100644 index 00000000..ede190e2 Binary files /dev/null and b/Results/AppSpainCentral.png differ diff --git a/Results/PublicApi.png b/Results/PublicApi.png new file mode 100644 index 00000000..547ae95a Binary files /dev/null and b/Results/PublicApi.png differ diff --git a/Results/Scale.png b/Results/Scale.png new file mode 100644 index 00000000..e0e5134b Binary files /dev/null and b/Results/Scale.png differ diff --git a/Results/TM.png b/Results/TM.png new file mode 100644 index 00000000..363952e4 Binary files /dev/null and b/Results/TM.png differ diff --git a/Results/WebEastUs.png b/Results/WebEastUs.png new file mode 100644 index 00000000..9fa26e6e Binary files /dev/null and b/Results/WebEastUs.png differ diff --git a/azure.yaml b/azure.yaml index c63402c1..5f85ca4e 100644 --- a/azure.yaml +++ b/azure.yaml @@ -2,7 +2,15 @@ name: eShopOnWeb services: - web: + web-primary: project: ./src/Web language: csharp + host: appservice + web-secondary: + project: ./src/Web + language: csharp + host: appservice + public-api: + project: ./src/PublicApi + language: csharp host: appservice \ No newline at end of file diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep index c65f2b89..85db7fb9 100644 --- a/infra/core/host/appservice.bicep +++ b/infra/core/host/appservice.bicep @@ -1,12 +1,11 @@ param name string param location string = resourceGroup().location param tags object = {} +param enableSlot bool = false +param slotName string = '' // Reference Properties -param applicationInsightsName string = '' param appServicePlanId string -param keyVaultName string = '' -param managedIdentity bool = !empty(keyVaultName) // Runtime Properties @allowed([ @@ -34,6 +33,7 @@ param scmDoBuildDuringDeployment bool = false param use32BitWorkerProcess bool = false param ftpsState string = 'FtpsOnly' param healthCheckPath string = '' +param aspNetCoreEnvironment string = 'Development' resource appService 'Microsoft.Web/sites@2022-03-01' = { name: name @@ -61,17 +61,15 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = { httpsOnly: true } - identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } + identity: { type: 'None' } resource configAppSettings 'config' = { name: 'appsettings' - properties: union(appSettings, - { + properties: union(appSettings, { SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) ENABLE_ORYX_BUILD: string(enableOryxBuild) - }, - !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, - !empty(keyVaultName) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) + ASPNETCORE_ENVIRONMENT: aspNetCoreEnvironment + }) } resource configLogs 'config' = { @@ -88,14 +86,31 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = { } } -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { - name: keyVaultName -} - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!empty(applicationInsightsName)) { - name: applicationInsightsName +// Deployment slot (created only when enabled) +resource appSlot 'Microsoft.Web/sites/slots@2022-03-01' = if (enableSlot) { + parent: appService + name: slotName + location: location + properties: { + serverFarmId: appServicePlanId + siteConfig: { + linuxFxVersion: linuxFxVersion + alwaysOn: alwaysOn + ftpsState: ftpsState + minTlsVersion: '1.2' + appCommandLine: appCommandLine + numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null + minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null + use32BitWorkerProcess: use32BitWorkerProcess + functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null + healthCheckPath: healthCheckPath + } + clientAffinityEnabled: clientAffinityEnabled + httpsOnly: true + } } -output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' +output id string = appService.id output name string = appService.name +output hostName string = appService.properties.defaultHostName output uri string = 'https://${appService.properties.defaultHostName}' diff --git a/infra/core/host/scaleoncpu.bicep b/infra/core/host/scaleoncpu.bicep new file mode 100644 index 00000000..2770df87 --- /dev/null +++ b/infra/core/host/scaleoncpu.bicep @@ -0,0 +1,70 @@ +param name string +param targetResourceUri string +param minimum string = '1' +param maximum string = '2' +param default string = '1' + +param location string = resourceGroup().location +param tags object = {} + +resource scaleOnCpu 'Microsoft.Insights/autoscaleSettings@2015-04-01' = { + name: name + location: location + tags: tags + properties: { + enabled: true + targetResourceUri: targetResourceUri + targetResourceLocation: location + notifications: [] + profiles: [ + { + name: 'Scale on CPU' + capacity: { + minimum: minimum + maximum: maximum + default: default + } + rules: [ + { + metricTrigger: { + metricName: 'CpuPercentage' + metricNamespace: 'microsoft.web/serverfarms' + metricResourceUri: targetResourceUri + operator: 'GreaterThan' + statistic: 'Average' + threshold: 70 + timeAggregation: 'Average' + timeGrain: 'PT1M' + timeWindow: 'PT10M' + } + scaleAction: { + cooldown: 'PT5M' + direction: 'Increase' + type: 'ChangeCount' + value: '1' + } + } + { + metricTrigger: { + metricName: 'CpuPercentage' + metricNamespace: 'microsoft.web/serverfarms' + metricResourceUri: targetResourceUri + operator: 'LessThan' + statistic: 'Average' + threshold: 50 + timeAggregation: 'Average' + timeGrain: 'PT1M' + timeWindow: 'PT10M' + } + scaleAction: { + cooldown: 'PT5M' + direction: 'Decrease' + type: 'ChangeCount' + value: '1' + } + } + ] + } + ] + } +} \ No newline at end of file diff --git a/infra/core/network/trafficManager.bicep b/infra/core/network/trafficManager.bicep new file mode 100644 index 00000000..79942655 --- /dev/null +++ b/infra/core/network/trafficManager.bicep @@ -0,0 +1,55 @@ +targetScope = 'resourceGroup' + +@minLength(1) +param profileName string + +@minLength(1) +param relativeName string + +param tags object = {} + +@minLength(1) +param primaryId string + +@minLength(1) +param secondaryId string + +resource profile 'Microsoft.Network/trafficManagerProfiles@2018-08-01' = { + name: profileName + location: 'global' + tags: tags + properties: { + profileStatus: 'Enabled' + trafficRoutingMethod: 'Performance' + dnsConfig: { + relativeName: relativeName + ttl: 30 + } + monitorConfig: { + protocol: 'HTTP' + port: 80 + path: '/' + } + endpoints: [ + { + name: 'primary-endpoint' + type: 'Microsoft.Network/trafficManagerProfiles/azureEndpoints' + properties: { + targetResourceId: primaryId + endpointStatus: 'Enabled' + } + } + { + name: 'secondary-endpoint' + type: 'Microsoft.Network/trafficManagerProfiles/azureEndpoints' + properties: { + targetResourceId: secondaryId + endpointStatus: 'Enabled' + } + } + ] + } +} + +output relativeName string = profile.properties.dnsConfig.relativeName +output profileName string = profile.name diff --git a/infra/main.bicep b/infra/main.bicep index dda4df5d..f674b803 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -7,123 +7,121 @@ param environmentName string @minLength(1) @description('Primary location for all resources') -param location string +param primaryLocation string = 'spaincentral' + +@minLength(1) +@description('Secondary location for secondary resources') +param secondaryLocation string = 'eastus' // Optional parameters to override the default azd resource naming conventions. Update the main.parameters.json file to provide values. e.g.,: // "resourceGroupName": { // "value": "myGroupName" // } param resourceGroupName string = '' -param webServiceName string = '' -param catalogDatabaseName string = 'catalogDatabase' -param catalogDatabaseServerName string = '' -param identityDatabaseName string = 'identityDatabase' -param identityDatabaseServerName string = '' +param webServiceName string = 'ek-cloudx-associate-shop' param appServicePlanName string = '' -param keyVaultName string = '' - -@description('Id of the user or app to assign application roles') -param principalId string = '' - -@secure() -@description('SQL Server administrator password') -param sqlAdminPassword string - -@secure() -@description('Application user password') -param appUserPassword string +param secondaryAppServicePlanName string = '' +param slotName string = 'test' var abbrs = loadJsonContent('./abbreviations.json') -var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var resourceTokenPrimary = toLower(uniqueString(subscription().id, environmentName, primaryLocation)) +var resourceTokenSecondary = toLower(uniqueString(subscription().id, environmentName, secondaryLocation)) 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 + location: primaryLocation tags: tags } -// The application frontend -module web './core/host/appservice.bicep' = { - name: 'web' +// Public API +module publicApi './core/host/appservice.bicep' = { + name: 'public-api' scope: rg params: { - name: !empty(webServiceName) ? webServiceName : '${abbrs.webSitesAppService}web-${resourceToken}' - location: location - appServicePlanId: appServicePlan.outputs.id - keyVaultName: keyVault.outputs.name + name: !empty(webServiceName) ? '${webServiceName}-api' : '${abbrs.webSitesAppService}web-${resourceTokenPrimary}-api' + location: primaryLocation + appServicePlanId: publicApiPlan.outputs.id runtimeName: 'dotnetcore' runtimeVersion: '9.0' - tags: union(tags, { 'azd-service-name': 'web' }) + tags: union(tags, { 'azd-service-name': 'public-api' }) appSettings: { - AZURE_SQL_CATALOG_CONNECTION_STRING_KEY: 'AZURE-SQL-CATALOG-CONNECTION-STRING' - AZURE_SQL_IDENTITY_CONNECTION_STRING_KEY: 'AZURE-SQL-IDENTITY-CONNECTION-STRING' - AZURE_KEY_VAULT_ENDPOINT: keyVault.outputs.endpoint + BaseUrls__WebBase: 'https://${webServiceName}-${environmentName}.trafficmanager.net' } + allowedOrigins: ['https://${webServiceName}-${environmentName}.trafficmanager.net'] } } -module apiKeyVaultAccess './core/security/keyvault-access.bicep' = { - name: 'api-keyvault-access' +// Public API scale rule +module publicApiAutoscale './core/host/scaleoncpu.bicep' = { + name: 'scale-rule' scope: rg params: { - keyVaultName: keyVault.outputs.name - principalId: web.outputs.identityPrincipalId + name: '${webServiceName}-api-autoscale-rule-${environmentName}' + location: primaryLocation + tags: tags + targetResourceUri: publicApiPlan.outputs.id } } -// The application database: Catalog -module catalogDb './core/database/sqlserver/sqlserver.bicep' = { - name: 'sql-catalog' +// Web (primary) +module webPrimary './core/host/appservice.bicep' = { + name: 'web-primary' scope: rg params: { - name: !empty(catalogDatabaseServerName) ? catalogDatabaseServerName : '${abbrs.sqlServers}catalog-${resourceToken}' - databaseName: catalogDatabaseName - location: location - tags: tags - sqlAdminPassword: sqlAdminPassword - appUserPassword: appUserPassword - keyVaultName: keyVault.outputs.name - connectionStringKey: 'AZURE-SQL-CATALOG-CONNECTION-STRING' + name: !empty(webServiceName) ? '${webServiceName}-${primaryLocation}' : '${abbrs.webSitesAppService}web-${resourceTokenPrimary}' + location: primaryLocation + appServicePlanId: appServicePlanPrimary.outputs.id + runtimeName: 'dotnetcore' + runtimeVersion: '9.0' + tags: union(tags, { 'azd-service-name': 'web-primary' }) + appSettings: { + BaseUrls__ApiBase: '${publicApi.outputs.uri}/api/' + } + enableSlot: true + slotName: slotName } } -// The application database: Identity -module identityDb './core/database/sqlserver/sqlserver.bicep' = { - name: 'sql-identity' +// Web (secondary) +module webSecondary './core/host/appservice.bicep' = { + name: 'web-secondary' scope: rg params: { - name: !empty(identityDatabaseServerName) ? identityDatabaseServerName : '${abbrs.sqlServers}identity-${resourceToken}' - databaseName: identityDatabaseName - location: location - tags: tags - sqlAdminPassword: sqlAdminPassword - appUserPassword: appUserPassword - keyVaultName: keyVault.outputs.name - connectionStringKey: 'AZURE-SQL-IDENTITY-CONNECTION-STRING' + name: !empty(webServiceName) ? '${webServiceName}-${secondaryLocation}' : '${abbrs.webSitesAppService}web-${resourceTokenSecondary}' + location: secondaryLocation + appServicePlanId: appServicePlanSecondary.outputs.id + runtimeName: 'dotnetcore' + runtimeVersion: '9.0' + tags: union(tags, { 'azd-service-name': 'web-secondary' }) + appSettings: { + BaseUrls__ApiBase: '${publicApi.outputs.uri}/api/' + } } } -// Store secrets in a keyvault -module keyVault './core/security/keyvault.bicep' = { - name: 'keyvault' +// Create an App Service Plan to group applications under the same payment plan and SKU (primary) +module appServicePlanPrimary './core/host/appserviceplan.bicep' = { + name: 'appserviceplan-primary' scope: rg params: { - name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' - location: location + name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceTokenPrimary}' + location: primaryLocation tags: tags - principalId: principalId + sku: { + name: 'S1' + } } } -// Create an App Service Plan to group applications under the same payment plan and SKU -module appServicePlan './core/host/appserviceplan.bicep' = { - name: 'appserviceplan' +// Secondary App Service Plan in the secondary location +module appServicePlanSecondary './core/host/appserviceplan.bicep' = { + name: 'appserviceplan-secondary' scope: rg params: { - name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' - location: location + name: !empty(secondaryAppServicePlanName) ? secondaryAppServicePlanName : '${abbrs.webServerFarms}${resourceTokenSecondary}' + location: secondaryLocation tags: tags sku: { name: 'B1' @@ -131,14 +129,31 @@ module appServicePlan './core/host/appserviceplan.bicep' = { } } -// Data outputs -output AZURE_SQL_CATALOG_CONNECTION_STRING_KEY string = catalogDb.outputs.connectionStringKey -output AZURE_SQL_IDENTITY_CONNECTION_STRING_KEY string = identityDb.outputs.connectionStringKey -output AZURE_SQL_CATALOG_DATABASE_NAME string = catalogDb.outputs.databaseName -output AZURE_SQL_IDENTITY_DATABASE_NAME string = identityDb.outputs.databaseName +// Create an App Service Plan to group applications under the same payment plan and SKU (primary) +module publicApiPlan './core/host/appserviceplan.bicep' = { + name: 'publicapiplan' + scope: rg + params: { + name: !empty(appServicePlanName) ? '${appServicePlanName}-app' : '${abbrs.webServerFarms}app-${resourceTokenPrimary}' + location: primaryLocation + tags: tags + sku: { + name: 'S1' + } + } +} + +// Traffic Manager deployed as a module at resource-group scope +module trafficManager './core/network/trafficManager.bicep' = { + name: 'traffic-manager' + scope: rg + params: { + profileName: 'tm-${webServiceName}-${environmentName}' + relativeName: '${webServiceName}-${environmentName}' + tags: tags + primaryId: webPrimary.outputs.id + secondaryId: webSecondary.outputs.id + } +} -// App outputs -output AZURE_LOCATION string = location -output AZURE_TENANT_ID string = tenant().tenantId -output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.endpoint -output AZURE_KEY_VAULT_NAME string = keyVault.outputs.name +output appUrl string = 'http://${trafficManager.outputs.relativeName}.trafficmanager.net' diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 0ef1d971..f6e33631 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -4,18 +4,6 @@ "parameters": { "environmentName": { "value": "${AZURE_ENV_NAME}" - }, - "location": { - "value": "${AZURE_LOCATION}" - }, - "principalId": { - "value": "${AZURE_PRINCIPAL_ID}" - }, - "sqlAdminPassword": { - "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} sqlAdminPassword)" - }, - "appUserPassword": { - "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} appUserPassword)" } } } \ No newline at end of file diff --git a/src/BlazorAdmin/wwwroot/appsettings.Development.json b/src/BlazorAdmin/wwwroot/appsettings.Development.json index cefa244d..f4842018 100644 --- a/src/BlazorAdmin/wwwroot/appsettings.Development.json +++ b/src/BlazorAdmin/wwwroot/appsettings.Development.json @@ -1,6 +1,6 @@ { "baseUrls": { - "apiBase": "https://localhost:5099/api/", + "apiBase": "https://ek-cloudx-associate-shop-api.azurewebsites.net/api/", "webBase": "https://localhost:44315/" }, "Logging": { diff --git a/src/PublicApi/appsettings.json b/src/PublicApi/appsettings.json index 680de210..244bd5bf 100644 --- a/src/PublicApi/appsettings.json +++ b/src/PublicApi/appsettings.json @@ -16,5 +16,6 @@ "System": "Warning" }, "AllowedHosts": "*" - } + }, + "UseOnlyInMemoryDatabase": true } diff --git a/src/Web/appsettings.json b/src/Web/appsettings.json index 9a201d43..c05da598 100644 --- a/src/Web/appsettings.json +++ b/src/Web/appsettings.json @@ -15,5 +15,6 @@ "System": "Warning" }, "AllowedHosts": "*" - } + }, + "UseOnlyInMemoryDatabase": true }