diff --git a/.gitignore b/.gitignore index 1148ecd7a..91a682b0a 100644 --- a/.gitignore +++ b/.gitignore @@ -257,3 +257,6 @@ pub/ #Ignore marker-file used to know which docker files we have. .eshopdocker_* +.devcontainer + +.azure diff --git a/.vscode/extensions.json b/.vscode/extensions.json index d0663c2ca..680470c7e 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,7 @@ "formulahendry.dotnet-test-explorer", "ms-vscode.vscode-node-azure-pack", "ms-kubernetes-tools.vscode-kubernetes-tools", - "redhat.vscode-yaml" + "redhat.vscode-yaml", + "ms-azuretools.azure-dev" ] } \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 6336a5b1d..cfd85c265 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,8 @@ + + diff --git a/README.md b/README.md index 831e0f58a..156b14fbf 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,55 @@ The goal for this sample is to demonstrate some of the principles and patterns d - Development Process for Azure-Hosted ASP.NET Core Apps - Azure Hosting Recommendations for ASP.NET Core Web Apps -## Running the sample +## Running the sample using Azd template The store's home page should look like this: ![eShopOnWeb home page screenshot](https://user-images.githubusercontent.com/782127/88414268-92d83a00-cdaa-11ea-9b4c-db67d95be039.png) +The Azure Developer CLI (`azd`) is a developer-centric command-line interface (CLI) tool for creating Azure applications. + +You need to install it before running and deploying with Azure Developer CLI. + +### Windows + +```powershell +powershell -ex AllSigned -c "Invoke-RestMethod 'https://aka.ms/install-azd.ps1' | Invoke-Expression" +``` + +### Linux/MacOS + +``` +curl -fsSL https://aka.ms/install-azd.sh | bash +``` + +And you can also install with package managers, like winget, choco, and brew. For more details, you can follow the documentation: https://aka.ms/azure-dev/install. + +After logging in with the following command, you will be able to use the azd cli to quickly provision and deploy the application. + +``` +azd auth login +``` + +Then, execute the `azd init` command to initialize the environment. +``` +azd init -t dotnet-architecture/eShopOnWeb +``` + +Run `azd up` to provision all the resources to Azure and deploy the code to those resources. +``` +azd up +``` + +According to the prompt, enter an `env name`, and select `subscription` and `location`, these are the necessary parameters when you create resources. Wait a moment for the resource deployment to complete, click the web endpoint and you will see the home page. + +**Notes:** +1. Considering security, we store its related data (id, password) in the **Azure Key Vault** when we create the database, and obtain it from the Key Vault when we use it. This is different from directly deploying applications locally. +2. The resource group name created in azure portal will be **rg-{env name}**. + +You can also run the sample directly locally (See below). + +## Running the sample locally Most of the site's functionality works with just the web application running. However, the site's Admin page relies on Blazor WebAssembly running in the browser, and it must communicate with the server using the site's PublicApi web application. You'll need to also run this project. You can configure Visual Studio to start multiple projects, or just go to the PublicApi folder in a terminal window and run `dotnet run` from there. After that from the Web folder you should run `dotnet run --launch-profile Web`. Now you should be able to browse to `https://localhost:5001/`. The admin part in Blazor is accessible to `https://localhost:5001/admin` Note that if you use this approach, you'll need to stop the application manually in order to build the solution (otherwise you'll get file locking errors). diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 000000000..c63402c1d --- /dev/null +++ b/azure.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/wbreza/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: eShopOnWeb +services: + web: + project: ./src/Web + language: csharp + host: appservice \ No newline at end of file diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 000000000..a4fc9dfed --- /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-" +} \ No newline at end of file diff --git a/infra/core/database/sqlserver/sqlserver.bicep b/infra/core/database/sqlserver/sqlserver.bicep new file mode 100644 index 000000000..64477a745 --- /dev/null +++ b/infra/core/database/sqlserver/sqlserver.bicep @@ -0,0 +1,129 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param appUser string = 'appUser' +param databaseName string +param keyVaultName string +param sqlAdmin string = 'sqlAdmin' +param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' + +@secure() +param sqlAdminPassword string +@secure() +param appUserPassword string + +resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = { + name: name + location: location + tags: tags + properties: { + version: '12.0' + minimalTlsVersion: '1.2' + publicNetworkAccess: 'Enabled' + administratorLogin: sqlAdmin + administratorLoginPassword: sqlAdminPassword + } + + resource database 'databases' = { + name: databaseName + location: location + } + + resource firewall 'firewallRules' = { + name: 'Azure Services' + properties: { + // Allow all clients + // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only". + // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes. + startIpAddress: '0.0.0.1' + endIpAddress: '255.255.255.254' + } + } +} + +resource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + name: '${name}-deployment-script' + location: location + kind: 'AzureCLI' + properties: { + azCliVersion: '2.37.0' + retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running + timeout: 'PT5M' // Five minutes + cleanupPreference: 'OnSuccess' + environmentVariables: [ + { + name: 'APPUSERNAME' + value: appUser + } + { + name: 'APPUSERPASSWORD' + secureValue: appUserPassword + } + { + name: 'DBNAME' + value: databaseName + } + { + name: 'DBSERVER' + value: sqlServer.properties.fullyQualifiedDomainName + } + { + name: 'SQLCMDPASSWORD' + secureValue: sqlAdminPassword + } + { + name: 'SQLADMIN' + value: sqlAdmin + } + ] + + scriptContent: ''' +wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2 +tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . + +cat < ./initDb.sql +drop user ${APPUSERNAME} +go +create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' +go +alter role db_owner add member ${APPUSERNAME} +go +SCRIPT_END + +./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql + ''' + } +} + +resource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: 'sqlAdminPassword' + properties: { + value: sqlAdminPassword + } +} + +resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: 'appUserPassword' + properties: { + value: appUserPassword + } +} + +resource sqlAzureConnectionStringSercret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: connectionStringKey + properties: { + value: '${connectionString}; Password=${appUserPassword}' + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}' +output connectionStringKey string = connectionStringKey +output databaseName string = sqlServer::database.name diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep new file mode 100644 index 000000000..c65f2b89e --- /dev/null +++ b/infra/core/host/appservice.bicep @@ -0,0 +1,101 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +// Reference Properties +param applicationInsightsName string = '' +param appServicePlanId string +param keyVaultName string = '' +param managedIdentity bool = !empty(keyVaultName) + +// Runtime Properties +@allowed([ + 'dotnet', 'dotnetcore', 'dotnet-isolated', 'node', 'python', 'java', 'powershell', 'custom' +]) +param runtimeName string +param runtimeNameAndVersion string = '${runtimeName}|${runtimeVersion}' +param runtimeVersion string + +// Microsoft.Web/sites Properties +param kind string = 'app,linux' + +// Microsoft.Web/sites/config +param allowedOrigins array = [] +param alwaysOn bool = true +param appCommandLine string = '' +param appSettings object = {} +param clientAffinityEnabled bool = false +param enableOryxBuild bool = contains(kind, 'linux') +param functionAppScaleLimit int = -1 +param linuxFxVersion string = runtimeNameAndVersion +param minimumElasticInstanceCount int = -1 +param numberOfWorkers int = -1 +param scmDoBuildDuringDeployment bool = false +param use32BitWorkerProcess bool = false +param ftpsState string = 'FtpsOnly' +param healthCheckPath string = '' + +resource appService 'Microsoft.Web/sites@2022-03-01' = { + name: name + location: location + tags: tags + kind: kind + 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 + cors: { + allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) + } + } + clientAffinityEnabled: clientAffinityEnabled + httpsOnly: true + } + + identity: { type: managedIdentity ? 'SystemAssigned' : 'None' } + + resource configAppSettings 'config' = { + name: '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 } : {}) + } + + resource configLogs 'config' = { + name: 'logs' + properties: { + applicationLogs: { fileSystem: { level: 'Verbose' } } + detailedErrorMessages: { enabled: true } + failedRequestsTracing: { enabled: true } + httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } + } + dependsOn: [ + configAppSettings + ] + } +} + +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 +} + +output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' +output name string = appService.name +output uri string = 'https://${appService.properties.defaultHostName}' diff --git a/infra/core/host/appserviceplan.bicep b/infra/core/host/appserviceplan.bicep new file mode 100644 index 000000000..69c35d78e --- /dev/null +++ b/infra/core/host/appserviceplan.bicep @@ -0,0 +1,20 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param kind string = '' +param reserved bool = true +param sku object + +resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { + name: name + location: location + tags: tags + sku: sku + kind: kind + properties: { + reserved: reserved + } +} + +output id string = appServicePlan.id diff --git a/infra/core/security/keyvault-access.bicep b/infra/core/security/keyvault-access.bicep new file mode 100644 index 000000000..aa989ebdc --- /dev/null +++ b/infra/core/security/keyvault-access.bicep @@ -0,0 +1,21 @@ +param name string = 'add' + +param keyVaultName string +param permissions object = { secrets: [ 'get', 'list' ] } +param principalId string + +resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { + parent: keyVault + name: name + properties: { + accessPolicies: [ { + objectId: principalId + tenantId: subscription().tenantId + permissions: permissions + } ] + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} diff --git a/infra/core/security/keyvault.bicep b/infra/core/security/keyvault.bicep new file mode 100644 index 000000000..0eb4a86db --- /dev/null +++ b/infra/core/security/keyvault.bicep @@ -0,0 +1,25 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +param principalId string = '' + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: name + location: location + tags: tags + properties: { + tenantId: subscription().tenantId + sku: { family: 'A', name: 'standard' } + accessPolicies: !empty(principalId) ? [ + { + objectId: principalId + permissions: { secrets: [ 'get', 'list' ] } + tenantId: subscription().tenantId + } + ] : [] + } +} + +output endpoint string = keyVault.properties.vaultUri +output name string = keyVault.name diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 000000000..f1187e74a --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,144 @@ +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 + +// 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 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 + +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 +} + +// The application frontend +module web './core/host/appservice.bicep' = { + name: 'web' + scope: rg + params: { + name: !empty(webServiceName) ? webServiceName : '${abbrs.webSitesAppService}web-${resourceToken}' + location: location + appServicePlanId: appServicePlan.outputs.id + keyVaultName: keyVault.outputs.name + runtimeName: 'dotnetcore' + runtimeVersion: '7.0' + tags: union(tags, { 'azd-service-name': 'web' }) + 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 + } + } +} + +module apiKeyVaultAccess './core/security/keyvault-access.bicep' = { + name: 'api-keyvault-access' + scope: rg + params: { + keyVaultName: keyVault.outputs.name + principalId: web.outputs.identityPrincipalId + } +} + +// The application database: Catalog +module catalogDb './core/database/sqlserver/sqlserver.bicep' = { + name: 'sql-catalog' + 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' + } +} + +// The application database: Identity +module identityDb './core/database/sqlserver/sqlserver.bicep' = { + name: 'sql-identity' + 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' + } +} + +// Store secrets in a keyvault +module keyVault './core/security/keyvault.bicep' = { + name: 'keyvault' + scope: rg + params: { + name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' + location: location + tags: tags + principalId: principalId + } +} + +// Create an App Service Plan to group applications under the same payment plan and SKU +module appServicePlan './core/host/appserviceplan.bicep' = { + name: 'appserviceplan' + scope: rg + params: { + name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}' + location: location + tags: tags + sku: { + name: 'B1' + } + } +} + +// 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 + +// 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 diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 000000000..0ef1d9715 --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,21 @@ +{ + "$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}" + }, + "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/Web/Program.cs b/src/Web/Program.cs index c5dbcc1e1..af23e7f53 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -1,5 +1,6 @@ using System.Net.Mime; using Ardalis.ListStartupServices; +using Azure.Identity; using BlazorAdmin; using BlazorAdmin.Services; using Blazored.LocalStorage; @@ -8,6 +9,7 @@ using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.EntityFrameworkCore; using Microsoft.eShopWeb; using Microsoft.eShopWeb.ApplicationCore.Interfaces; using Microsoft.eShopWeb.Infrastructure.Data; @@ -19,9 +21,31 @@ var builder = WebApplication.CreateBuilder(args); +var detectEnvBuilder = WebApplication.CreateBuilder(args); + builder.Logging.AddConsole(); -Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services); +var detectEnvApp = detectEnvBuilder.Build(); + +if (detectEnvApp.Environment.IsDevelopment() || detectEnvApp.Environment.EnvironmentName == "Docker"){ + // Configure SQL Server (local) + Microsoft.eShopWeb.Infrastructure.Dependencies.ConfigureServices(builder.Configuration, builder.Services); +} +else{ + // Configure SQL Server (prod) + var credential = new ChainedTokenCredential(new AzureDeveloperCliCredential(), new DefaultAzureCredential()); + builder.Configuration.AddAzureKeyVault(new Uri(builder.Configuration["AZURE_KEY_VAULT_ENDPOINT"] ?? ""), credential); + builder.Services.AddDbContext(c => + { + var connectionString = builder.Configuration[builder.Configuration["AZURE_SQL_CATALOG_CONNECTION_STRING_KEY"] ?? ""]; + c.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure()); + }); + builder.Services.AddDbContext(options => + { + var connectionString = builder.Configuration[builder.Configuration["AZURE_SQL_IDENTITY_CONNECTION_STRING_KEY"] ?? ""]; + options.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure()); + }); +} builder.Services.AddCookieSettings(); diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index fdb65743b..6ed273bfa 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -16,6 +16,8 @@ + +