From 159bad87d1b27c4d2b5d861bdf78cc6c634f4a0c Mon Sep 17 00:00:00 2001 From: Yunge Zhu Date: Wed, 1 Feb 2023 15:56:02 +0800 Subject: [PATCH 1/3] add serviceconnector --- infra/app/api.bicep | 11 ++++ infra/app/db.bicep | 1 + infra/core/database/sqlserver/sqlserver.bicep | 10 +-- infra/core/host/appservice.bicep | 65 ++++++++++++++++++- infra/main.bicep | 15 ++--- 5 files changed, 81 insertions(+), 21 deletions(-) diff --git a/infra/app/api.bicep b/infra/app/api.bicep index 55954f1e..213c1faa 100644 --- a/infra/app/api.bicep +++ b/infra/app/api.bicep @@ -10,6 +10,13 @@ param appSettings object = {} param keyVaultName string param serviceName string = 'api' +// Target DB properties +param connectionStringKey string = '' +param targetResourceId string = '' +param appUser string = '' +@secure() +param appUserPassword string + module api '../core/host/appservice.bicep' = { name: '${name}-app-module' params: { @@ -25,6 +32,10 @@ module api '../core/host/appservice.bicep' = { runtimeName: 'dotnetcore' runtimeVersion: '6.0' scmDoBuildDuringDeployment: false + targetResourceId: targetResourceId + appUser: appUser + appUserPassword: appUserPassword + connectionStringKey: connectionStringKey } } diff --git a/infra/app/db.bicep b/infra/app/db.bicep index a21cc0ff..add48c21 100644 --- a/infra/app/db.bicep +++ b/infra/app/db.bicep @@ -29,3 +29,4 @@ module sqlServer '../core/database/sqlserver/sqlserver.bicep' = { output connectionStringKey string = sqlServer.outputs.connectionStringKey output databaseName string = sqlServer.outputs.databaseName +output id string = sqlServer.outputs.id diff --git a/infra/core/database/sqlserver/sqlserver.bicep b/infra/core/database/sqlserver/sqlserver.bicep index 821a9087..6dff5025 100644 --- a/infra/core/database/sqlserver/sqlserver.bicep +++ b/infra/core/database/sqlserver/sqlserver.bicep @@ -112,18 +112,10 @@ resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = } } -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 +output id string = sqlServer.id diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep index 62e34a65..26d3ea7f 100644 --- a/infra/core/host/appservice.bicep +++ b/infra/core/host/appservice.bicep @@ -33,6 +33,13 @@ param numberOfWorkers int = -1 param scmDoBuildDuringDeployment bool = false param use32BitWorkerProcess bool = false +// Target DB properties +param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' +param targetResourceId string = '' +param appUser string = '' +@secure() +param appUserPassword string = '' + resource appService 'Microsoft.Web/sites@2022-03-01' = { name: name location: location @@ -66,8 +73,7 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = { 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 } : {}) + !empty(applicationInsightsName) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}) } resource configLogs 'config' = { @@ -92,6 +98,61 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing name: applicationInsightsName } +// if !empty(keyVaultName), create connection to keyvault so that db credentials could be saved into keyvault, +// and app service could retrieve secrets from keyvault using managed identity +resource connectionToKeyVault 'Microsoft.ServiceLinker/linkers@2022-11-01-preview' = if(!empty(keyVaultName)) { + name: 'conn_kv' + scope: appService + properties: { + targetService: { + id: keyVault.id + type: 'AzureResource' + } + clientType: 'none' + authInfo: { + authType: 'systemAssignedIdentity' + roles: [ + '4633458b-17de-408a-b874-0445c86b69e6' + ] + } + configurationInfo: { + customizedKeys: { + 'AZURE_KEYVAULT_RESOURCEENDPOINT': 'AZURE_KEY_VAULT_ENDPOINT' + } + } + } +} + +// if !empty(targetResourceId), create connection to target database, including: +// - add db connectionstr (from keyvault if applicable) in webapp appsettings or connectionString(for dotnetcore convention) +// - allow webapp firewall at target database if applicable (target allows firewall instead of public access) +resource connectionToTargetDB 'Microsoft.ServiceLinker/linkers@2022-11-01-preview' = if (!empty(targetResourceId)) { + name: 'conn_db' + scope: appService + properties: { + targetService: { + id: targetResourceId + type: 'AzureResource' + } + secretStore: { + keyVaultId: !empty(keyVaultName) ? keyVault.id : '' + keyVaultSecretName: !empty(keyVaultName) ? connectionStringKey : '' + } + authInfo: { + authType: 'secret' + name: appUser + secretInfo: { + secretType: 'rawValue' + value: appUserPassword + } + } + clientType: 'dotnet' + } + dependsOn: [ + connectionToKeyVault + ] +} + output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' output name string = appService.name output uri string = 'https://${appService.properties.defaultHostName}' diff --git a/infra/main.bicep b/infra/main.bicep index f78e41dd..8e154b6f 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -24,6 +24,7 @@ param sqlServerName string = '' param sqlDatabaseName string = '' param webServiceName string = '' param apimServiceName string = '' +param appUser string = 'appUser' @description('Flag to use Azure API Management to mediate the calls between the Web frontend and the backend API') param useAPIM bool = false @@ -78,16 +79,10 @@ module api './app/api.bicep' = { appSettings: { AZURE_SQL_CONNECTION_STRING_KEY: sqlServer.outputs.connectionStringKey } - } -} - -// Give the API access to KeyVault -module apiKeyVaultAccess './core/security/keyvault-access.bicep' = { - name: 'api-keyvault-access' - scope: rg - params: { - keyVaultName: keyVault.outputs.name - principalId: api.outputs.SERVICE_API_IDENTITY_PRINCIPAL_ID + targetResourceId: '${sqlServer.outputs.id}/databases/${sqlServer.outputs.databaseName}' + appUser: appUser + appUserPassword: appUserPassword + connectionStringKey: sqlServer.outputs.connectionStringKey } } From b34ca02029f8130d64e89f1eba9d674e6dbe7565 Mon Sep 17 00:00:00 2001 From: Yunge Zhu Date: Wed, 1 Feb 2023 16:33:34 +0800 Subject: [PATCH 2/3] keyvault reference --- infra/app/db.bicep | 1 - infra/core/database/sqlserver/sqlserver.bicep | 2 -- infra/main.bicep | 7 +------ src/api/Program.cs | 12 +++++++++--- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/infra/app/db.bicep b/infra/app/db.bicep index add48c21..2c3c5c4d 100644 --- a/infra/app/db.bicep +++ b/infra/app/db.bicep @@ -27,6 +27,5 @@ module sqlServer '../core/database/sqlserver/sqlserver.bicep' = { } } -output connectionStringKey string = sqlServer.outputs.connectionStringKey output databaseName string = sqlServer.outputs.databaseName output id string = sqlServer.outputs.id diff --git a/infra/core/database/sqlserver/sqlserver.bicep b/infra/core/database/sqlserver/sqlserver.bicep index 6dff5025..1820638c 100644 --- a/infra/core/database/sqlserver/sqlserver.bicep +++ b/infra/core/database/sqlserver/sqlserver.bicep @@ -6,7 +6,6 @@ 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 @@ -116,6 +115,5 @@ resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVaultName } -output connectionStringKey string = connectionStringKey output databaseName string = sqlServer::database.name output id string = sqlServer.id diff --git a/infra/main.bicep b/infra/main.bicep index 8e154b6f..2493758e 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -76,13 +76,10 @@ module api './app/api.bicep' = { appServicePlanId: appServicePlan.outputs.id keyVaultName: keyVault.outputs.name allowedOrigins: [ web.outputs.SERVICE_WEB_URI ] - appSettings: { - AZURE_SQL_CONNECTION_STRING_KEY: sqlServer.outputs.connectionStringKey - } targetResourceId: '${sqlServer.outputs.id}/databases/${sqlServer.outputs.databaseName}' appUser: appUser appUserPassword: appUserPassword - connectionStringKey: sqlServer.outputs.connectionStringKey + connectionStringKey: 'AZURE-SQL-CONNECTION-STRING' } } @@ -167,8 +164,6 @@ module apimApi './app/apim-api.bicep' = if (useAPIM) { } } -// Data outputs -output AZURE_SQL_CONNECTION_STRING_KEY string = sqlServer.outputs.connectionStringKey // App outputs output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString diff --git a/src/api/Program.cs b/src/api/Program.cs index abace678..a853b6bb 100644 --- a/src/api/Program.cs +++ b/src/api/Program.cs @@ -3,13 +3,19 @@ using SimpleTodo.Api; var builder = WebApplication.CreateBuilder(args); -var credential = new ChainedTokenCredential(new AzureDeveloperCliCredential(), new DefaultAzureCredential()); -builder.Configuration.AddAzureKeyVault(new Uri(builder.Configuration["AZURE_KEY_VAULT_ENDPOINT"]), credential); +// if non-development node, get target connection string from runtime directly, because Azure webapp resolve keyvault reference already. +var connectionString = builder.Configuration["AZURE_SQL_CONNECTIONSTRING"]; +// if development node, get target connection string from keyvault config provider. +if (builder.Environment.IsDevelopment()) +{ + var credential = new ChainedTokenCredential(new AzureDeveloperCliCredential(), new DefaultAzureCredential()); + builder.Configuration.AddAzureKeyVault(new Uri(builder.Configuration["AZURE_KEY_VAULT_ENDPOINT"]), credential); + connectionString = builder.Configuration["AZURE-SQL-CONNECTION-STRING"]; +} builder.Services.AddScoped(); builder.Services.AddDbContext(options => { - var connectionString = builder.Configuration[builder.Configuration["AZURE_SQL_CONNECTION_STRING_KEY"]]; options.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure()); }); From 80366dda1f80c1e014d4e5985020641c05e2f59c Mon Sep 17 00:00:00 2001 From: Yunge Zhu Date: Thu, 23 Mar 2023 12:49:52 +0800 Subject: [PATCH 3/3] resolve comments --- infra/app/api.bicep | 11 --- infra/app/connection.bicep | 28 +++++++ infra/app/db.bicep | 1 + infra/core/database/sqlserver/sqlserver.bicep | 2 + infra/core/host/appservice.bicep | 80 +++++-------------- infra/core/host/servicelinker.bicep | 80 +++++++++++++++++++ infra/main.bicep | 33 +++++++- 7 files changed, 158 insertions(+), 77 deletions(-) create mode 100644 infra/app/connection.bicep create mode 100644 infra/core/host/servicelinker.bicep diff --git a/infra/app/api.bicep b/infra/app/api.bicep index 213c1faa..55954f1e 100644 --- a/infra/app/api.bicep +++ b/infra/app/api.bicep @@ -10,13 +10,6 @@ param appSettings object = {} param keyVaultName string param serviceName string = 'api' -// Target DB properties -param connectionStringKey string = '' -param targetResourceId string = '' -param appUser string = '' -@secure() -param appUserPassword string - module api '../core/host/appservice.bicep' = { name: '${name}-app-module' params: { @@ -32,10 +25,6 @@ module api '../core/host/appservice.bicep' = { runtimeName: 'dotnetcore' runtimeVersion: '6.0' scmDoBuildDuringDeployment: false - targetResourceId: targetResourceId - appUser: appUser - appUserPassword: appUserPassword - connectionStringKey: connectionStringKey } } diff --git a/infra/app/connection.bicep b/infra/app/connection.bicep new file mode 100644 index 00000000..15d40b9a --- /dev/null +++ b/infra/app/connection.bicep @@ -0,0 +1,28 @@ +param authType string +//param appResourceId string +param targetResourceId string +param runtimeName string +param dbUserName string = '' +param keyVaultName string +param webAppName string +param connectionStringKey string = '' + +@secure() +param dbUserPassword string + +var resourcePrefix = uniqueString(webAppName) + +module connections '../core/host/servicelinker.bicep' = { + name: '${resourcePrefix}conns' + params: { + authType: authType + //appResourceId: appResourceId + targetResourceId: targetResourceId + runtimeName: runtimeName + dbUserName: dbUserName + dbUserPassword: dbUserPassword + keyVaultName: keyVaultName + webAppName: webAppName + connectionStringKey: connectionStringKey + } +} diff --git a/infra/app/db.bicep b/infra/app/db.bicep index 2c3c5c4d..add48c21 100644 --- a/infra/app/db.bicep +++ b/infra/app/db.bicep @@ -27,5 +27,6 @@ module sqlServer '../core/database/sqlserver/sqlserver.bicep' = { } } +output connectionStringKey string = sqlServer.outputs.connectionStringKey output databaseName string = sqlServer.outputs.databaseName output id string = sqlServer.outputs.id diff --git a/infra/core/database/sqlserver/sqlserver.bicep b/infra/core/database/sqlserver/sqlserver.bicep index 1820638c..6dff5025 100644 --- a/infra/core/database/sqlserver/sqlserver.bicep +++ b/infra/core/database/sqlserver/sqlserver.bicep @@ -6,6 +6,7 @@ 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 @@ -115,5 +116,6 @@ resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVaultName } +output connectionStringKey string = connectionStringKey output databaseName string = sqlServer::database.name output id string = sqlServer.id diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep index 26d3ea7f..749fb844 100644 --- a/infra/core/host/appservice.bicep +++ b/infra/core/host/appservice.bicep @@ -33,12 +33,6 @@ param numberOfWorkers int = -1 param scmDoBuildDuringDeployment bool = false param use32BitWorkerProcess bool = false -// Target DB properties -param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' -param targetResourceId string = '' -param appUser string = '' -@secure() -param appUserPassword string = '' resource appService 'Microsoft.Web/sites@2022-03-01' = { name: name @@ -90,68 +84,30 @@ resource appService 'Microsoft.Web/sites@2022-03-01' = { } } -resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { - name: keyVaultName -} +// 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 } -// if !empty(keyVaultName), create connection to keyvault so that db credentials could be saved into keyvault, -// and app service could retrieve secrets from keyvault using managed identity -resource connectionToKeyVault 'Microsoft.ServiceLinker/linkers@2022-11-01-preview' = if(!empty(keyVaultName)) { - name: 'conn_kv' - scope: appService - properties: { - targetService: { - id: keyVault.id - type: 'AzureResource' - } - clientType: 'none' - authInfo: { - authType: 'systemAssignedIdentity' - roles: [ - '4633458b-17de-408a-b874-0445c86b69e6' - ] - } - configurationInfo: { - customizedKeys: { - 'AZURE_KEYVAULT_RESOURCEENDPOINT': 'AZURE_KEY_VAULT_ENDPOINT' - } - } - } -} +// resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { +// parent: keyVault +// name: 'appServiceUserPassword' +// properties: { +// value: appUserPassword +// } +// } -// if !empty(targetResourceId), create connection to target database, including: -// - add db connectionstr (from keyvault if applicable) in webapp appsettings or connectionString(for dotnetcore convention) -// - allow webapp firewall at target database if applicable (target allows firewall instead of public access) -resource connectionToTargetDB 'Microsoft.ServiceLinker/linkers@2022-11-01-preview' = if (!empty(targetResourceId)) { - name: 'conn_db' - scope: appService - properties: { - targetService: { - id: targetResourceId - type: 'AzureResource' - } - secretStore: { - keyVaultId: !empty(keyVaultName) ? keyVault.id : '' - keyVaultSecretName: !empty(keyVaultName) ? connectionStringKey : '' - } - authInfo: { - authType: 'secret' - name: appUser - secretInfo: { - secretType: 'rawValue' - value: appUserPassword - } - } - clientType: 'dotnet' - } - dependsOn: [ - connectionToKeyVault - ] -} +//resource appUserdSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { +// parent: keyVault +// name: 'appServiceUser' +// properties: { +// value: dbUserName +// } +//} output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' output name string = appService.name diff --git a/infra/core/host/servicelinker.bicep b/infra/core/host/servicelinker.bicep new file mode 100644 index 00000000..ac253648 --- /dev/null +++ b/infra/core/host/servicelinker.bicep @@ -0,0 +1,80 @@ +param authType string +//param appResourceId string +param targetResourceId string +param runtimeName string +param dbUserName string = '' +param keyVaultName string +param webAppName string +param connectionStringKey string ='' + +@secure() +param dbUserPassword string = '' + + +// if !empty(keyVaultName), create connection to keyvault so that db credentials could be saved into keyvault, +// and app service could retrieve secrets from keyvault using managed identity +resource connectionToKeyVault 'Microsoft.ServiceLinker/linkers@2022-11-01-preview' = if(!empty(keyVaultName)) { + name: 'conn_kv' + scope: webApp + properties: { + targetService: { + id: keyVault.id + type: 'AzureResource' + } + clientType: 'none' + authInfo: { + authType: 'systemAssignedIdentity' + roles: [ + '4633458b-17de-408a-b874-0445c86b69e6' + ] + } + configurationInfo: { + customizedKeys: { + 'AZURE_KEYVAULT_RESOURCEENDPOINT': 'AZURE_KEY_VAULT_ENDPOINT' + } + } + } + dependsOn: [ + webApp + ] +} + +// if !empty(targetResourceId), create connection to target database, including: +// - add db connectionstr (from keyvault if applicable) in webapp appsettings or connectionString(for dotnetcore convention) +// - allow webapp firewall at target database if applicable (target allows firewall instead of public access) +resource connectionToTargetDB 'Microsoft.ServiceLinker/linkers@2022-11-01-preview' = if (!empty(targetResourceId)) { + name: 'conn_db' + scope: webApp + properties: { + targetService: { + id: targetResourceId + type: 'AzureResource' + } + secretStore: { + keyVaultId: !empty(keyVaultName) ? keyVault.id : '' + keyVaultSecretName: !empty(keyVaultName) ? connectionStringKey : '' + } + authInfo: { + authType: authType + name: dbUserName + secretInfo: { + secretType: 'rawValue' + value: dbUserPassword + } + } + clientType: runtimeName + } + dependsOn: [ + connectionToKeyVault + ] +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!empty(keyVaultName)) { + name: keyVaultName + scope: resourceGroup() +} + +resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { + name: webAppName + scope: resourceGroup() +} diff --git a/infra/main.bicep b/infra/main.bicep index 2493758e..467b9be6 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -76,10 +76,6 @@ module api './app/api.bicep' = { appServicePlanId: appServicePlan.outputs.id keyVaultName: keyVault.outputs.name allowedOrigins: [ web.outputs.SERVICE_WEB_URI ] - targetResourceId: '${sqlServer.outputs.id}/databases/${sqlServer.outputs.databaseName}' - appUser: appUser - appUserPassword: appUserPassword - connectionStringKey: 'AZURE-SQL-CONNECTION-STRING' } } @@ -124,6 +120,33 @@ module keyVault './core/security/keyvault.bicep' = { } } +// creation connections +// 1. if !empty(keyVaultName), create connection to keyvault so that db credentials could be saved into keyvault, +// and app service could retrieve secrets from keyvault +// 2. create connection to target database +module connections './app/connection.bicep' = { + name: 'conn${resourceToken}' + scope: rg + //scope: apiWebApp + //scope: api.outputs.SERVICE_API_NAME + params: { + authType: 'secret' + //appResourceId: api.outputs.api.id + targetResourceId: '${sqlServer.outputs.id}/databases/${sqlServer.outputs.databaseName}' + runtimeName: 'dotnet' + dbUserName: appUser + dbUserPassword: appUserPassword + keyVaultName: keyVault.outputs.name + webAppName: api.outputs.SERVICE_API_NAME + connectionStringKey: 'AZURE-SQL-CONNECTION-STRING' + } +} + +// resource apiWebApp 'Microsoft.Web/sites@2022-03-01' existing = { +// name: api.outputs.SERVICE_API_NAME +// scope: rg +// } + // Monitor application with Azure Monitor module monitoring './core/monitor/monitoring.bicep' = { name: 'monitoring' @@ -164,6 +187,8 @@ module apimApi './app/apim-api.bicep' = if (useAPIM) { } } +// Data outputs +output AZURE_SQL_CONNECTION_STRING_KEY string = sqlServer.outputs.connectionStringKey // App outputs output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString