diff --git a/infra/shared/terraform/modules/app-service/main.tf b/infra/shared/terraform/modules/app-service/main.tf index abb0f96b..b6ae0647 100644 --- a/infra/shared/terraform/modules/app-service/main.tf +++ b/infra/shared/terraform/modules/app-service/main.tf @@ -48,7 +48,8 @@ resource "azurerm_linux_web_app" "application" { virtual_network_subnet_id = var.appsvc_subnet_id identity { - type = "SystemAssigned" + type = var.identity.type + identity_ids = var.identity.type == "SystemAssigned" ? [] : var.identity.identity_ids } tags = { @@ -107,9 +108,9 @@ resource "azurerm_linux_web_app" "application" { SPRING_CLOUD_AZURE_ACTIVE_DIRECTORY_CREDENTIAL_CLIENT_SECRET = var.contoso_webapp_options.contoso_active_directory_client_secret SPRING_CLOUD_AZURE_ACTIVE_DIRECTORY_PROFILE_TENANT_ID = var.contoso_webapp_options.contoso_active_directory_tenant_id - SPRING_DATA_REDIS_HOST = var.contoso_webapp_options.redis_host_name - SPRING_DATA_REDIS_PORT = var.contoso_webapp_options.redis_port - SPRING_DATA_REDIS_PASSWORD = var.contoso_webapp_options.redis_password + AZURE_CACHE_REDIS_HOST = var.contoso_webapp_options.redis_host_name + AZURE_CACHE_REDIS_PORT = var.contoso_webapp_options.redis_port + AZURE_CACHE_REDIS_CLIENT_ID = var.contoso_webapp_options.redis_user_client_id CONTOSO_RETRY_DEMO = "0" } diff --git a/infra/shared/terraform/modules/app-service/variables.tf b/infra/shared/terraform/modules/app-service/variables.tf index a9789967..292c16db 100644 --- a/infra/shared/terraform/modules/app-service/variables.tf +++ b/infra/shared/terraform/modules/app-service/variables.tf @@ -59,6 +59,25 @@ variable "public_network_access_enabled" { description = "Should public network access be enabled for the Web App." } +variable "identity" { + type = object({ + type = string + identity_ids = optional(list(string)) + }) + + description = "The identity type and the list of identities ids" + + default = { + type = "SystemAssigned" + identity_ids = [] + } + + validation { + condition = contains(["SystemAssigned", "UserAssigned", "SystemAssigned, UserAssigned"], var.identity.type) + error_message = "Please, choose among one of the following identity types: SystemAssigned, UserAssigned or SystemAssigned, UserAssigned." + } +} + variable "contoso_webapp_options" { type = object({ contoso_active_directory_tenant_id = string @@ -71,7 +90,7 @@ variable "contoso_webapp_options" { redis_host_name = string redis_port = number - redis_password = string + redis_user_client_id = string }) description = "The options for the webapp" diff --git a/infra/shared/terraform/modules/cache/main.tf b/infra/shared/terraform/modules/cache/main.tf index 984c4060..353127c5 100644 --- a/infra/shared/terraform/modules/cache/main.tf +++ b/infra/shared/terraform/modules/cache/main.tf @@ -24,8 +24,9 @@ resource "azurerm_redis_cache" "cache" { # public network access will be allowed for non-prod so devs can do integration testing while debugging locally public_network_access_enabled = var.environment == "prod" ? false : true - # https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-configure#default-redis-server-configuration redis_configuration { + enable_authentication = true + active_directory_authentication_enabled = true } } diff --git a/infra/shared/terraform/modules/cache/outputs.tf b/infra/shared/terraform/modules/cache/outputs.tf index acfb994d..96231e29 100644 --- a/infra/shared/terraform/modules/cache/outputs.tf +++ b/infra/shared/terraform/modules/cache/outputs.tf @@ -1,6 +1,6 @@ -output "cache_secret" { - value = azurerm_redis_cache.cache.primary_access_key - description = "The secret to use when connecting to Azure Cache for Redis" +output "cache_id" { + value = azurerm_redis_cache.cache.id + description = "The id of the Azure Cache for Redis" } output "cache_hostname" { diff --git a/infra/terraform/application.tf b/infra/terraform/application.tf index 55c9d9d0..5f97c18f 100644 --- a/infra/terraform/application.tf +++ b/infra/terraform/application.tf @@ -22,16 +22,23 @@ module "application" { frontdoor_profile_uuid = module.frontdoor[0].resource_guid public_network_access_enabled = false + identity = { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.primary_app_service_identity[0].id + ] + } + contoso_webapp_options = { - contoso_active_directory_tenant_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_tenant_id[0].id})" - contoso_active_directory_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_id[0].id})" - contoso_active_directory_client_secret = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_secret[0].id})" - postgresql_database_url = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_url[0].id})" - postgresql_database_user = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin[0].id})" - postgresql_database_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin_password[0].id})" - redis_host_name = module.cache[0].cache_hostname - redis_port = module.cache[0].cache_ssl_port - redis_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_cache_secret[0].id})" + contoso_active_directory_tenant_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_tenant_id[0].id})" + contoso_active_directory_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_id[0].id})" + contoso_active_directory_client_secret = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_secret[0].id})" + postgresql_database_url = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_url[0].id})" + postgresql_database_user = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin[0].id})" + postgresql_database_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin_password[0].id})" + redis_host_name = module.cache[0].cache_hostname + redis_port = module.cache[0].cache_ssl_port + redis_user_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.primary_redis_user_secret[0].id})" } } @@ -48,23 +55,30 @@ module "secondary_application" { location = var.secondary_location private_dns_resource_group = azurerm_resource_group.hub[0].name appsvc_subnet_id = module.secondary_spoke_vnet[0].subnets[local.app_service_subnet_name].id - private_endpoint_subnet_id = module.secondary_spoke_vnet[0].subnets[local.private_link_subnet_name].id + private_endpoint_subnet_id = module.secondary_spoke_vnet[0].subnets[local.private_link_subnet_name].id app_insights_connection_string = module.hub_app_insights[0].connection_string log_analytics_workspace_id = module.hub_app_insights[0].log_analytics_workspace_id frontdoor_host_name = module.frontdoor[0].host_name frontdoor_profile_uuid = module.frontdoor[0].resource_guid public_network_access_enabled = false + identity = { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.secondary_app_service_identity[0].id + ] + } + contoso_webapp_options = { - contoso_active_directory_tenant_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_tenant_id[0].id})" - contoso_active_directory_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_id[0].id})" - contoso_active_directory_client_secret = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_secret[0].id})" - postgresql_database_url = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.secondary_contoso_database_url[0].id})" - postgresql_database_user = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin[0].id})" - postgresql_database_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin_password[0].id})" - redis_host_name = module.secondary_cache[0].cache_hostname - redis_port = module.secondary_cache[0].cache_ssl_port - redis_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_cache_secret[0].id})" + contoso_active_directory_tenant_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_tenant_id[0].id})" + contoso_active_directory_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_id[0].id})" + contoso_active_directory_client_secret = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_application_client_secret[0].id})" + postgresql_database_url = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.secondary_contoso_database_url[0].id})" + postgresql_database_user = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin[0].id})" + postgresql_database_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.contoso_database_admin_password[0].id})" + redis_host_name = module.secondary_cache[0].cache_hostname + redis_port = module.secondary_cache[0].cache_ssl_port + redis_user_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.secondary_redis_user_secret[0].id})" } } @@ -92,6 +106,13 @@ module "dev_application" { frontdoor_profile_uuid = module.dev_frontdoor[0].resource_guid public_network_access_enabled = true + identity = { + type = "SystemAssigned, UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.dev_app_service_identity[0].id + ] + } + contoso_webapp_options = { contoso_active_directory_tenant_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.dev_contoso_application_tenant_id[0].id})" contoso_active_directory_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.dev_contoso_application_client_id[0].id})" @@ -101,6 +122,6 @@ module "dev_application" { postgresql_database_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.dev_contoso_database_admin_password[0].id})" redis_host_name = module.dev-cache[0].cache_hostname redis_port = module.dev-cache[0].cache_ssl_port - redis_password = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.dev_contoso_cache_secret[0].id})" + redis_user_client_id = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.dev_redis_user_secret[0].id})" } } diff --git a/infra/terraform/cache.tf b/infra/terraform/cache.tf index b92723fb..72704a17 100644 --- a/infra/terraform/cache.tf +++ b/infra/terraform/cache.tf @@ -14,6 +14,30 @@ module "cache" { log_analytics_workspace_id = module.hub_app_insights[0].log_analytics_workspace_id } + +resource "azurerm_redis_cache_access_policy_assignment" "primary_current_user" { + count = var.environment == "prod" ? 1 : 0 + name = "primarycurrentuser" + redis_cache_id = module.cache[0].cache_id + access_policy_name = "Data Contributor" + object_id = data.azuread_client_config.current.object_id + object_id_alias = "currentuser" +} + +resource "azurerm_redis_cache_access_policy_assignment" "app_user" { + count = var.environment == "prod" ? 1 : 0 + name = "primaryappuser" + redis_cache_id = module.cache[0].cache_id + access_policy_name = "Data Contributor" + object_id = azurerm_user_assigned_identity.primary_app_service_identity[0].principal_id + object_id_alias = azurerm_user_assigned_identity.primary_app_service_identity[0].principal_id + + # Ensure that the current user has been created before creating the app user + depends_on = [ + azurerm_redis_cache_access_policy_assignment.primary_current_user + ] +} + # ---------------------------------------------------------------------------------------------- # Cache - Prod - Secondary Region # ---------------------------------------------------------------------------------------------- @@ -29,6 +53,29 @@ module "secondary_cache" { log_analytics_workspace_id = module.hub_app_insights[0].log_analytics_workspace_id } +resource "azurerm_redis_cache_access_policy_assignment" "secondary_current_user" { + count = var.environment == "prod" ? 1 : 0 + name = "secondarycurrentuser" + redis_cache_id = module.secondary_cache[0].cache_id + access_policy_name = "Data Contributor" + object_id = data.azuread_client_config.current.object_id + object_id_alias = "currentuser" +} + +resource "azurerm_redis_cache_access_policy_assignment" "secondary_app_user" { + count = var.environment == "prod" ? 1 : 0 + name = "secondaryappuser" + redis_cache_id = module.secondary_cache[0].cache_id + access_policy_name = "Data Contributor" + object_id = azurerm_user_assigned_identity.secondary_app_service_identity[0].principal_id + object_id_alias = azurerm_user_assigned_identity.secondary_app_service_identity[0].principal_id + + # Ensure that the current user has been created before creating the app user + depends_on = [ + azurerm_redis_cache_access_policy_assignment.secondary_current_user + ] +} + # ---------------------------------------------------------------------------------------------- # Cache - Dev # ---------------------------------------------------------------------------------------------- @@ -43,3 +90,26 @@ module "dev-cache" { private_endpoint_subnet_id = null log_analytics_workspace_id = module.dev_app_insights[0].log_analytics_workspace_id } + +resource "azurerm_redis_cache_access_policy_assignment" "dev_current_user" { + count = var.environment == "dev" ? 1 : 0 + name = "devcurrentuser" + redis_cache_id = module.dev-cache[0].cache_id + access_policy_name = "Data Contributor" + object_id = data.azuread_client_config.current.object_id + object_id_alias = "currentuser" +} + +resource "azurerm_redis_cache_access_policy_assignment" "dev_app_user" { + count = var.environment == "dev" ? 1 : 0 + name = "devappuser" + redis_cache_id = module.dev-cache[0].cache_id + access_policy_name = "Data Contributor" + object_id = azurerm_user_assigned_identity.dev_app_service_identity[0].principal_id + object_id_alias = azurerm_user_assigned_identity.dev_app_service_identity[0].principal_id + + # Ensure that the current user has been created before creating the app user + depends_on = [ + azurerm_redis_cache_access_policy_assignment.dev_current_user + ] +} diff --git a/infra/terraform/identity.tf b/infra/terraform/identity.tf new file mode 100644 index 00000000..74d2b7ff --- /dev/null +++ b/infra/terraform/identity.tf @@ -0,0 +1,54 @@ +# ------------------------------------------------ +# Identity for the Production Primary App Service +# ------------------------------------------------ + +resource "azurecaf_name" "primary_app_service_identity_name" { + count = var.environment == "prod" ? 1 : 0 + name = var.application_name + resource_type = "azurerm_user_assigned_identity" + suffixes = [var.location, var.environment] +} + +resource "azurerm_user_assigned_identity" "primary_app_service_identity" { + count = var.environment == "prod" ? 1 : 0 + location = azurerm_resource_group.spoke[0].location + name = azurecaf_name.primary_app_service_identity_name[0].result + resource_group_name = azurerm_resource_group.spoke[0].name +} + +# ------------------------------------------------ +# Identity for the Production Secondary App Service +# ------------------------------------------------ + +resource "azurecaf_name" "secondary_app_service_identity_name" { + count = var.environment == "prod" ? 1 : 0 + name = var.application_name + resource_type = "azurerm_user_assigned_identity" + suffixes = [var.secondary_location, var.environment] +} + +resource "azurerm_user_assigned_identity" "secondary_app_service_identity" { + count = var.environment == "prod" ? 1 : 0 + location = azurerm_resource_group.secondary_spoke[0].location + name = azurecaf_name.secondary_app_service_identity_name[0].result + resource_group_name = azurerm_resource_group.secondary_spoke[0].name +} + + +# ------------------------------------------------ +# Identity for the Production Dev App Service +# ------------------------------------------------ + +resource "azurecaf_name" "dev_app_service_identity_name" { + count = var.environment == "dev" ? 1 : 0 + name = var.application_name + resource_type = "azurerm_user_assigned_identity" + suffixes = [var.location, var.environment] +} + +resource "azurerm_user_assigned_identity" "dev_app_service_identity" { + count = var.environment == "dev" ? 1 : 0 + location = azurerm_resource_group.dev[0].location + name = azurecaf_name.dev_app_service_identity_name[0].result + resource_group_name = azurerm_resource_group.dev[0].name +} diff --git a/infra/terraform/secrets.tf b/infra/terraform/secrets.tf index 94f3dded..d7c66e5b 100644 --- a/infra/terraform/secrets.tf +++ b/infra/terraform/secrets.tf @@ -94,10 +94,10 @@ resource "azurerm_key_vault_secret" "contoso_application_client_secret" { ] } -resource "azurerm_key_vault_secret" "contoso_cache_secret" { +resource "azurerm_key_vault_secret" "primary_redis_user_secret" { count = var.environment == "prod" ? 1 : 0 - name = "contoso-redis-password" - value = module.cache[0].cache_secret + name = "contoso-primary-redis-user-object-id" + value = azurerm_user_assigned_identity.primary_app_service_identity[0].client_id key_vault_id = module.hub_key_vault[0].vault_id depends_on = [ azurerm_role_assignment.kv_administrator_user_role_assignement @@ -128,6 +128,16 @@ resource "azurerm_key_vault_secret" "secondary_contoso_database_url" { ] } +resource "azurerm_key_vault_secret" "secondary_redis_user_secret" { + count = var.environment == "prod" ? 1 : 0 + name = "contoso-secondary-redis-user-object-id" + value = azurerm_user_assigned_identity.secondary_app_service_identity[0].client_id + key_vault_id = module.hub_key_vault[0].vault_id + depends_on = [ + azurerm_role_assignment.kv_administrator_user_role_assignement + ] +} + # Give the app access to the key vault secrets - https://learn.microsoft.com/azure/key-vault/general/rbac-guide?tabs=azure-cli#secret-scope-role-assignment resource azurerm_role_assignment app_keyvault_role_assignment { count = var.environment == "prod" ? 1 : 0 @@ -216,10 +226,10 @@ resource "azurerm_key_vault_secret" "dev_contoso_application_client_secret" { ] } -resource "azurerm_key_vault_secret" "dev_contoso_cache_secret" { +resource "azurerm_key_vault_secret" "dev_redis_user_secret" { count = var.environment == "dev" ? 1 : 0 - name = "contoso-redis-password" - value = module.dev-cache[0].cache_secret + name = "contoso-dev-redis-user-object-id" + value = azurerm_user_assigned_identity.dev_app_service_identity[0].client_id key_vault_id = module.dev_key_vault[0].vault_id depends_on = [ azurerm_role_assignment.dev_kv_administrator_user_role_assignement diff --git a/infra/terraform/versions.tf b/infra/terraform/versions.tf index db30b3bf..53ce0610 100644 --- a/infra/terraform/versions.tf +++ b/infra/terraform/versions.tf @@ -2,7 +2,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "3.112.0" + version = "3.114.0" } azurecaf = { source = "aztfmod/azurecaf" diff --git a/src/contoso-fiber/pom.xml b/src/contoso-fiber/pom.xml index fb2f5a80..2f47a70a 100644 --- a/src/contoso-fiber/pom.xml +++ b/src/contoso-fiber/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.2 + 3.2.5 @@ -18,8 +18,8 @@ 17 - 5.5.0 - 2022.0.4 + 5.14.0 + 2023.0.2 @@ -71,6 +71,10 @@ org.springframework.boot spring-boot-starter-data-redis + + com.azure.spring + spring-cloud-azure-starter-data-redis-lettuce + org.springframework.session spring-session-data-redis @@ -100,12 +104,10 @@ nz.net.ultraq.thymeleaf thymeleaf-layout-dialect - 3.2.1 com.fasterxml.jackson.core jackson-databind - 2.15.2 @@ -123,6 +125,11 @@ spring-boot-starter-aop + + org.apache.commons + commons-lang3 + + diff --git a/src/contoso-fiber/src/main/resources/application.properties b/src/contoso-fiber/src/main/resources/application.properties index 05ceab10..3e439bfb 100644 --- a/src/contoso-fiber/src/main/resources/application.properties +++ b/src/contoso-fiber/src/main/resources/application.properties @@ -5,6 +5,10 @@ spring.jpa.hibernate.ddl-auto=validate spring.cloud.azure.active-directory.enabled=true # Redis +spring.data.redis.host=${AZURE_CACHE_REDIS_HOST} +spring.data.redis.port=${AZURE_CACHE_REDIS_PORT} +spring.data.redis.azure.passwordless-enabled=true +spring.data.redis.azure.credential.client-id=${AZURE_CACHE_REDIS_CLIENT_ID} spring.data.redis.ssl.enabled=true # Spring Session to leverage Redis to back a web application’s HttpSession