diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore index d49ee98d..3e8d0394 100644 --- a/infrastructure/.gitignore +++ b/infrastructure/.gitignore @@ -28,3 +28,4 @@ override.tf.json # Ignore CLI configuration files .terraformrc terraform.rc +.terraform.lock.hcl diff --git a/infrastructure/environments/poc/variables.sh b/infrastructure/environments/poc/variables.sh index 83cecc05..f3b37dd9 100644 --- a/infrastructure/environments/poc/variables.sh +++ b/infrastructure/environments/poc/variables.sh @@ -3,7 +3,7 @@ ENV_CONFIG=poc AZURE_SUBSCRIPTION="Lung Cancer Screening - Dev" HUB_SUBSCRIPTION="Lung Cancer Screening - Dev" STORAGE_ACCOUNT_RG=rg-tfstate-poc-uks -TERRAFORM_MODULES_REF=main +TERRAFORM_MODULES_REF=feat/public-container-app-env ENABLE_SOFT_DELETE=false DOCKER_IMAGE=docker.io/nginxdemos/hello DOCKER_IMAGE_TAG=latest diff --git a/infrastructure/environments/poc/variables.tfvars b/infrastructure/environments/poc/variables.tfvars index 8c55e584..31233e81 100644 --- a/infrastructure/environments/poc/variables.tfvars +++ b/infrastructure/environments/poc/variables.tfvars @@ -1,10 +1,10 @@ -features = { +deploy_database_as_container = true +features = { front_door = false hub_and_spoke = false private_networking = false } postgres_backup_retention_days = 7 postgres_geo_redundant_backup_enabled = false -private_networking = false protect_keyvault = false vnet_address_space = "10.65.0.0/16" diff --git a/infrastructure/environments/poc/variables.yml b/infrastructure/environments/poc/variables.yml new file mode 100644 index 00000000..ab403b92 --- /dev/null +++ b/infrastructure/environments/poc/variables.yml @@ -0,0 +1 @@ +EXAMPLE_KEY: example_value diff --git a/infrastructure/modules/container-apps/data.tf b/infrastructure/modules/container-apps/data.tf new file mode 100644 index 00000000..4c90b849 --- /dev/null +++ b/infrastructure/modules/container-apps/data.tf @@ -0,0 +1,32 @@ +data "azurerm_client_config" "current" {} + +# data "azuread_group" "postgres_sql_admin_group" { +# display_name = var.postgres_sql_admin_group +# } + +data "azurerm_private_dns_zone" "storage" { + count = var.features.private_networking ? 1 : 0 + + provider = azurerm.hub + + name = "privatelink.blob.core.windows.net" + resource_group_name = "rg-hub-${var.hub}-uks-private-dns-zones" +} + +data "azurerm_private_dns_zone" "storage-account-blob" { + count = var.features.private_networking ? 1 : 0 + + provider = azurerm.hub + + name = "privatelink.blob.core.windows.net" + resource_group_name = "rg-hub-${var.hub}-uks-private-dns-zones" +} + +data "azurerm_private_dns_zone" "storage-account-queue" { + count = var.features.private_networking ? 1 : 0 + + provider = azurerm.hub + + name = "privatelink.queue.core.windows.net" + resource_group_name = "rg-hub-${var.hub}-uks-private-dns-zones" +} diff --git a/infrastructure/modules/container-apps/front_door.tf b/infrastructure/modules/container-apps/front_door.tf new file mode 100644 index 00000000..c99dc67e --- /dev/null +++ b/infrastructure/modules/container-apps/front_door.tf @@ -0,0 +1,45 @@ +data "azurerm_cdn_frontdoor_profile" "this" { + count = var.features.front_door ? 1 : 0 + + provider = azurerm.hub + + name = var.front_door_profile + resource_group_name = "rg-hub-${var.hub}-uks-${var.app_short_name}" +} + +module "frontdoor_endpoint" { + count = var.features.front_door ? 1 : 0 + + source = "../dtos-devops-templates/infrastructure/modules/cdn-frontdoor-endpoint" + + providers = { + azurerm = azurerm.hub # Each project's Front Door profile (with secrets) resides in Hub since it's shared infra with a Non-live/Live deployment pattern + azurerm.dns = azurerm.hub + } + + cdn_frontdoor_profile_id = data.azurerm_cdn_frontdoor_profile.this[0].id + custom_domains = { + "${var.environment}-domain" = { + host_name = local.hostname # For prod it must be equal to the dns_zone_name to use apex + dns_zone_name = var.dns_zone_name + dns_zone_rg_name = "rg-hub-${var.hub}-uks-public-dns-zones" + } + } + name = var.environment # environment-specific to avoid naming collisions within a Front Door Profile + + origins = { + "${var.environment}-origin" = { + hostname = module.webapp.fqdn + origin_host_header = module.webapp.fqdn + private_link = { + target_type = "managedEnvironments" + location = var.region + private_link_target_id = var.container_app_environment_id + } + } + } + route = { + https_redirect_enabled = true + supported_protocols = ["Http", "Https"] + } +} diff --git a/infrastructure/modules/container-apps/main.tf b/infrastructure/modules/container-apps/main.tf new file mode 100644 index 00000000..92e9f948 --- /dev/null +++ b/infrastructure/modules/container-apps/main.tf @@ -0,0 +1,34 @@ +resource "azurerm_resource_group" "main" { + name = local.resource_group_name + location = var.region +} + +module "webapp" { + source = "../dtos-devops-templates/infrastructure/modules/container-app" + + providers = { + azurerm = azurerm + azurerm.hub = azurerm.hub + } + + name = "${var.app_short_name}-web-${var.environment}" + container_app_environment_id = var.container_app_environment_id + resource_group_name = azurerm_resource_group.main.name + fetch_secrets_from_app_key_vault = var.fetch_secrets_from_app_key_vault + infra_key_vault_name = "kv-${var.app_short_name}-${var.env_config}-inf" + infra_key_vault_rg = "rg-${var.app_short_name}-${var.env_config}-infra" + enable_auth = var.enable_auth + app_key_vault_id = var.app_key_vault_id + docker_image = var.docker_image + user_assigned_identity_ids = var.deploy_database_as_container ? [] : [module.db_connect_identity[0].id] + environment_variables = merge( + local.common_env, + { + ALLOWED_HOSTS = "${var.app_short_name}-web-${var.environment}.${var.default_domain}" + }, + var.deploy_database_as_container ? local.container_db_env : local.azure_db_env + ) + secret_variables = var.deploy_database_as_container ? { DATABASE_PASSWORD = resource.random_password.admin_password[0].result } : {} + is_web_app = true + port = 80 +} diff --git a/infrastructure/modules/container-apps/output.tf b/infrastructure/modules/container-apps/output.tf new file mode 100644 index 00000000..15d19fe0 --- /dev/null +++ b/infrastructure/modules/container-apps/output.tf @@ -0,0 +1,7 @@ +output "internal_url" { + value = module.webapp.url +} + +output "external_url" { + value = var.features.front_door ? "https://${module.frontdoor_endpoint.custom_domains["${var.environment}-domain"].host_name}/" : null +} diff --git a/infrastructure/modules/container-apps/postgres.tf b/infrastructure/modules/container-apps/postgres.tf new file mode 100644 index 00000000..eaa42924 --- /dev/null +++ b/infrastructure/modules/container-apps/postgres.tf @@ -0,0 +1,100 @@ +data "azurerm_private_dns_zone" "postgres" { + count = var.features.private_networking ? 1 : 0 + + provider = azurerm.hub + + name = "privatelink.postgres.database.azure.com" + resource_group_name = "rg-hub-${var.hub}-uks-private-dns-zones" +} + +# Don't deploy if deploy_database_as_container is true +module "postgres" { + count = var.deploy_database_as_container ? 0 : 1 + + source = "../dtos-devops-templates/infrastructure/modules/postgresql-flexible" + + # postgresql Server + name = "postgres-${var.app_short_name}-${var.environment}-uks" + resource_group_name = azurerm_resource_group.main.name + location = var.region + + backup_retention_days = var.postgres_backup_retention_days + geo_redundant_backup_enabled = var.postgres_geo_redundant_backup_enabled + postgresql_admin_object_id = "" #data.azuread_group.postgres_sql_admin_group.object_id + postgresql_admin_principal_name = var.postgres_sql_admin_group + postgresql_admin_principal_type = "Group" + administrator_login = local.database_user + admin_identities = [module.db_connect_identity[0]] + + # Diagnostic Settings + log_analytics_workspace_id = var.log_analytics_workspace_audit_id + monitor_diagnostic_setting_postgresql_server_enabled_logs = ["PostgreSQLLogs", "PostgreSQLFlexSessions", "PostgreSQLFlexQueryStoreRuntime", "PostgreSQLFlexQueryStoreWaitStats", "PostgreSQLFlexTableStats", "PostgreSQLFlexDatabaseXacts"] + monitor_diagnostic_setting_postgresql_server_metrics = ["AllMetrics"] + + sku_name = var.postgres_sku_name + storage_mb = var.postgres_storage_mb + storage_tier = var.postgres_storage_tier + + server_version = "16" + tenant_id = data.azurerm_client_config.current.tenant_id + + private_endpoint_properties = var.features.private_networking ? { + private_dns_zone_ids_postgresql = [data.azurerm_private_dns_zone.postgres[0].id] + private_endpoint_enabled = true + private_endpoint_subnet_id = var.postgres_subnet_id + private_endpoint_resource_group_name = azurerm_resource_group.main.name + private_service_connection_is_manual = false + } : null + + databases = { + db1 = { + collation = "en_US.utf8" + charset = "UTF8" + max_size_gb = 10 + name = local.database_name + } + } + + tags = {} +} + +module "db_connect_identity" { + count = var.deploy_database_as_container ? 0 : 1 + + source = "../dtos-devops-templates/infrastructure/modules/managed-identity" + resource_group_name = azurerm_resource_group.main.name + location = var.region + uai_name = "mi-${var.app_short_name}-${var.environment}-db-connect" +} + +resource "random_password" "admin_password" { + count = var.deploy_database_as_container ? 1 : 0 + + length = 30 + special = true + override_special = "!@#$%^&*()-_=+" +} + +module "database_container" { + count = var.deploy_database_as_container ? 1 : 0 + + providers = { + azurerm = azurerm + azurerm.hub = azurerm.hub + } + + source = "../dtos-devops-templates/infrastructure/modules/container-app" + name = "${var.app_short_name}-db-${var.environment}" + container_app_environment_id = var.container_app_environment_id + docker_image = "postgres:16" + secret_variables = var.deploy_database_as_container ? { POSTGRES_PASSWORD = resource.random_password.admin_password[0].result } : {} + environment_variables = { + POSTGRES_USER = local.database_user + POSTGRES_DB = local.database_name + } + resource_group_name = azurerm_resource_group.main.name + is_tcp_app = true + # postgres has a port of 5432 + port = 5432 + exposed_port = local.database_port +} diff --git a/infrastructure/modules/container-apps/providers.tf b/infrastructure/modules/container-apps/providers.tf new file mode 100644 index 00000000..63db07d5 --- /dev/null +++ b/infrastructure/modules/container-apps/providers.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + configuration_aliases = [azurerm.hub] + } + } +} diff --git a/infrastructure/modules/container-apps/storage.tf b/infrastructure/modules/container-apps/storage.tf new file mode 100644 index 00000000..1f9ac80d --- /dev/null +++ b/infrastructure/modules/container-apps/storage.tf @@ -0,0 +1,53 @@ +module "azure_blob_storage_identity" { + source = "../dtos-devops-templates/infrastructure/modules/managed-identity" + resource_group_name = azurerm_resource_group.main.name + location = var.region + uai_name = "mi-${var.app_short_name}-${var.environment}-blob-storage" +} + +module "azure_queue_storage_identity" { + source = "../dtos-devops-templates/infrastructure/modules/managed-identity" + resource_group_name = azurerm_resource_group.main.name + location = var.region + uai_name = "mi-${var.app_short_name}-${var.environment}-queue-storage" +} + +module "storage" { + source = "../dtos-devops-templates/infrastructure/modules/storage" + + containers = local.storage_containers + location = var.region + log_analytics_workspace_id = var.log_analytics_workspace_audit_id + + monitor_diagnostic_setting_storage_account_enabled_logs = ["StorageWrite", "StorageRead", "StorageDelete"] + monitor_diagnostic_setting_storage_account_metrics = ["AllMetrics"] + + name = replace(lower(local.storage_account_name), "-", "") + + private_endpoint_properties = var.features.private_networking ? { + private_dns_zone_ids_blob = [data.azurerm_private_dns_zone.storage-account-blob[0].id] + private_dns_zone_ids_queue = [data.azurerm_private_dns_zone.storage-account-queue[0].id] + private_endpoint_enabled = true + private_endpoint_subnet_id = var.main_subnet_id + private_endpoint_resource_group_name = azurerm_resource_group.main.name + private_service_connection_is_manual = false + } : null + queues = local.storage_queues + resource_group_name = azurerm_resource_group.main.name +} + +module "blob_storage_role_assignment" { + source = "../dtos-devops-templates/infrastructure/modules/rbac-assignment" + principal_id = module.azure_blob_storage_identity.principal_id + role_definition_name = "Storage Blob Data Contributor" + scope = module.storage.storage_account_id + depends_on = [module.storage, module.azure_blob_storage_identity] +} + +module "queue_storage_role_assignment" { + source = "../dtos-devops-templates/infrastructure/modules/rbac-assignment" + principal_id = module.azure_queue_storage_identity.principal_id + role_definition_name = "Storage Queue Data Contributor" + scope = module.storage.storage_account_id + depends_on = [module.storage, module.azure_queue_storage_identity] +} diff --git a/infrastructure/modules/container-apps/variables.tf b/infrastructure/modules/container-apps/variables.tf new file mode 100644 index 00000000..e479533b --- /dev/null +++ b/infrastructure/modules/container-apps/variables.tf @@ -0,0 +1,186 @@ +variable "api_oauth_token_url" { + description = "The OAuth API endpoint URL used to request client credentials for NHS Notify API" + type = string + default = null +} + +variable "app_key_vault_id" { + description = "Application key vault ID" + type = string +} + +variable "app_short_name" { + description = "Application short name (6 characters)" + type = string +} + +variable "container_app_environment_id" { + description = "The ID of the container app environment where container apps are deployed" + type = string +} + +variable "default_domain" { + description = "The container app environment default domain" + type = string +} + +variable "dns_zone_name" { + description = "Public DNS zone name" + type = string + default = "" +} + +variable "docker_image" { + description = "Docker image full path including registry, repository and tag" + type = string +} + +variable "enable_auth" { + description = "Enable authentication for the container app. If true, the app will use Azure AD authentication." + type = bool +} + +variable "env_config" { + description = "Environment configuration. Different environments may share the same environment config and the same infrastructure" + type = string +} + +variable "environment" { + description = "Application environment name" + type = string +} + +variable "features" { + description = "Feature flags for the deployment" + type = object({ + front_door = optional(bool, true) + hub_and_spoke = optional(bool, true) + private_networking = optional(bool, true) + }) +} + +variable "fetch_secrets_from_app_key_vault" { + description = <