From 624ee007841b2926f72c0f2211f5bf7c3cf56ad9 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Fri, 23 May 2025 14:46:55 +0100 Subject: [PATCH 01/24] audit infra --- .../cd-infrastructure-dev-audit.yaml | 4 +- .../pipelines/cd-infrastructure-dev-core.yaml | 4 +- .github/workflows/cicd-1-pull-request.yaml | 2 +- infrastructure/tf-audit/app_insights.tf | 15 +++ infrastructure/tf-audit/config.tf | 21 +++ infrastructure/tf-audit/data.tf | 12 ++ .../tf-audit/diagnostic_settings_audit.tf | 33 +++++ .../tf-audit/environments/development.tfvars | 54 ++++++++ .../tf-audit/environments/integration.tfvars | 54 ++++++++ .../tf-audit/environments/nft.tfvars | 54 ++++++++ .../tf-audit/log_analytics_workspace.tf | 57 ++++++++ infrastructure/tf-audit/networking.tf | 127 ++++++++++++++++++ infrastructure/tf-audit/outputs.tf | 21 +++ .../tf-audit/private_link_scoped_service.tf | 33 +++++ infrastructure/tf-audit/providers.tf | 28 ++++ infrastructure/tf-audit/rbac.tf | 7 + infrastructure/tf-audit/storage.tf | 51 +++++++ infrastructure/tf-audit/variables.tf | 126 +++++++++++++++++ 18 files changed, 698 insertions(+), 5 deletions(-) create mode 100644 infrastructure/tf-audit/app_insights.tf create mode 100644 infrastructure/tf-audit/config.tf create mode 100644 infrastructure/tf-audit/data.tf create mode 100644 infrastructure/tf-audit/diagnostic_settings_audit.tf create mode 100644 infrastructure/tf-audit/environments/development.tfvars create mode 100644 infrastructure/tf-audit/environments/integration.tfvars create mode 100644 infrastructure/tf-audit/environments/nft.tfvars create mode 100644 infrastructure/tf-audit/log_analytics_workspace.tf create mode 100644 infrastructure/tf-audit/networking.tf create mode 100644 infrastructure/tf-audit/outputs.tf create mode 100644 infrastructure/tf-audit/private_link_scoped_service.tf create mode 100644 infrastructure/tf-audit/providers.tf create mode 100644 infrastructure/tf-audit/rbac.tf create mode 100644 infrastructure/tf-audit/storage.tf create mode 100644 infrastructure/tf-audit/variables.tf diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-audit.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-audit.yaml index 86710f4..3d6d8ad 100644 --- a/.azuredevops/pipelines/cd-infrastructure-dev-audit.yaml +++ b/.azuredevops/pipelines/cd-infrastructure-dev-audit.yaml @@ -14,7 +14,7 @@ resources: - repository: dtos-devops-templates type: github name: NHSDigital/dtos-devops-templates - ref: f8141ab50ec0f3630044fa0f531952d2dbbd1e85 + ref: 9673ee4ef9770e80d0714c3966a699414b7b43c7 endpoint: NHSDigital variables: @@ -23,7 +23,7 @@ variables: - name: TF_DIRECTORY value: $(System.DefaultWorkingDirectory)/$(System.TeamProject)/infrastructure/tf-audit - name: TF_VERSION - value: 1.9.2 + value: 1.11.4 - name: TF_PLAN_ARTIFACT value: tf_plan_audit_DEV - name: ENVIRONMENT diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml index 2e987c0..fe9eae6 100644 --- a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml +++ b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml @@ -14,7 +14,7 @@ resources: - repository: dtos-devops-templates type: github name: NHSDigital/dtos-devops-templates - ref: f8141ab50ec0f3630044fa0f531952d2dbbd1e85 + ref: 9673ee4ef9770e80d0714c3966a699414b7b43c7 endpoint: NHSDigital variables: @@ -24,7 +24,7 @@ variables: - name: TF_DIRECTORY value: $(System.DefaultWorkingDirectory)/$(System.TeamProject)/infrastructure/tf-core - name: TF_VERSION - value: 1.9.2 + value: 1.11.4 - name: TF_PLAN_ARTIFACT value: tf_plan_core_DEV - name: ENVIRONMENT diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index cbf106a..6de28c6 100644 --- a/.github/workflows/cicd-1-pull-request.yaml +++ b/.github/workflows/cicd-1-pull-request.yaml @@ -107,7 +107,7 @@ jobs: if: needs.metadata.outputs.does_pull_request_exist == 'true' || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')) with: docker_compose_file: ./compose.yaml - excluded_containers_csv_list: azurite,azurite-setup,sql-database,database-setup + excluded_containers_csv_list: azurite,azurite-setup,sql-database,database-setup,db environment_tag: ${{ needs.metadata.outputs.environment_tag }} function_app_source_code_path: src project_name: service-layer diff --git a/infrastructure/tf-audit/app_insights.tf b/infrastructure/tf-audit/app_insights.tf new file mode 100644 index 0000000..cc0bfc6 --- /dev/null +++ b/infrastructure/tf-audit/app_insights.tf @@ -0,0 +1,15 @@ +module "app_insights_audit" { + for_each = { for key, val in var.regions : key => val if val.is_primary_region } + + source = "../../../dtos-devops-templates/infrastructure/modules/app-insights" + + name = module.regions_config[each.key].names.app-insights + location = each.key + appinsights_type = var.app_insights.appinsights_type + + log_analytics_workspace_id = module.log_analytics_workspace_audit[each.key].id + + resource_group_name = azurerm_resource_group.audit[each.key].name + tags = var.tags + +} diff --git a/infrastructure/tf-audit/config.tf b/infrastructure/tf-audit/config.tf new file mode 100644 index 0000000..fee7f15 --- /dev/null +++ b/infrastructure/tf-audit/config.tf @@ -0,0 +1,21 @@ +resource "azurerm_resource_group" "audit" { + for_each = { for key, val in var.regions : key => val if val.is_primary_region } + + name = "${module.regions_config[each.key].names.resource-group}-audit" + location = each.key + + lifecycle { + ignore_changes = [tags] + } +} + +module "regions_config" { + for_each = var.regions + + source = "../../../dtos-devops-templates/infrastructure/modules/shared-config" + + location = each.key + application = var.application + env = var.environment + tags = var.tags +} diff --git a/infrastructure/tf-audit/data.tf b/infrastructure/tf-audit/data.tf new file mode 100644 index 0000000..442d60f --- /dev/null +++ b/infrastructure/tf-audit/data.tf @@ -0,0 +1,12 @@ +data "azurerm_client_config" "current" {} + +data "terraform_remote_state" "hub" { + backend = "azurerm" + config = { + subscription_id = var.HUB_SUBSCRIPTION_ID + storage_account_name = var.HUB_BACKEND_AZURE_STORAGE_ACCOUNT_NAME + container_name = var.HUB_BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME + key = var.HUB_BACKEND_AZURE_STORAGE_ACCOUNT_KEY + resource_group_name = var.HUB_BACKEND_AZURE_RESOURCE_GROUP_NAME + } +} diff --git a/infrastructure/tf-audit/diagnostic_settings_audit.tf b/infrastructure/tf-audit/diagnostic_settings_audit.tf new file mode 100644 index 0000000..f52e48e --- /dev/null +++ b/infrastructure/tf-audit/diagnostic_settings_audit.tf @@ -0,0 +1,33 @@ +locals { + #APPSERVICEPLAN + monitor_diagnostic_setting_appserviceplan_metrics = ["AllMetrics"] + + #FUNCTIONAPP + monitor_diagnostic_setting_function_app_enabled_logs = ["AppServiceAuthenticationLogs", "FunctionAppLogs"] + monitor_diagnostic_setting_function_app_metrics = ["AllMetrics"] + + # KEYVAULT + monitor_diagnostic_setting_keyvault_enabled_logs = ["AuditEvent", "AzurePolicyEvaluationDetails"] + monitor_diagnostic_setting_keyvault_metrics = ["AllMetrics"] + + # LOG ANALYTICS WORKSPACE + monitor_diagnostic_setting_log_analytics_workspace_enabled_logs = ["SummaryLogs", "Audit"] + monitor_diagnostic_setting_log_analytics_workspace_metrics = ["AllMetrics"] + + #SQL SERVER AND DATABASE + monitor_diagnostic_setting_database_enabled_logs = ["SQLSecurityAuditEvents", "SQLInsights", "QueryStoreWaitStatistics", "Errors", "DatabaseWaitStatistics", "Timeouts"] + monitor_diagnostic_setting_database_metrics = ["Basic", "InstanceAndAppAdvanced", "WorkloadManagement"] + monitor_diagnostic_setting_sql_server_enabled_logs = ["SQLSecurityAuditEvents"] + monitor_diagnostic_setting_sql_server_metrics = ["Basic", "InstanceAndAppAdvanced", "WorkloadManagement"] + + #STORAGE ACCOUNT + monitor_diagnostic_setting_storage_account_enabled_logs = ["StorageWrite", "StorageRead", "StorageDelete"] + monitor_diagnostic_setting_storage_account_metrics = ["Capacity", "Transaction"] + + #SUBNET + monitor_diagnostic_setting_network_security_group_enabled_logs = ["NetworkSecurityGroupEvent", "NetworkSecurityGroupRuleCounter"] + + #VNET + monitor_diagnostic_setting_vnet_enabled_logs = ["VMProtectionAlerts"] + monitor_diagnostic_setting_vnet_metrics = ["AllMetrics"] +} diff --git a/infrastructure/tf-audit/environments/development.tfvars b/infrastructure/tf-audit/environments/development.tfvars new file mode 100644 index 0000000..4b4a028 --- /dev/null +++ b/infrastructure/tf-audit/environments/development.tfvars @@ -0,0 +1,54 @@ +application = "svclyr" +application_full_name = "service-layer" +environment = "DEV" + +features = { + private_endpoints_enabled = true + private_service_connection_is_manual = false + log_analytics_data_export_rule_enabled = false + public_network_access_enabled = false +} + +tags = { + Project = "Service-Layer" +} + +regions = { + uksouth = { + is_primary_region = true + address_space = "10.135.0.0/16" + connect_peering = true + subnets = { + pep = { + cidr_newbits = 8 + cidr_offset = 1 + } + } + } +} + +app_insights = { + appinsights_type = "web" +} + +law = { + law_sku = "PerGB2018" + retention_days = 30 + export_enabled = false + export_table_names = ["Alert"] +} + +storage_accounts = { + sqllogs = { + name_suffix = "sqllogs" + account_tier = "Standard" + replication_type = "LRS" + public_network_access_enabled = false + containers = { + vulnerability-assessment = { + container_name = "vulnerability-assessment" + container_access_type = "private" + } + } + } +} diff --git a/infrastructure/tf-audit/environments/integration.tfvars b/infrastructure/tf-audit/environments/integration.tfvars new file mode 100644 index 0000000..cab25fc --- /dev/null +++ b/infrastructure/tf-audit/environments/integration.tfvars @@ -0,0 +1,54 @@ +application = "svclyr" +application_full_name = "service-layer" +environment = "INT" + +features = { + private_endpoints_enabled = true + private_service_connection_is_manual = false + log_analytics_data_export_rule_enabled = false + public_network_access_enabled = false +} + +tags = { + Project = "Service-Layer" +} + +regions = { + uksouth = { + is_primary_region = true + address_space = "10.139.0.0/16" + connect_peering = true + subnets = { + pep = { + cidr_newbits = 8 + cidr_offset = 1 + } + } + } +} + +app_insights = { + appinsights_type = "web" +} + +law = { + law_sku = "PerGB2018" + retention_days = 30 + export_enabled = false + export_table_names = ["Alert"] +} + +storage_accounts = { + sqllogs = { + name_suffix = "sqllogs" + account_tier = "Standard" + replication_type = "LRS" + public_network_access_enabled = false + containers = { + vulnerability-assessment = { + container_name = "vulnerability-assessment" + container_access_type = "private" + } + } + } +} diff --git a/infrastructure/tf-audit/environments/nft.tfvars b/infrastructure/tf-audit/environments/nft.tfvars new file mode 100644 index 0000000..bb6ad31 --- /dev/null +++ b/infrastructure/tf-audit/environments/nft.tfvars @@ -0,0 +1,54 @@ +application = "svclyr" +application_full_name = "service-layer" +environment = "NFT" + +features = { + private_endpoints_enabled = true + private_service_connection_is_manual = false + log_analytics_data_export_rule_enabled = false + public_network_access_enabled = false +} + +tags = { + Project = "Service-Layer" +} + +regions = { + uksouth = { + is_primary_region = true + address_space = "10.137.0.0/16" + connect_peering = true + subnets = { + pep = { + cidr_newbits = 8 + cidr_offset = 1 + } + } + } +} + +app_insights = { + appinsights_type = "web" +} + +law = { + law_sku = "PerGB2018" + retention_days = 30 + export_enabled = false + export_table_names = ["Alert"] +} + +storage_accounts = { + sqllogs = { + name_suffix = "sqllogs" + account_tier = "Standard" + replication_type = "LRS" + public_network_access_enabled = false + containers = { + vulnerability-assessment = { + container_name = "vulnerability-assessment" + container_access_type = "private" + } + } + } +} diff --git a/infrastructure/tf-audit/log_analytics_workspace.tf b/infrastructure/tf-audit/log_analytics_workspace.tf new file mode 100644 index 0000000..79be9f7 --- /dev/null +++ b/infrastructure/tf-audit/log_analytics_workspace.tf @@ -0,0 +1,57 @@ +module "log_analytics_workspace_audit" { + for_each = var.regions + + source = "../../../dtos-devops-templates/infrastructure/modules/log-analytics-workspace" + + name = module.regions_config[each.key].names.log-analytics-workspace + location = each.key + + law_sku = var.law.law_sku + retention_days = var.law.retention_days + + monitor_diagnostic_setting_log_analytics_workspace_enabled_logs = local.monitor_diagnostic_setting_log_analytics_workspace_enabled_logs + monitor_diagnostic_setting_log_analytics_workspace_metrics = local.monitor_diagnostic_setting_log_analytics_workspace_metrics + + resource_group_name = azurerm_resource_group.audit[each.key].name + + tags = var.tags +} + +# Add a data export rule to forward logs to the Event Hub in the Hub subscription +module "log_analytics_data_export_rule" { + for_each = var.features.log_analytics_data_export_rule_enabled ? var.regions : {} + + source = "../../../dtos-devops-templates/infrastructure/modules/log-analytics-data-export-rule" + + name = "${module.regions_config[each.key].names.log-analytics-workspace}-export-rule" + resource_group_name = azurerm_resource_group.audit[each.key].name + workspace_resource_id = module.log_analytics_workspace_audit[each.key].id + destination_resource_id = data.terraform_remote_state.hub.outputs.event_hubs["dtos-hub-${each.key}"]["${var.application_full_name}-${lower(var.environment)}"].id + table_names = var.law.export_table_names + enabled = var.law.export_enabled +} + +/*-------------------------------------------------------------------------------------------------- + RBAC Assignments +--------------------------------------------------------------------------------------------------*/ +/* +For sending events to the Event Hub: +* Azure Event Hubs Data Sender: Grants permissions to send events to the Event Hub.   +* For receiving events from the Event Hub: + +For receiving events from the Event Hub (i.e. remote resource): +* Azure Event Hubs Data Receiver: Grants permissions to receive events from the Event Hub. +*/ +# module "rbac_assignments" { +# for_each = var.regions + +# source = "../../../dtos-devops-templates/infrastructure/modules/rbac-assignment" + +# principal_id = module.log_analytics_workspace_audit[each.key].0.principal_id +# role_definition_name = "Azure Event Hubs Data Sender" +# scope = data.terraform_remote_state.hub.outputs.eventhub_law_export_id["dtos-hub-${each.key}"] +# } + +output "log_analytics_workspace_audit" { + value = module.log_analytics_workspace_audit +} diff --git a/infrastructure/tf-audit/networking.tf b/infrastructure/tf-audit/networking.tf new file mode 100644 index 0000000..a117221 --- /dev/null +++ b/infrastructure/tf-audit/networking.tf @@ -0,0 +1,127 @@ +locals { + primary_region = [for k, v in var.regions : k if v.is_primary_region][0] +} + +resource "azurerm_resource_group" "rg_vnet" { + for_each = var.regions + + name = "${module.regions_config[each.key].names.resource-group}-audit-networking" + location = each.key +} + +resource "azurerm_resource_group" "rg_private_endpoints" { + for_each = var.features.private_endpoints_enabled ? var.regions : {} + + name = "${module.regions_config[each.key].names.resource-group}-audit-private-endpoints" + location = each.key +} + +module "vnet" { + for_each = var.regions + + source = "../../../dtos-devops-templates/infrastructure/modules/vnet" + + name = module.regions_config[each.key].names.virtual-network + resource_group_name = azurerm_resource_group.rg_vnet[each.key].name + location = each.key + vnet_address_space = each.value.address_space + + log_analytics_workspace_id = module.log_analytics_workspace_audit[local.primary_region].id + monitor_diagnostic_setting_vnet_enabled_logs = local.monitor_diagnostic_setting_vnet_enabled_logs + monitor_diagnostic_setting_vnet_metrics = local.monitor_diagnostic_setting_vnet_metrics + + dns_servers = [data.terraform_remote_state.hub.outputs.private_dns_resolver_inbound_ips[each.key].private_dns_resolver_ip] + + tags = var.tags +} + +/*-------------------------------------------------------------------------------------------------- + Create Subnets +--------------------------------------------------------------------------------------------------*/ + +module "subnets" { + for_each = local.subnets_map + + source = "../../../dtos-devops-templates/infrastructure/modules/subnet" + + name = each.value.subnet_name + location = module.vnet[each.value.vnet_key].vnet.location + network_security_group_name = each.value.nsg_name + network_security_group_nsg_rules = each.value.nsg_rules + create_nsg = each.value.create_nsg + resource_group_name = module.vnet[each.value.vnet_key].vnet.resource_group_name + vnet_name = module.vnet[each.value.vnet_key].name + address_prefixes = [each.value.address_prefixes] + default_outbound_access_enabled = true + private_endpoint_network_policies = "Disabled" # Default as per compliance requirements + + log_analytics_workspace_id = module.log_analytics_workspace_audit[local.primary_region].id + monitor_diagnostic_setting_network_security_group_enabled_logs = local.monitor_diagnostic_setting_network_security_group_enabled_logs + + delegation_name = each.value.delegation_name != null ? each.value.delegation_name : "" + service_delegation_name = each.value.service_delegation_name != null ? each.value.service_delegation_name : "" + service_delegation_actions = each.value.service_delegation_actions != null ? each.value.service_delegation_actions : [] + + tags = var.tags +} + +locals { + # Expand a flattened list of objects for all subnets (allows nested for loops) + subnets_flatlist = flatten([ + for key, val in var.regions : [ + for subnet_key, subnet in val.subnets : merge({ + vnet_key = key + subnet_name = coalesce(subnet.name, "${module.regions_config[key].names.subnet}-${subnet_key}") + nsg_name = "${module.regions_config[key].names.network-security-group}-${subnet_key}" + nsg_rules = lookup(var.network_security_group_rules, subnet_key, []) + create_nsg = coalesce(subnet.create_nsg, true) + address_prefixes = cidrsubnet(val.address_space, subnet.cidr_newbits, subnet.cidr_offset) + }, subnet) # include all the declared key/value pairs for a specific subnet + ] + ]) + # Project the above list into a map with unique keys for consumption in a for_each meta argument + subnets_map = { for subnet in local.subnets_flatlist : subnet.subnet_name => subnet } +} + +/*-------------------------------------------------------------------------------------------------- + Create peering +--------------------------------------------------------------------------------------------------*/ + +module "peering_spoke_hub" { + # loop through regions and only create peering if connect_peering is set to true + for_each = { for key, val in var.regions : key => val if val.connect_peering == true } + + source = "../../../dtos-devops-templates/infrastructure/modules/vnet-peering" + + name = "${module.regions_config[each.key].names.virtual-network}-audit-to-hub-peering" + resource_group_name = azurerm_resource_group.rg_vnet[each.key].name + vnet_name = module.vnet[each.key].vnet.name + remote_vnet_id = data.terraform_remote_state.hub.outputs.vnets_hub[each.key].vnet.id + + allow_virtual_network_access = true + allow_forwarded_traffic = true + allow_gateway_transit = false + + use_remote_gateways = false +} + +module "peering_hub_spoke" { + for_each = { for key, val in var.regions : key => val if val.connect_peering == true } + + providers = { + azurerm = azurerm.hub + } + + source = "../../../dtos-devops-templates/infrastructure/modules/vnet-peering" + + name = "hub-to-${module.regions_config[each.key].names.virtual-network}-audit-peering" + resource_group_name = data.terraform_remote_state.hub.outputs.vnets_hub[each.key].vnet.resource_group_name + vnet_name = data.terraform_remote_state.hub.outputs.vnets_hub[each.key].name + remote_vnet_id = module.vnet[each.key].vnet.id + + allow_virtual_network_access = true + allow_forwarded_traffic = true + allow_gateway_transit = false + + use_remote_gateways = false +} diff --git a/infrastructure/tf-audit/outputs.tf b/infrastructure/tf-audit/outputs.tf new file mode 100644 index 0000000..1ac72f7 --- /dev/null +++ b/infrastructure/tf-audit/outputs.tf @@ -0,0 +1,21 @@ +output "application_insights" { + value = { + name = module.app_insights_audit[local.primary_region].name + resource_group_name = module.app_insights_audit[local.primary_region].resource_group_name + } +} + +output "log_analytics_workspace_id" { + value = { for k, v in module.log_analytics_workspace_audit : k => v.id } +} + +output "storage_account_audit" { + value = { + for k, v in module.storage : k => { + name = v.storage_account_name + id = v.storage_account_id + primary_blob_endpoint_name = v.primary_blob_endpoint_name + containers = v.storage_containers + } + } +} diff --git a/infrastructure/tf-audit/private_link_scoped_service.tf b/infrastructure/tf-audit/private_link_scoped_service.tf new file mode 100644 index 0000000..a8d1c2a --- /dev/null +++ b/infrastructure/tf-audit/private_link_scoped_service.tf @@ -0,0 +1,33 @@ +# Create the private link service for Application Insights +module "private_link_scoped_service_app_insights" { + for_each = var.features.private_endpoints_enabled ? var.regions : {} + + source = "../../../dtos-devops-templates/infrastructure/modules/private-link-scoped-service" + + providers = { + azurerm = azurerm.hub + } + + name = "${module.regions_config[each.key].names.log-analytics-workspace}-ampls-service-app-insights" + resource_group_name = data.terraform_remote_state.hub.outputs.private_endpoint_rg_name[each.key] + + linked_resource_id = module.app_insights_audit[each.key].id + scope_name = data.terraform_remote_state.hub.outputs.azure_monitor_private_link_scope_name +} + +# Create the private link service for Log Analytics +module "private_link_scoped_service_law" { + for_each = var.features.private_endpoints_enabled ? var.regions : {} + + source = "../../../dtos-devops-templates/infrastructure/modules/private-link-scoped-service" + + providers = { + azurerm = azurerm.hub + } + + name = "${module.regions_config[each.key].names.log-analytics-workspace}-ampls-service-law" + resource_group_name = data.terraform_remote_state.hub.outputs.private_endpoint_rg_name[each.key] + + linked_resource_id = module.log_analytics_workspace_audit[each.key].id + scope_name = data.terraform_remote_state.hub.outputs.azure_monitor_private_link_scope_name +} diff --git a/infrastructure/tf-audit/providers.tf b/infrastructure/tf-audit/providers.tf new file mode 100644 index 0000000..35a1065 --- /dev/null +++ b/infrastructure/tf-audit/providers.tf @@ -0,0 +1,28 @@ +terraform { + backend "azurerm" {} + required_version = ">= 1.9.2" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "4.26" + } + azuread = { + source = "hashicorp/azuread" + version = "2.53.1" + } + random = "~> 3.5.1" + } +} + +provider "azurerm" { + subscription_id = var.TARGET_SUBSCRIPTION_ID + features {} +} + +provider "azurerm" { + alias = "hub" + subscription_id = var.HUB_SUBSCRIPTION_ID + features {} +} + +provider "azuread" {} diff --git a/infrastructure/tf-audit/rbac.tf b/infrastructure/tf-audit/rbac.tf new file mode 100644 index 0000000..b3f5d76 --- /dev/null +++ b/infrastructure/tf-audit/rbac.tf @@ -0,0 +1,7 @@ +locals { + rbac_roles_storage = [ + "Storage Account Contributor", + "Storage Blob Data Owner", + "Storage Queue Data Contributor" + ] +} diff --git a/infrastructure/tf-audit/storage.tf b/infrastructure/tf-audit/storage.tf new file mode 100644 index 0000000..dc7c696 --- /dev/null +++ b/infrastructure/tf-audit/storage.tf @@ -0,0 +1,51 @@ +module "storage" { + for_each = local.storage_accounts_map + source = "../../../dtos-devops-templates/infrastructure/modules/storage" + + name = substr("${module.regions_config[each.value.region_key].names.storage-account}${lower(each.value.name_suffix)}", 0, 24) + resource_group_name = azurerm_resource_group.audit[each.value.region_key].name + location = each.value.region_key + + containers = each.value.containers + + log_analytics_workspace_id = module.log_analytics_workspace_audit[local.primary_region].id + monitor_diagnostic_setting_storage_account_enabled_logs = local.monitor_diagnostic_setting_storage_account_enabled_logs + monitor_diagnostic_setting_storage_account_metrics = local.monitor_diagnostic_setting_storage_account_metrics + + account_replication_type = each.value.replication_type + account_tier = each.value.account_tier + public_network_access_enabled = each.value.public_network_access_enabled + + rbac_roles = local.rbac_roles_storage + + # Private Endpoint Configuration if enabled + private_endpoint_properties = var.features.private_endpoints_enabled ? { + private_dns_zone_ids_blob = [data.terraform_remote_state.hub.outputs.private_dns_zones["${each.value.region_key}-storage_blob"].id] + private_dns_zone_ids_queue = [data.terraform_remote_state.hub.outputs.private_dns_zones["${each.value.region_key}-storage_queue"].id] + private_endpoint_enabled = var.features.private_endpoints_enabled + private_endpoint_subnet_id = module.subnets["${module.regions_config[each.value.region_key].names.subnet}-pep"].id + private_endpoint_resource_group_name = azurerm_resource_group.rg_private_endpoints[each.value.region_key].name + private_service_connection_is_manual = var.features.private_service_connection_is_manual + } : null + + tags = var.tags +} + +locals { + storage_accounts_flatlist = flatten([ + for region_key, region_val in var.regions : [ + for storage_key, storage_val in var.storage_accounts : { + name = "${storage_key}-${region_key}" + region_key = region_key + name_suffix = storage_val.name_suffix + replication_type = storage_val.replication_type + account_tier = storage_val.account_tier + public_network_access_enabled = storage_val.public_network_access_enabled + containers = storage_val.containers + } + ] + ]) + + # Project the above list into a map with unique keys for consumption in a for_each meta argument + storage_accounts_map = { for storage in local.storage_accounts_flatlist : storage.name => storage } +} diff --git a/infrastructure/tf-audit/variables.tf b/infrastructure/tf-audit/variables.tf new file mode 100644 index 0000000..530e093 --- /dev/null +++ b/infrastructure/tf-audit/variables.tf @@ -0,0 +1,126 @@ +variable "TARGET_SUBSCRIPTION_ID" { + description = "ID of a subscription to deploy infrastructure" + type = string +} + +variable "HUB_SUBSCRIPTION_ID" { + description = "ID of the subscription hosting the DevOps resources" + type = string +} + +variable "HUB_BACKEND_AZURE_STORAGE_ACCOUNT_NAME" { + description = "The name of the Azure Storage Account for the backend" + type = string +} + +variable "HUB_BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME" { + description = "The name of the container in the Azure Storage Account for the backend" + type = string +} + +variable "HUB_BACKEND_AZURE_STORAGE_ACCOUNT_KEY" { + description = "The name of the Statefile for the hub resources" + type = string +} + +variable "HUB_BACKEND_AZURE_RESOURCE_GROUP_NAME" { + description = "The name of the resource group for the Azure Storage Account" + type = string +} + +variable "application" { + description = "Project/Application code for deployment" + type = string + default = "DToS" +} + +variable "application_full_name" { + description = "Full name of the Project/Application code for deployment" + type = string + default = "DToS" +} + +variable "environment" { + description = "Environment code for deployments" + type = string + default = "DEV" +} + +variable "features" { + description = "Feature flags for the deployment" + type = map(bool) +} + +variable "regions" { + type = map(object({ + address_space = optional(string) + is_primary_region = bool + connect_peering = optional(bool, false) + subnets = optional(map(object({ + cidr_newbits = string + cidr_offset = string + create_nsg = optional(bool, true) # defaults to true + name = optional(string) # Optional name override + delegation_name = optional(string) + service_delegation_name = optional(string) + service_delegation_actions = optional(list(string)) + }))) + })) +} + +variable "app_insights" { + description = "Configuration of the App Insights" + type = object({ + name = optional(string, "cohman") + appinsights_type = optional(string, "web") + }) +} + +variable "law" { + description = "Configuration of the Log Analytics Workspace" + type = object({ + name = optional(string, "cohman") + law_sku = optional(string, "PerGB2018") + retention_days = optional(number, 30) + export_enabled = optional(bool, false) + export_eventhub_key = optional(string, "") + export_table_names = optional(list(string), []) + }) +} + + +variable "network_security_group_rules" { + description = "The network security group rules." + default = {} + type = map(list(object({ + name = string + priority = number + direction = string + access = string + protocol = string + source_port_range = string + destination_port_range = string + source_address_prefix = string + destination_address_prefix = string + }))) +} + +variable "tags" { + description = "Default tags to be applied to resources" + type = map(string) +} + + +variable "storage_accounts" { + description = "Configuration for the Storage Account, currently used for SQL Server audit logs" + type = map(object({ + name_suffix = string + account_tier = optional(string, "Standard") + replication_type = optional(string, "LRS") + public_network_access_enabled = optional(bool, false) + containers = optional(map(object({ + container_name = string + container_access_type = optional(string, "private") + })), {}) + })) +} From bd7f7270d1a6a6eb81dfff65877ca82e3f7ac2e5 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Fri, 23 May 2025 16:34:16 +0100 Subject: [PATCH 02/24] core infra first pass --- .azuredevops/pipelines/cd-infrastructure-dev-core.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml index fe9eae6..a245059 100644 --- a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml +++ b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml @@ -14,7 +14,7 @@ resources: - repository: dtos-devops-templates type: github name: NHSDigital/dtos-devops-templates - ref: 9673ee4ef9770e80d0714c3966a699414b7b43c7 + ref: fix/function-app-acr-is-optional endpoint: NHSDigital variables: From a6bcba55764637e519e94106b5514d3bac497e75 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Fri, 23 May 2025 18:56:28 +0100 Subject: [PATCH 03/24] infra core --- infrastructure/tf-core/app_service_plan.tf | 68 +++ infrastructure/tf-core/config.tf | 21 + infrastructure/tf-core/data.tf | 55 +++ infrastructure/tf-core/diagnostic_settings.tf | 38 ++ .../tf-core/environments/development.tfvars | 231 ++++++++++ .../tf-core/environments/integration.tfvars | 231 ++++++++++ .../tf-core/environments/nft.tfvars | 231 ++++++++++ infrastructure/tf-core/function_app.tf | 127 ++++++ infrastructure/tf-core/key_vault.tf | 32 ++ infrastructure/tf-core/network_routing.tf | 84 ++++ infrastructure/tf-core/networking.tf | 125 ++++++ infrastructure/tf-core/providers.tf | 34 ++ infrastructure/tf-core/rbac.tf | 24 ++ infrastructure/tf-core/sql_server.tf | 58 +++ infrastructure/tf-core/storage.tf | 52 +++ infrastructure/tf-core/variables.tf | 398 ++++++++++++++++++ 16 files changed, 1809 insertions(+) create mode 100644 infrastructure/tf-core/app_service_plan.tf create mode 100644 infrastructure/tf-core/config.tf create mode 100644 infrastructure/tf-core/data.tf create mode 100644 infrastructure/tf-core/diagnostic_settings.tf create mode 100644 infrastructure/tf-core/environments/development.tfvars create mode 100644 infrastructure/tf-core/environments/integration.tfvars create mode 100644 infrastructure/tf-core/environments/nft.tfvars create mode 100644 infrastructure/tf-core/function_app.tf create mode 100644 infrastructure/tf-core/key_vault.tf create mode 100644 infrastructure/tf-core/network_routing.tf create mode 100644 infrastructure/tf-core/networking.tf create mode 100644 infrastructure/tf-core/providers.tf create mode 100644 infrastructure/tf-core/rbac.tf create mode 100644 infrastructure/tf-core/sql_server.tf create mode 100644 infrastructure/tf-core/storage.tf create mode 100644 infrastructure/tf-core/variables.tf diff --git a/infrastructure/tf-core/app_service_plan.tf b/infrastructure/tf-core/app_service_plan.tf new file mode 100644 index 0000000..58a82d5 --- /dev/null +++ b/infrastructure/tf-core/app_service_plan.tf @@ -0,0 +1,68 @@ +locals { + # There are multiple App Service Plans and possibly multiple regions. + # We cannot nest for loops inside a map, so first iterate all permutations of both as a list of objects... + app_service_object_list = flatten([ + for region in keys(var.regions) : [ + for app_service_plan, config in var.app_service_plan.instances : merge( + { + region = region # 1st iterator + app_service_plan = app_service_plan # 2nd iterator + }, + config # the rest of the key/value pairs for a specific app_service_plan + ) + ] + ]) + + # ...then project the list of objects into a map with unique keys (combining the iterators), for consumption by a for_each meta argument + app_service_plans_map = { + for object in local.app_service_object_list : "${object.app_service_plan}-${object.region}" => object + } +} + +module "app-service-plan" { + for_each = local.app_service_plans_map + + source = "../../../dtos-devops-templates/infrastructure/modules/app-service-plan" + + name = "${module.regions_config[each.value.region].names.app-service-plan}-${lower(each.value.app_service_plan)}" + resource_group_name = azurerm_resource_group.core[each.value.region].name + location = each.value.region + + log_analytics_workspace_id = data.terraform_remote_state.audit.outputs.log_analytics_workspace_id[local.primary_region] + monitor_diagnostic_setting_appserviceplan_metrics = local.monitor_diagnostic_setting_appserviceplan_metrics + os_type = lookup(each.value, "os_type", var.app_service_plan.os_type) + sku_name = lookup(each.value, "sku_name", var.app_service_plan.sku_name) + vnet_integration_subnet_id = module.subnets["${module.regions_config[each.value.region].names.subnet}-apps"].id + wildcard_ssl_cert_name = each.value.wildcard_ssl_cert_key + wildcard_ssl_cert_pfx_blob_key_vault_secret_name = each.value.wildcard_ssl_cert_key != null ? data.terraform_remote_state.hub.outputs.certificates[each.value.wildcard_ssl_cert_key].key_vault_certificate[each.value.region].pfx_blob_secret_name : null + wildcard_ssl_cert_pfx_password = each.value.wildcard_ssl_cert_key != null ? data.terraform_remote_state.hub.outputs.certificates[each.value.wildcard_ssl_cert_key].key_vault_certificate[each.value.region].pfx_password : null + wildcard_ssl_cert_key_vault_id = each.value.wildcard_ssl_cert_key != null ? data.terraform_remote_state.hub.outputs.key_vault[each.value.region].key_vault_id : null + + tags = var.tags + + ## autoscale rule + metric = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.metric, var.app_service_plan.autoscale.scaling_rule.metric) : var.app_service_plan.autoscale.scaling_rule.metric + + capacity_min = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.capacity_min, var.app_service_plan.autoscale.scaling_rule.capacity_min) : var.app_service_plan.autoscale.scaling_rule.capacity_min + capacity_max = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.capacity_max, var.app_service_plan.autoscale.scaling_rule.capacity_max) : var.app_service_plan.autoscale.scaling_rule.capacity_max + capacity_def = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.capacity_def, var.app_service_plan.autoscale.scaling_rule.capacity_def) : var.app_service_plan.autoscale.scaling_rule.capacity_def + + time_grain = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.time_grain, var.app_service_plan.autoscale.scaling_rule.time_grain) : var.app_service_plan.autoscale.scaling_rule.time_grain + statistic = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.statistic, var.app_service_plan.autoscale.scaling_rule.statistic) : var.app_service_plan.autoscale.scaling_rule.statistic + time_window = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.time_window, var.app_service_plan.autoscale.scaling_rule.time_window) : var.app_service_plan.autoscale.scaling_rule.time_window + time_aggregation = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.time_aggregation, var.app_service_plan.autoscale.scaling_rule.time_aggregation) : var.app_service_plan.autoscale.scaling_rule.time_aggregation + + inc_operator = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.inc_operator, var.app_service_plan.autoscale.scaling_rule.inc_operator) : var.app_service_plan.autoscale.scaling_rule.inc_operator + inc_threshold = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.inc_threshold, var.app_service_plan.autoscale.scaling_rule.inc_threshold) : var.app_service_plan.autoscale.scaling_rule.inc_threshold + inc_scale_direction = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.inc_scale_direction, var.app_service_plan.autoscale.scaling_rule.inc_scale_direction) : var.app_service_plan.autoscale.scaling_rule.inc_scale_direction + inc_scale_type = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.inc_scale_type, var.app_service_plan.autoscale.scaling_rule.inc_scale_type) : var.app_service_plan.autoscale.scaling_rule.inc_scale_type + inc_scale_value = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.inc_scale_value, var.app_service_plan.autoscale.scaling_rule.inc_scale_value) : var.app_service_plan.autoscale.scaling_rule.inc_scale_value + inc_scale_cooldown = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.inc_scale_cooldown, var.app_service_plan.autoscale.scaling_rule.inc_scale_cooldown) : var.app_service_plan.autoscale.scaling_rule.inc_scale_cooldown + + dec_operator = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.dec_operator, var.app_service_plan.autoscale.scaling_rule.dec_operator) : var.app_service_plan.autoscale.scaling_rule.dec_operator + dec_threshold = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.dec_threshold, var.app_service_plan.autoscale.scaling_rule.dec_threshold) : var.app_service_plan.autoscale.scaling_rule.dec_threshold + dec_scale_direction = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.dec_scale_direction, var.app_service_plan.autoscale.scaling_rule.dec_scale_direction) : var.app_service_plan.autoscale.scaling_rule.dec_scale_direction + dec_scale_type = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.dec_scale_type, var.app_service_plan.autoscale.scaling_rule.dec_scale_type) : var.app_service_plan.autoscale.scaling_rule.dec_scale_type + dec_scale_value = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.dec_scale_value, var.app_service_plan.autoscale.scaling_rule.dec_scale_value) : var.app_service_plan.autoscale.scaling_rule.dec_scale_value + dec_scale_cooldown = each.value.autoscale_override != null ? coalesce(each.value.autoscale_override.scaling_rule.dec_scale_cooldown, var.app_service_plan.autoscale.scaling_rule.dec_scale_cooldown) : var.app_service_plan.autoscale.scaling_rule.dec_scale_cooldown +} diff --git a/infrastructure/tf-core/config.tf b/infrastructure/tf-core/config.tf new file mode 100644 index 0000000..68ac1a0 --- /dev/null +++ b/infrastructure/tf-core/config.tf @@ -0,0 +1,21 @@ +resource "azurerm_resource_group" "core" { + for_each = var.regions + + name = module.regions_config[each.key].names.resource-group + location = each.key + + lifecycle { + ignore_changes = [tags] + } +} + +module "regions_config" { + for_each = var.regions + + source = "../../../dtos-devops-templates/infrastructure/modules/shared-config" + + location = each.key + application = var.application + env = var.environment + tags = var.tags +} diff --git a/infrastructure/tf-core/data.tf b/infrastructure/tf-core/data.tf new file mode 100644 index 0000000..805fc24 --- /dev/null +++ b/infrastructure/tf-core/data.tf @@ -0,0 +1,55 @@ +data "azurerm_client_config" "current" {} + +data "terraform_remote_state" "audit" { + backend = "azurerm" + config = { + subscription_id = var.HUB_SUBSCRIPTION_ID + storage_account_name = var.AUDIT_BACKEND_AZURE_STORAGE_ACCOUNT_NAME + container_name = var.AUDIT_BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME + key = var.AUDIT_BACKEND_AZURE_STORAGE_ACCOUNT_KEY + resource_group_name = var.AUDIT_BACKEND_AZURE_RESOURCE_GROUP_NAME + } +} + +data "terraform_remote_state" "hub" { + backend = "azurerm" + config = { + subscription_id = var.HUB_SUBSCRIPTION_ID + storage_account_name = var.HUB_BACKEND_AZURE_STORAGE_ACCOUNT_NAME + container_name = var.HUB_BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME + key = var.HUB_BACKEND_AZURE_STORAGE_ACCOUNT_KEY + resource_group_name = var.HUB_BACKEND_AZURE_RESOURCE_GROUP_NAME + } +} + +data "azurerm_application_insights" "ai" { + provider = azurerm.audit + + name = data.terraform_remote_state.audit.outputs.application_insights.name + resource_group_name = data.terraform_remote_state.audit.outputs.application_insights.resource_group_name +} + +# Note the following two Networking data look-ups only work becasue the names for the +# resources are effectively the same in both subscriptions (with additional name suffix for Audit RG) +data "azurerm_virtual_network" "vnet_audit" { + for_each = var.regions + + provider = azurerm.audit + + name = module.regions_config[each.key].names.virtual-network + resource_group_name = "${module.regions_config[each.key].names.resource-group}-audit-networking" +} + +data "azurerm_subnet" "subnet_audit_pep" { + for_each = var.regions + + provider = azurerm.audit + + name = "${module.regions_config[each.key].names.subnet}-pep" + resource_group_name = "${module.regions_config[each.key].names.resource-group}-audit-networking" + virtual_network_name = module.regions_config[each.key].names.virtual-network +} + +data "azuread_group" "sql_admin_group" { + display_name = var.sqlserver.sql_admin_group_name +} diff --git a/infrastructure/tf-core/diagnostic_settings.tf b/infrastructure/tf-core/diagnostic_settings.tf new file mode 100644 index 0000000..41804b9 --- /dev/null +++ b/infrastructure/tf-core/diagnostic_settings.tf @@ -0,0 +1,38 @@ +locals { + #APPSERVICEPLAN + monitor_diagnostic_setting_appserviceplan_metrics = ["AllMetrics"] + + #FUNCTIONAPP + monitor_diagnostic_setting_function_app_enabled_logs = ["AppServiceAuthenticationLogs", "FunctionAppLogs"] + monitor_diagnostic_setting_function_app_metrics = ["AllMetrics"] + + # KEYVAULT + monitor_diagnostic_setting_keyvault_enabled_logs = ["AuditEvent", "AzurePolicyEvaluationDetails"] + monitor_diagnostic_setting_keyvault_metrics = ["AllMetrics"] + + # LOG ANALYTICS WORKSPACE + monitor_diagnostic_setting_log_analytics_workspace_enabled_logs = ["SummaryLogs", "Audit"] + monitor_diagnostic_setting_log_analytics_workspace_metrics = ["AllMetrics"] + + #SQL SERVER AND DATABASE + monitor_diagnostic_setting_database_enabled_logs = ["SQLSecurityAuditEvents", "SQLInsights", "QueryStoreWaitStatistics", "Errors", "DatabaseWaitStatistics", "Timeouts"] + monitor_diagnostic_setting_database_metrics = ["Basic", "InstanceAndAppAdvanced", "WorkloadManagement"] + monitor_diagnostic_setting_sql_server_enabled_logs = ["SQLSecurityAuditEvents"] + monitor_diagnostic_setting_sql_server_metrics = ["Basic", "InstanceAndAppAdvanced", "WorkloadManagement"] + + #STORAGE ACCOUNT + monitor_diagnostic_setting_storage_account_enabled_logs = ["StorageWrite", "StorageRead", "StorageDelete"] + monitor_diagnostic_setting_storage_account_metrics = ["Capacity", "Transaction"] + + #SUBNET + monitor_diagnostic_setting_network_security_group_enabled_logs = ["NetworkSecurityGroupEvent", "NetworkSecurityGroupRuleCounter"] + + #VNET + monitor_diagnostic_setting_vnet_enabled_logs = ["VMProtectionAlerts"] + monitor_diagnostic_setting_vnet_metrics = ["AllMetrics"] + + # WEB APP + monitor_diagnostic_setting_linux_web_app_enabled_logs = ["AppServicePlatformLogs"] + monitor_diagnostic_setting_linux_web_app_metrics = ["AllMetrics"] +} + diff --git a/infrastructure/tf-core/environments/development.tfvars b/infrastructure/tf-core/environments/development.tfvars new file mode 100644 index 0000000..7f8b76d --- /dev/null +++ b/infrastructure/tf-core/environments/development.tfvars @@ -0,0 +1,231 @@ +application = "svclyr" +application_full_name = "service-layer" +environment = "DEV" + +features = { + acr_enabled = false + api_management_enabled = false + event_grid_enabled = false + private_endpoints_enabled = true + private_service_connection_is_manual = false + public_network_access_enabled = false +} + +tags = { + Project = "Service-Layer" +} + +regions = { + uksouth = { + is_primary_region = true + address_space = "10.134.0.0/16" + connect_peering = true + subnets = { + apps = { + cidr_newbits = 8 + cidr_offset = 2 + delegation_name = "Microsoft.Web/serverFarms" + service_delegation_name = "Microsoft.Web/serverFarms" + service_delegation_actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + pep = { + cidr_newbits = 8 + cidr_offset = 1 + } + sql = { + cidr_newbits = 8 + cidr_offset = 3 + } + webapps = { + cidr_newbits = 8 + cidr_offset = 4 + delegation_name = "Microsoft.Web/serverFarms" + service_delegation_name = "Microsoft.Web/serverFarms" + service_delegation_actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + pep-dmz = { + cidr_newbits = 8 + cidr_offset = 5 + } + } + } +} + +routes = { + uksouth = { + firewall_policy_priority = 100 + application_rules = [] + nat_rules = [] + network_rules = [ + { + name = "AllowSvclyrToAudit" + priority = 800 + action = "Allow" + rule_name = "SvclyrToAudit" + source_addresses = ["10.134.0.0/16"] + destination_addresses = ["10.135.0.0/16"] + protocols = ["TCP", "UDP"] + destination_ports = ["443"] + }, + { + name = "AllowAuditToSvclyr" + priority = 810 + action = "Allow" + rule_name = "AuditToSvclyr" + source_addresses = ["10.135.0.0/16"] + destination_addresses = ["10.134.0.0/16"] + protocols = ["TCP", "UDP"] + destination_ports = ["443"] + } + ] + route_table_routes_to_audit = [ + { + name = "SvclyrToAudit" + address_prefix = "10.135.0.0/16" + next_hop_type = "VirtualAppliance" + next_hop_in_ip_address = "" # will be populated with the Firewall Private IP address + } + ] + route_table_routes_from_audit = [ + { + name = "AuditToSvclyr" + address_prefix = "10.134.0.0/16" + next_hop_type = "VirtualAppliance" + next_hop_in_ip_address = "" # will be populated with the Firewall Private IP address + } + ] + } +} + +app_service_plan = { + os_type = "Linux" + sku_name = "P2v3" + vnet_integration_enabled = true + + autoscale = { + scaling_rule = { + metric = "MemoryPercentage" + + capacity_min = "1" + capacity_max = "5" + capacity_def = "1" + + time_grain = "PT1M" + statistic = "Average" + time_window = "PT10M" + time_aggregation = "Average" + + inc_operator = "GreaterThan" + inc_threshold = 70 + inc_scale_direction = "Increase" + inc_scale_type = "ChangeCount" + inc_scale_value = 1 + inc_scale_cooldown = "PT5M" + + dec_operator = "LessThan" + dec_threshold = 25 + dec_scale_direction = "Decrease" + dec_scale_type = "ChangeCount" + dec_scale_value = 1 + dec_scale_cooldown = "PT5M" + } + } + + instances = { + Default = {} + # BIAnalyticsDataService = {} + # BIAnalyticsService = {} + # DemographicsService = {} + # EpisodeDataService = {} + # EpisodeIntegrationService = {} + # EpisodeManagementService = {} + # MeshIntegrationService = {} + # ParticipantManagementService = {} + # ReferenceDataService = {} + } +} + +diagnostic_settings = { + metric_enabled = true +} + +function_apps = { + app_service_logs_disk_quota_mb = 35 + app_service_logs_retention_period_days = 7 + always_on = true + docker_env_tag = "development" + docker_img_prefix = "service-layer" + enable_appsrv_storage = "false" + ftps_state = "Disabled" + https_only = true + remote_debugging_enabled = false + storage_uses_managed_identity = null + worker_32bit = false + ip_restriction_default_action = "Deny" + + function_app_config = { + + + + } +} + +function_app_slots = [] + +key_vault = { + disk_encryption = true + soft_del_ret_days = 7 + purge_prot = true + sku_name = "standard" +} + +sqlserver = { + sql_uai_name = "dtos-service-layer-sql-adm" + sql_admin_group_name = "sqlsvr_svclyr_dev_uks_admin" + ad_auth_only = true + auditing_policy_retention_in_days = 30 + security_alert_policy_retention_days = 30 + + server = { + sqlversion = "12.0" + tlsversion = 1.2 + azure_services_access_enabled = true + } + + # parman database + dbs = { + parman = { + db_name_suffix = "service_layer_database" + collation = "SQL_Latin1_General_CP1_CI_AS" + licence_type = "LicenseIncluded" + max_gb = 5 + read_scale = false + sku = "S0" + } + } + + fw_rules = {} +} + +storage_accounts = { + fnapp = { + name_suffix = "fnappstor" + account_tier = "Standard" + replication_type = "LRS" + public_network_access_enabled = false + containers = {} + } + # webapp = { + # name_suffix = "webappstor" + # account_tier = "Standard" + # replication_type = "LRS" + # public_network_access_enabled = true + # blob_properties_delete_retention_policy = 7 + # blob_properties_versioning_enabled = false + # containers = { + # webapp = { + # container_name = "webapp" + # } + # } + # } +} diff --git a/infrastructure/tf-core/environments/integration.tfvars b/infrastructure/tf-core/environments/integration.tfvars new file mode 100644 index 0000000..31a0c6f --- /dev/null +++ b/infrastructure/tf-core/environments/integration.tfvars @@ -0,0 +1,231 @@ +application = "svclyr" +application_full_name = "service-layer" +environment = "INT" + +features = { + acr_enabled = false + api_management_enabled = false + event_grid_enabled = false + private_endpoints_enabled = true + private_service_connection_is_manual = false + public_network_access_enabled = false +} + +tags = { + Project = "Service-Layer" +} + +regions = { + uksouth = { + is_primary_region = true + address_space = "10.138.0.0/16" + connect_peering = true + subnets = { + apps = { + cidr_newbits = 8 + cidr_offset = 2 + delegation_name = "Microsoft.Web/serverFarms" + service_delegation_name = "Microsoft.Web/serverFarms" + service_delegation_actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + pep = { + cidr_newbits = 8 + cidr_offset = 1 + } + sql = { + cidr_newbits = 8 + cidr_offset = 3 + } + webapps = { + cidr_newbits = 8 + cidr_offset = 4 + delegation_name = "Microsoft.Web/serverFarms" + service_delegation_name = "Microsoft.Web/serverFarms" + service_delegation_actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + pep-dmz = { + cidr_newbits = 8 + cidr_offset = 5 + } + } + } +} + +routes = { + uksouth = { + firewall_policy_priority = 100 + application_rules = [] + nat_rules = [] + network_rules = [ + { + name = "AllowSvclyrToAudit" + priority = 800 + action = "Allow" + rule_name = "SvclyrToAudit" + source_addresses = ["10.138.0.0/16"] + destination_addresses = ["10.139.0.0/16"] + protocols = ["TCP", "UDP"] + destination_ports = ["443"] + }, + { + name = "AllowAuditToSvclyr" + priority = 810 + action = "Allow" + rule_name = "AuditToSvclyr" + source_addresses = ["10.139.0.0/16"] + destination_addresses = ["10.138.0.0/16"] + protocols = ["TCP", "UDP"] + destination_ports = ["443"] + } + ] + route_table_routes_to_audit = [ + { + name = "SvclyrToAudit" + address_prefix = "10.139.0.0/16" + next_hop_type = "VirtualAppliance" + next_hop_in_ip_address = "" # will be populated with the Firewall Private IP address + } + ] + route_table_routes_from_audit = [ + { + name = "AuditToSvclyr" + address_prefix = "10.138.0.0/16" + next_hop_type = "VirtualAppliance" + next_hop_in_ip_address = "" # will be populated with the Firewall Private IP address + } + ] + } +} + +app_service_plan = { + os_type = "Linux" + sku_name = "P2v3" + vnet_integration_enabled = true + + autoscale = { + scaling_rule = { + metric = "MemoryPercentage" + + capacity_min = "1" + capacity_max = "5" + capacity_def = "1" + + time_grain = "PT1M" + statistic = "Average" + time_window = "PT10M" + time_aggregation = "Average" + + inc_operator = "GreaterThan" + inc_threshold = 70 + inc_scale_direction = "Increase" + inc_scale_type = "ChangeCount" + inc_scale_value = 1 + inc_scale_cooldown = "PT5M" + + dec_operator = "LessThan" + dec_threshold = 25 + dec_scale_direction = "Decrease" + dec_scale_type = "ChangeCount" + dec_scale_value = 1 + dec_scale_cooldown = "PT5M" + } + } + + instances = { + Default = {} + # BIAnalyticsDataService = {} + # BIAnalyticsService = {} + # DemographicsService = {} + # EpisodeDataService = {} + # EpisodeIntegrationService = {} + # EpisodeManagementService = {} + # MeshIntegrationService = {} + # ParticipantManagementService = {} + # ReferenceDataService = {} + } +} + +diagnostic_settings = { + metric_enabled = true +} + +function_apps = { + app_service_logs_disk_quota_mb = 35 + app_service_logs_retention_period_days = 7 + always_on = true + docker_env_tag = "integration" + docker_img_prefix = "service-layer" + enable_appsrv_storage = "false" + ftps_state = "Disabled" + https_only = true + remote_debugging_enabled = false + storage_uses_managed_identity = null + worker_32bit = false + ip_restriction_default_action = "Deny" + + function_app_config = { + + + + } +} + +function_app_slots = [] + +key_vault = { + disk_encryption = true + soft_del_ret_days = 7 + purge_prot = true + sku_name = "standard" +} + +sqlserver = { + sql_uai_name = "dtos-service-layer-sql-adm" + sql_admin_group_name = "sqlsvr_svclyr_int_uks_admin" + ad_auth_only = true + auditing_policy_retention_in_days = 30 + security_alert_policy_retention_days = 30 + + server = { + sqlversion = "12.0" + tlsversion = 1.2 + azure_services_access_enabled = true + } + + # parman database + dbs = { + parman = { + db_name_suffix = "service_layer_database" + collation = "SQL_Latin1_General_CP1_CI_AS" + licence_type = "LicenseIncluded" + max_gb = 5 + read_scale = false + sku = "S0" + } + } + + fw_rules = {} +} + +storage_accounts = { + fnapp = { + name_suffix = "fnappstor" + account_tier = "Standard" + replication_type = "LRS" + public_network_access_enabled = false + containers = {} + } + # webapp = { + # name_suffix = "webappstor" + # account_tier = "Standard" + # replication_type = "LRS" + # public_network_access_enabled = true + # blob_properties_delete_retention_policy = 7 + # blob_properties_versioning_enabled = false + # containers = { + # webapp = { + # container_name = "webapp" + # } + # } + # } +} diff --git a/infrastructure/tf-core/environments/nft.tfvars b/infrastructure/tf-core/environments/nft.tfvars new file mode 100644 index 0000000..51c5b68 --- /dev/null +++ b/infrastructure/tf-core/environments/nft.tfvars @@ -0,0 +1,231 @@ +application = "svclyr" +application_full_name = "service-layer" +environment = "NFT" + +features = { + acr_enabled = false + api_management_enabled = false + event_grid_enabled = false + private_endpoints_enabled = true + private_service_connection_is_manual = false + public_network_access_enabled = false +} + +tags = { + Project = "Service-Layer" +} + +regions = { + uksouth = { + is_primary_region = true + address_space = "10.136.0.0/16" + connect_peering = true + subnets = { + apps = { + cidr_newbits = 8 + cidr_offset = 2 + delegation_name = "Microsoft.Web/serverFarms" + service_delegation_name = "Microsoft.Web/serverFarms" + service_delegation_actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + pep = { + cidr_newbits = 8 + cidr_offset = 1 + } + sql = { + cidr_newbits = 8 + cidr_offset = 3 + } + webapps = { + cidr_newbits = 8 + cidr_offset = 4 + delegation_name = "Microsoft.Web/serverFarms" + service_delegation_name = "Microsoft.Web/serverFarms" + service_delegation_actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + pep-dmz = { + cidr_newbits = 8 + cidr_offset = 5 + } + } + } +} + +routes = { + uksouth = { + firewall_policy_priority = 100 + application_rules = [] + nat_rules = [] + network_rules = [ + { + name = "AllowSvclyrToAudit" + priority = 800 + action = "Allow" + rule_name = "SvclyrToAudit" + source_addresses = ["10.136.0.0/16"] + destination_addresses = ["10.137.0.0/16"] + protocols = ["TCP", "UDP"] + destination_ports = ["443"] + }, + { + name = "AllowAuditToSvclyr" + priority = 810 + action = "Allow" + rule_name = "AuditToSvclyr" + source_addresses = ["10.137.0.0/16"] + destination_addresses = ["10.136.0.0/16"] + protocols = ["TCP", "UDP"] + destination_ports = ["443"] + } + ] + route_table_routes_to_audit = [ + { + name = "SvclyrToAudit" + address_prefix = "10.137.0.0/16" + next_hop_type = "VirtualAppliance" + next_hop_in_ip_address = "" # will be populated with the Firewall Private IP address + } + ] + route_table_routes_from_audit = [ + { + name = "AuditToSvclyr" + address_prefix = "10.136.0.0/16" + next_hop_type = "VirtualAppliance" + next_hop_in_ip_address = "" # will be populated with the Firewall Private IP address + } + ] + } +} + +app_service_plan = { + os_type = "Linux" + sku_name = "P2v3" + vnet_integration_enabled = true + + autoscale = { + scaling_rule = { + metric = "MemoryPercentage" + + capacity_min = "1" + capacity_max = "5" + capacity_def = "1" + + time_grain = "PT1M" + statistic = "Average" + time_window = "PT10M" + time_aggregation = "Average" + + inc_operator = "GreaterThan" + inc_threshold = 70 + inc_scale_direction = "Increase" + inc_scale_type = "ChangeCount" + inc_scale_value = 1 + inc_scale_cooldown = "PT5M" + + dec_operator = "LessThan" + dec_threshold = 25 + dec_scale_direction = "Decrease" + dec_scale_type = "ChangeCount" + dec_scale_value = 1 + dec_scale_cooldown = "PT5M" + } + } + + instances = { + Default = {} + # BIAnalyticsDataService = {} + # BIAnalyticsService = {} + # DemographicsService = {} + # EpisodeDataService = {} + # EpisodeIntegrationService = {} + # EpisodeManagementService = {} + # MeshIntegrationService = {} + # ParticipantManagementService = {} + # ReferenceDataService = {} + } +} + +diagnostic_settings = { + metric_enabled = true +} + +function_apps = { + app_service_logs_disk_quota_mb = 35 + app_service_logs_retention_period_days = 7 + always_on = true + docker_env_tag = "nft" + docker_img_prefix = "service-layer" + enable_appsrv_storage = "false" + ftps_state = "Disabled" + https_only = true + remote_debugging_enabled = false + storage_uses_managed_identity = null + worker_32bit = false + ip_restriction_default_action = "Deny" + + function_app_config = { + + + + } +} + +function_app_slots = [] + +key_vault = { + disk_encryption = true + soft_del_ret_days = 7 + purge_prot = true + sku_name = "standard" +} + +sqlserver = { + sql_uai_name = "dtos-service-layer-sql-adm" + sql_admin_group_name = "sqlsvr_svclyr_nft_uks_admin" + ad_auth_only = true + auditing_policy_retention_in_days = 30 + security_alert_policy_retention_days = 30 + + server = { + sqlversion = "12.0" + tlsversion = 1.2 + azure_services_access_enabled = true + } + + # parman database + dbs = { + parman = { + db_name_suffix = "service_layer_database" + collation = "SQL_Latin1_General_CP1_CI_AS" + licence_type = "LicenseIncluded" + max_gb = 5 + read_scale = false + sku = "S0" + } + } + + fw_rules = {} +} + +storage_accounts = { + fnapp = { + name_suffix = "fnappstor" + account_tier = "Standard" + replication_type = "LRS" + public_network_access_enabled = false + containers = {} + } + # webapp = { + # name_suffix = "webappstor" + # account_tier = "Standard" + # replication_type = "LRS" + # public_network_access_enabled = true + # blob_properties_delete_retention_policy = 7 + # blob_properties_versioning_enabled = false + # containers = { + # webapp = { + # container_name = "webapp" + # } + # } + # } +} diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf new file mode 100644 index 0000000..9f1013c --- /dev/null +++ b/infrastructure/tf-core/function_app.tf @@ -0,0 +1,127 @@ +module "functionapp" { + for_each = local.function_app_map + + source = "../../../dtos-devops-templates/infrastructure/modules/function-app" + + function_app_name = "${module.regions_config[each.value.region].names.function-app}-${lower(each.value.name_suffix)}" + resource_group_name = azurerm_resource_group.core[each.value.region].name + location = each.value.region + + acr_login_server = "ghcr.io/nhsdigital" + acr_mi_client_id = data.azurerm_user_assigned_identity.acr_mi.client_id + ai_connstring = data.azurerm_application_insights.ai.connection_string + always_on = var.function_apps.always_on + app_service_logs_disk_quota_mb = var.function_apps.app_service_logs_disk_quota_mb + app_service_logs_retention_period_days = var.function_apps.app_service_logs_retention_period_days + app_settings = each.value.app_settings + asp_id = module.app-service-plan["${each.value.app_service_plan_key}-${each.value.region}"].app_service_plan_id + assigned_identity_ids = var.function_apps.cont_registry_use_mi ? [data.azurerm_user_assigned_identity.acr_mi.id] : [] + cont_registry_use_mi = var.function_apps.cont_registry_use_mi + # azuread_group_ids = each.value.azuread_group_ids + function_app_slots = var.function_app_slots + health_check_path = var.function_apps.health_check_path + image_name = "${var.function_apps.docker_img_prefix}-${lower(each.value.name_suffix)}" + image_tag = var.function_apps.docker_env_tag + ip_restriction_default_action = var.function_apps.ip_restriction_default_action + ip_restrictions = each.value.ip_restrictions + log_analytics_workspace_id = data.terraform_remote_state.audit.outputs.log_analytics_workspace_id[local.primary_region] + monitor_diagnostic_setting_function_app_enabled_logs = local.monitor_diagnostic_setting_function_app_enabled_logs + monitor_diagnostic_setting_function_app_metrics = local.monitor_diagnostic_setting_function_app_metrics + + private_endpoint_properties = var.features.private_endpoints_enabled ? { + private_dns_zone_ids = [data.terraform_remote_state.hub.outputs.private_dns_zones["${each.value.region}-app_services"].id] + private_endpoint_enabled = var.features.private_endpoints_enabled + private_endpoint_resource_group_name = azurerm_resource_group.rg_private_endpoints[each.value.region].name + private_endpoint_subnet_id = module.subnets["${module.regions_config[each.value.region].names.subnet}-pep"].id + private_service_connection_is_manual = var.features.private_service_connection_is_manual + } : null + + public_network_access_enabled = length(keys(each.value.ip_restrictions)) > 0 ? true : var.features.public_network_access_enabled + rbac_role_assignments = each.value.rbac_role_assignments + storage_account_access_key = var.function_apps.storage_uses_managed_identity == true ? null : module.storage["fnapp-${each.value.region}"].storage_account_primary_access_key + storage_account_name = module.storage["fnapp-${each.value.region}"].storage_account_name + storage_uses_managed_identity = var.function_apps.storage_uses_managed_identity + vnet_integration_subnet_id = module.subnets["${module.regions_config[each.value.region].names.subnet}-apps"].id + worker_32bit = var.function_apps.worker_32bit + + tags = var.tags +} + + +/* ------------------------------------------------------------------------------------------------- + Local variables used to create the Environment Variables for the Function Apps +-------------------------------------------------------------------------------------------------- */ +locals { + primary_region = [for k, v in var.regions : k if v.is_primary_region][0] + + app_settings_common = { + REMOTE_DEBUGGING_ENABLED = var.function_apps.remote_debugging_enabled + WEBSITES_ENABLE_APP_SERVICE_STORAGE = var.function_apps.enable_appsrv_storage + WEBSITE_PULL_IMAGE_OVER_VNET = var.features.private_endpoints_enabled + FUNCTIONS_WORKER_RUNTIME = "dotnet-isolated" + } + + # There are multiple Function Apps and possibly multiple regions. + # We cannot nest for loops inside a map, so first iterate all permutations of both as a list of objects... + function_app_config_object_list = flatten([ + for region in keys(var.regions) : [ + for function, config in var.function_apps.function_app_config : merge( + { + region = region # 1st iterator + function = function # 2nd iterator + }, + config, # the rest of the key/value pairs for a specific function + { + ip_restriction = config.ip_restrictions + + app_settings = merge( + local.app_settings_common, + config.env_vars.static, + { + for k, v in config.env_vars.from_key_vault : k => "@Microsoft.KeyVault(SecretUri=${module.key_vault[region].key_vault_url}secrets/${v})" + }, + { + for k, v in config.env_vars.local_urls : k => format(v, module.regions_config[region].names["function-app"]) # Function App and Web App have the same naming prefix + }, + length(config.db_connection_string) > 0 ? { + (config.db_connection_string) = "Server=${module.regions_config[region].names.sql-server}.database.windows.net; Authentication=Active Directory Managed Identity; Database=${var.sqlserver.dbs.parman.db_name_suffix}" + } : {} + ) + + # azuread_group_ids = flatten([ + # length(config.db_connection_string) > 0 ? [data.azuread_group.sql_admin_group.object_id] : [], + # ]) + + # These RBAC assignments are for the Function Apps only + rbac_role_assignments = flatten([ + var.key_vault != {} && length(config.env_vars.from_key_vault) > 0 ? [ + for role in local.rbac_roles_key_vault : { + role_definition_name = role + scope = module.key_vault[region].key_vault_id + } + ] : [], + [ + for account in keys(var.storage_accounts) : [ + for role in local.rbac_roles_storage : { + role_definition_name = role + scope = module.storage["${account}-${region}"].storage_account_id + } + ] + ], + [ + for role in local.rbac_roles_database : { + role_definition_name = role + scope = module.azure_sql_server[region].sql_server_id + } + ] + ]) + } + ) + ] + ]) + + # ...then project the list of objects into a map with unique keys (combining the iterators), for consumption by a for_each meta argument + function_app_map = { + for object in local.function_app_config_object_list : "${object.function}-${object.region}" => object + } +} diff --git a/infrastructure/tf-core/key_vault.tf b/infrastructure/tf-core/key_vault.tf new file mode 100644 index 0000000..bb8eac7 --- /dev/null +++ b/infrastructure/tf-core/key_vault.tf @@ -0,0 +1,32 @@ +module "key_vault" { + for_each = var.key_vault != {} ? var.regions : {} + + source = "../../../dtos-devops-templates/infrastructure/modules/key-vault" + + name = module.regions_config[each.key].names.key-vault + resource_group_name = azurerm_resource_group.core[each.key].name + location = each.key + + log_analytics_workspace_id = data.terraform_remote_state.audit.outputs.log_analytics_workspace_id[local.primary_region] + monitor_diagnostic_setting_keyvault_enabled_logs = local.monitor_diagnostic_setting_keyvault_enabled_logs + monitor_diagnostic_setting_keyvault_metrics = local.monitor_diagnostic_setting_keyvault_metrics + metric_enabled = var.diagnostic_settings.metric_enabled + enable_rbac_authorization = true + rbac_roles = local.rbac_roles_key_vault_officers + + disk_encryption = var.key_vault.disk_encryption + soft_delete_retention = var.key_vault.soft_del_ret_days + purge_protection_enabled = var.key_vault.purge_prot + sku_name = var.key_vault.sku_name + + # Private Endpoint Configuration if enabled + private_endpoint_properties = var.features.private_endpoints_enabled ? { + private_dns_zone_ids_keyvault = [data.terraform_remote_state.hub.outputs.private_dns_zones["${each.key}-key_vault"].id] + private_endpoint_enabled = var.features.private_endpoints_enabled + private_endpoint_subnet_id = module.subnets["${module.regions_config[each.key].names.subnet}-pep"].id + private_endpoint_resource_group_name = azurerm_resource_group.rg_private_endpoints[each.key].name + private_service_connection_is_manual = var.features.private_service_connection_is_manual + } : null + + tags = var.tags +} diff --git a/infrastructure/tf-core/network_routing.tf b/infrastructure/tf-core/network_routing.tf new file mode 100644 index 0000000..6ff76ed --- /dev/null +++ b/infrastructure/tf-core/network_routing.tf @@ -0,0 +1,84 @@ +module "firewall_policy_rule_collection_group" { + for_each = var.routes + + source = "../../../dtos-devops-templates/infrastructure/modules/firewall-rule-collection-group" + + name = "${module.regions_config[each.key].names.firewall}-policy-rule-collection-group" + firewall_policy_id = data.terraform_remote_state.hub.outputs.firewall_policy_id[each.key] + priority = each.value.firewall_policy_priority + + network_rule_collection = [ + for rule_key, rule_val in each.value.network_rules : { + name = rule_val.name + priority = rule_val.priority + action = rule_val.action + rule_name = rule_val.rule_name + source_addresses = rule_val.source_addresses + destination_addresses = rule_val.destination_addresses + protocols = rule_val.protocols + destination_ports = rule_val.destination_ports + } + ] + +} + +module "route_table" { + for_each = var.routes + + source = "../../../dtos-devops-templates/infrastructure/modules/route-table" + + name = module.regions_config[each.key].names.route-table + resource_group_name = azurerm_resource_group.rg_vnet[each.key].name + location = each.key + + bgp_route_propagation_enabled = each.value.bgp_route_propagation_enabled + + routes = [ + for route_key, route_val in each.value.route_table_routes_to_audit : { + name = route_val.name + address_prefix = route_val.address_prefix + next_hop_type = route_val.next_hop_type + next_hop_in_ip_address = route_val.next_hop_in_ip_address == "" ? data.terraform_remote_state.hub.outputs.firewall_private_ip_addresses[each.key] : route_val.next_hop_in_ip_address + } + ] + + subnet_ids = [ + module.subnets["${module.regions_config[each.key].names.subnet}-apps"].id, + module.subnets["${module.regions_config[each.key].names.subnet}-pep"].id, + module.subnets["${module.regions_config[each.key].names.subnet}-webapps"].id, + module.subnets["${module.regions_config[each.key].names.subnet}-pep-dmz"].id + ] + + tags = var.tags +} + +module "route_table_audit" { + for_each = var.routes + + providers = { + azurerm = azurerm.audit + } + + source = "../../../dtos-devops-templates/infrastructure/modules/route-table" + + name = module.regions_config[each.key].names.route-table + resource_group_name = "${module.regions_config[each.key].names.resource-group}-audit-networking" + location = each.key + + bgp_route_propagation_enabled = each.value.bgp_route_propagation_enabled + + routes = [ + for route_key, route_val in each.value.route_table_routes_from_audit : { + name = route_val.name + address_prefix = route_val.address_prefix + next_hop_type = route_val.next_hop_type + next_hop_in_ip_address = route_val.next_hop_in_ip_address == "" ? data.terraform_remote_state.hub.outputs.firewall_private_ip_addresses[each.key] : route_val.next_hop_in_ip_address + } + ] + + subnet_ids = [ + data.azurerm_subnet.subnet_audit_pep[each.key].id + ] + + tags = var.tags +} diff --git a/infrastructure/tf-core/networking.tf b/infrastructure/tf-core/networking.tf new file mode 100644 index 0000000..c369ebc --- /dev/null +++ b/infrastructure/tf-core/networking.tf @@ -0,0 +1,125 @@ +resource "azurerm_resource_group" "rg_vnet" { + for_each = var.regions + + name = "${module.regions_config[each.key].names.resource-group}-networking" + location = each.key +} + +resource "azurerm_resource_group" "rg_private_endpoints" { + for_each = var.features.private_endpoints_enabled ? var.regions : {} + + name = "${module.regions_config[each.key].names.resource-group}-private-endpoints" + location = each.key +} + +module "vnet" { + for_each = var.regions + + source = "../../../dtos-devops-templates/infrastructure/modules/vnet" + + name = module.regions_config[each.key].names.virtual-network + resource_group_name = azurerm_resource_group.rg_vnet[each.key].name + location = each.key + vnet_address_space = each.value.address_space + + log_analytics_workspace_id = data.terraform_remote_state.audit.outputs.log_analytics_workspace_id[local.primary_region] + monitor_diagnostic_setting_vnet_enabled_logs = local.monitor_diagnostic_setting_vnet_enabled_logs + monitor_diagnostic_setting_vnet_metrics = local.monitor_diagnostic_setting_vnet_metrics + + dns_servers = [data.terraform_remote_state.hub.outputs.private_dns_resolver_inbound_ips[each.key].private_dns_resolver_ip] + + tags = var.tags +} + +/*-------------------------------------------------------------------------------------------------- + Create Subnets +--------------------------------------------------------------------------------------------------*/ + +locals { + # Expand a flattened list of objects for all subnets (allows nested for loops) + subnets_flatlist = flatten([ + for key, val in var.regions : [ + for subnet_key, subnet in val.subnets : merge({ + vnet_key = key + subnet_name = coalesce(subnet.name, "${module.regions_config[key].names.subnet}-${subnet_key}") + nsg_name = "${module.regions_config[key].names.network-security-group}-${subnet_key}" + nsg_rules = lookup(var.network_security_group_rules, subnet_key, []) + create_nsg = coalesce(subnet.create_nsg, true) + address_prefixes = cidrsubnet(val.address_space, subnet.cidr_newbits, subnet.cidr_offset) + }, subnet) # include all the declared key/value pairs for a specific subnet + ] + ]) + # Project the above list into a map with unique keys for consumption in a for_each meta argument + subnets_map = { for subnet in local.subnets_flatlist : subnet.subnet_name => subnet } +} + +module "subnets" { + for_each = local.subnets_map + + source = "../../../dtos-devops-templates/infrastructure/modules/subnet" + + name = each.value.subnet_name + location = module.vnet[each.value.vnet_key].vnet.location + network_security_group_name = each.value.nsg_name + network_security_group_nsg_rules = each.value.nsg_rules + create_nsg = each.value.create_nsg + resource_group_name = module.vnet[each.value.vnet_key].vnet.resource_group_name + vnet_name = module.vnet[each.value.vnet_key].name + address_prefixes = [each.value.address_prefixes] + default_outbound_access_enabled = true + private_endpoint_network_policies = "Disabled" # Default as per compliance requirements + + log_analytics_workspace_id = data.terraform_remote_state.audit.outputs.log_analytics_workspace_id[local.primary_region] + monitor_diagnostic_setting_network_security_group_enabled_logs = local.monitor_diagnostic_setting_network_security_group_enabled_logs + + delegation_name = each.value.delegation_name != null ? each.value.delegation_name : "" + service_delegation_name = each.value.service_delegation_name != null ? each.value.service_delegation_name : "" + service_delegation_actions = each.value.service_delegation_actions != null ? each.value.service_delegation_actions : [] + + tags = var.tags +} + + + +/*-------------------------------------------------------------------------------------------------- + Create peering +--------------------------------------------------------------------------------------------------*/ + +module "peering_spoke_hub" { + # loop through regions and only create peering if connect_peering is set to true + for_each = { for key, val in var.regions : key => val if val.connect_peering == true } + + source = "../../../dtos-devops-templates/infrastructure/modules/vnet-peering" + + name = "${module.regions_config[each.key].names.virtual-network}-to-hub-peering" + resource_group_name = azurerm_resource_group.rg_vnet[each.key].name + vnet_name = module.vnet[each.key].vnet.name + remote_vnet_id = data.terraform_remote_state.hub.outputs.vnets_hub[each.key].vnet.id + + allow_virtual_network_access = true + allow_forwarded_traffic = true + allow_gateway_transit = false + + use_remote_gateways = false +} + +module "peering_hub_spoke" { + for_each = { for key, val in var.regions : key => val if val.connect_peering == true } + + providers = { + azurerm = azurerm.hub + } + + source = "../../../dtos-devops-templates/infrastructure/modules/vnet-peering" + + name = "hub-to-${module.regions_config[each.key].names.virtual-network}-peering" + resource_group_name = data.terraform_remote_state.hub.outputs.vnets_hub[each.key].vnet.resource_group_name + vnet_name = data.terraform_remote_state.hub.outputs.vnets_hub[each.key].name + remote_vnet_id = module.vnet[each.key].vnet.id + + allow_virtual_network_access = true + allow_forwarded_traffic = true + allow_gateway_transit = false + + use_remote_gateways = false +} diff --git a/infrastructure/tf-core/providers.tf b/infrastructure/tf-core/providers.tf new file mode 100644 index 0000000..451f0af --- /dev/null +++ b/infrastructure/tf-core/providers.tf @@ -0,0 +1,34 @@ +terraform { + backend "azurerm" {} + required_version = ">= 1.9.2" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "4.26" + } + azuread = { + source = "hashicorp/azuread" + version = "2.53.1" + } + random = "~> 3.5.1" + } +} + +provider "azurerm" { + subscription_id = var.TARGET_SUBSCRIPTION_ID + features {} +} + +provider "azurerm" { + alias = "audit" + subscription_id = var.AUDIT_SUBSCRIPTION_ID + features {} +} + +provider "azurerm" { + alias = "hub" + subscription_id = var.HUB_SUBSCRIPTION_ID + features {} +} + +provider "azuread" {} diff --git a/infrastructure/tf-core/rbac.tf b/infrastructure/tf-core/rbac.tf new file mode 100644 index 0000000..433cdcc --- /dev/null +++ b/infrastructure/tf-core/rbac.tf @@ -0,0 +1,24 @@ +locals { + + rbac_roles_key_vault_officers = [ + "Key Vault Certificates Officer", + "Key Vault Crypto Officer", + "Key Vault Secrets Officer" + ] + + rbac_roles_key_vault = [ + "Key Vault Certificate User", + "Key Vault Crypto User", + "Key Vault Secrets User" + ] + + rbac_roles_storage = [ + "Storage Account Contributor", + "Storage Blob Data Owner", + "Storage Queue Data Contributor" + ] + + rbac_roles_database = [ + "Contributor" + ] +} diff --git a/infrastructure/tf-core/sql_server.tf b/infrastructure/tf-core/sql_server.tf new file mode 100644 index 0000000..314bf7a --- /dev/null +++ b/infrastructure/tf-core/sql_server.tf @@ -0,0 +1,58 @@ +module "azure_sql_server" { + for_each = var.sqlserver != {} ? var.regions : {} + # for_each = var.sqlserver + + source = "../../../dtos-devops-templates/infrastructure/modules/sql-server" + + # Azure SQL Server + name = module.regions_config[each.key].names.sql-server + resource_group_name = azurerm_resource_group.core[each.key].name + location = each.key + + sqlversion = var.sqlserver.server.sqlversion + tlsver = var.sqlserver.server.tlsversion + kv_id = module.key_vault[each.key].key_vault_id + + # Diagnostic Settings + log_analytics_workspace_id = data.terraform_remote_state.audit.outputs.log_analytics_workspace_id[local.primary_region] + primary_blob_endpoint_name = data.terraform_remote_state.audit.outputs.storage_account_audit["sqllogs-${local.primary_region}"].primary_blob_endpoint_name + storage_account_name = data.terraform_remote_state.audit.outputs.storage_account_audit["sqllogs-${local.primary_region}"].name + storage_account_id = data.terraform_remote_state.audit.outputs.storage_account_audit["sqllogs-${local.primary_region}"].id + storage_container_id = data.terraform_remote_state.audit.outputs.storage_account_audit["sqllogs-${local.primary_region}"].containers["vulnerability-assessment"].id + monitor_diagnostic_setting_database_enabled_logs = local.monitor_diagnostic_setting_database_enabled_logs + monitor_diagnostic_setting_database_metrics = local.monitor_diagnostic_setting_database_metrics + monitor_diagnostic_setting_sql_server_enabled_logs = local.monitor_diagnostic_setting_sql_server_enabled_logs + monitor_diagnostic_setting_sql_server_metrics = local.monitor_diagnostic_setting_sql_server_metrics + log_monitoring_enabled = true + + sql_server_alert_policy_state = "Enabled" + + sql_uai_name = var.sqlserver.sql_uai_name + sql_admin_group_name = var.sqlserver.sql_admin_group_name + sql_admin_object_id = data.azuread_group.sql_admin_group.object_id + ad_auth_only = var.sqlserver.ad_auth_only + security_alert_policy_retention_days = var.sqlserver.security_alert_policy_retention_days + auditing_policy_retention_in_days = var.sqlserver.auditing_policy_retention_in_days + + # Default database + db_name_suffix = var.sqlserver.dbs.parman.db_name_suffix + collation = var.sqlserver.dbs.parman.collation + licence_type = var.sqlserver.dbs.parman.licence_type + max_gb = var.sqlserver.dbs.parman.max_gb + read_scale = var.sqlserver.dbs.parman.read_scale + sku = var.sqlserver.dbs.parman.sku + + # FW Rules + firewall_rules = var.sqlserver.fw_rules + + # Private Endpoint Configuration if enabled + private_endpoint_properties = var.features.private_endpoints_enabled ? { + private_dns_zone_ids_sql = [data.terraform_remote_state.hub.outputs.private_dns_zones["${each.key}-azure_sql"].id] + private_endpoint_enabled = var.features.private_endpoints_enabled + private_endpoint_subnet_id = module.subnets["${module.regions_config[each.key].names.subnet}-pep"].id + private_endpoint_resource_group_name = azurerm_resource_group.rg_private_endpoints[each.key].name + private_service_connection_is_manual = var.features.private_service_connection_is_manual + } : null + + tags = var.tags +} diff --git a/infrastructure/tf-core/storage.tf b/infrastructure/tf-core/storage.tf new file mode 100644 index 0000000..9ef76b8 --- /dev/null +++ b/infrastructure/tf-core/storage.tf @@ -0,0 +1,52 @@ +module "storage" { + for_each = local.storage_accounts_map + + source = "../../../dtos-devops-templates/infrastructure/modules/storage" + + name = substr("${module.regions_config[each.value.region_key].names.storage-account}${lower(each.value.name_suffix)}", 0, 24) + resource_group_name = azurerm_resource_group.core[each.value.region_key].name + location = each.value.region_key + + containers = each.value.containers + + log_analytics_workspace_id = data.terraform_remote_state.audit.outputs.log_analytics_workspace_id[local.primary_region] + monitor_diagnostic_setting_storage_account_enabled_logs = local.monitor_diagnostic_setting_storage_account_enabled_logs + monitor_diagnostic_setting_storage_account_metrics = local.monitor_diagnostic_setting_storage_account_metrics + + account_replication_type = each.value.replication_type + account_tier = each.value.account_tier + public_network_access_enabled = each.value.public_network_access_enabled + + rbac_roles = local.rbac_roles_storage + + # Private Endpoint Configuration if enabled + private_endpoint_properties = var.features.private_endpoints_enabled ? { + private_dns_zone_ids_blob = [data.terraform_remote_state.hub.outputs.private_dns_zones["${each.value.region_key}-storage_blob"].id] + private_dns_zone_ids_queue = [data.terraform_remote_state.hub.outputs.private_dns_zones["${each.value.region_key}-storage_queue"].id] + private_endpoint_enabled = var.features.private_endpoints_enabled + private_endpoint_subnet_id = module.subnets["${module.regions_config[each.value.region_key].names.subnet}-pep"].id + private_endpoint_resource_group_name = azurerm_resource_group.rg_private_endpoints[each.value.region_key].name + private_service_connection_is_manual = var.features.private_service_connection_is_manual + } : null + + tags = var.tags +} + +locals { + storage_accounts_flatlist = flatten([ + for region_key, region_val in var.regions : [ + for storage_key, storage_val in var.storage_accounts : { + name = "${storage_key}-${region_key}" + region_key = region_key + name_suffix = storage_val.name_suffix + replication_type = storage_val.replication_type + account_tier = storage_val.account_tier + public_network_access_enabled = storage_val.public_network_access_enabled + containers = storage_val.containers + } + ] + ]) + + # Project the above list into a map with unique keys for consumption in a for_each meta argument + storage_accounts_map = { for storage in local.storage_accounts_flatlist : storage.name => storage } +} diff --git a/infrastructure/tf-core/variables.tf b/infrastructure/tf-core/variables.tf new file mode 100644 index 0000000..d89edd9 --- /dev/null +++ b/infrastructure/tf-core/variables.tf @@ -0,0 +1,398 @@ +variable "AUDIT_BACKEND_AZURE_STORAGE_ACCOUNT_NAME" { + description = "The name of the Azure Storage Account for the audit backend" + type = string +} + +variable "AUDIT_BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME" { + description = "The name of the container in the Audit Azure Storage Account for the backend" + type = string +} + +variable "AUDIT_BACKEND_AZURE_RESOURCE_GROUP_NAME" { + description = "The name of the audit resource group for the Azure Storage Account" + type = string +} + +variable "AUDIT_BACKEND_AZURE_STORAGE_ACCOUNT_KEY" { + description = "The name of the audit resource group for the Azure Storage Account" + type = string +} + +variable "TARGET_SUBSCRIPTION_ID" { + description = "ID of a subscription to deploy infrastructure" + type = string +} + +variable "AUDIT_SUBSCRIPTION_ID" { + description = "ID of the Audit subscription to deploy infrastructure" + type = string +} + +variable "HUB_SUBSCRIPTION_ID" { + description = "ID of the subscription hosting the DevOps resources" + type = string +} + +variable "HUB_BACKEND_AZURE_STORAGE_ACCOUNT_NAME" { + description = "The name of the Azure Storage Account for the backend" + type = string +} + +variable "HUB_BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME" { + description = "The name of the container in the Azure Storage Account for the backend" + type = string +} + +variable "HUB_BACKEND_AZURE_STORAGE_ACCOUNT_KEY" { + description = "The name of the Statefile for the hub resources" + type = string +} + +variable "HUB_BACKEND_AZURE_RESOURCE_GROUP_NAME" { + description = "The name of the resource group for the Azure Storage Account" + type = string +} + +variable "application" { + description = "Project/Application code for deployment" + type = string + default = "DToS" +} + +variable "application_full_name" { + description = "Full name of the Project/Application code for deployment" + type = string + default = "DToS" +} + +variable "app_service_plan" { + description = "Configuration for the app service plan" + type = object({ + sku_name = optional(string, "P2v3") + os_type = optional(string, "Linux") + vnet_integration_enabled = optional(bool, false) + + autoscale = object({ + scaling_rule = object({ + metric = optional(string) + capacity_min = optional(string) + capacity_max = optional(string) + capacity_def = optional(string) + time_grain = optional(string) + statistic = optional(string) + time_window = optional(string) + time_aggregation = optional(string) + inc_operator = optional(string) + inc_threshold = optional(number) + inc_scale_direction = optional(string) + inc_scale_type = optional(string) + inc_scale_value = optional(number) + inc_scale_cooldown = optional(string) + dec_operator = optional(string) + dec_threshold = optional(number) + dec_scale_direction = optional(string) + dec_scale_type = optional(string) + dec_scale_value = optional(number) + dec_scale_cooldown = optional(string) + }) + }) + + instances = map(object({ + autoscale_override = optional(object({ + scaling_rule = object({ + metric = optional(string) + capacity_min = optional(string) + capacity_max = optional(string) + capacity_def = optional(string) + time_grain = optional(string) + statistic = optional(string) + time_window = optional(string) + time_aggregation = optional(string) + inc_operator = optional(string) + inc_threshold = optional(number) + inc_scale_direction = optional(string) + inc_scale_type = optional(string) + inc_scale_value = optional(number) + inc_scale_cooldown = optional(string) + dec_operator = optional(string) + dec_threshold = optional(number) + dec_scale_direction = optional(string) + dec_scale_type = optional(string) + dec_scale_value = optional(number) + dec_scale_cooldown = optional(string) + }) + })) + wildcard_ssl_cert_key = optional(string, null) + })) + }) +} + +variable "diagnostic_settings" { + description = "Configuration for the diagnostic settings" + type = object({ + metric_enabled = optional(bool, false) + }) +} + +variable "environment" { + description = "Environment code for deployments" + type = string + default = "DEV" +} + +variable "features" { + description = "Feature flags for the deployment" + type = map(bool) +} + +variable "function_apps" { + description = "Configuration for function apps" + type = object({ + always_on = bool + app_service_logs_disk_quota_mb = optional(number) + app_service_logs_retention_period_days = optional(number) + docker_env_tag = string + docker_img_prefix = string + enable_appsrv_storage = bool + ftps_state = string + health_check_path = optional(string) + https_only = bool + ip_restriction_default_action = optional(string, "Deny") + remote_debugging_enabled = bool + storage_uses_managed_identity = bool + worker_32bit = bool + slots = optional(map(object({ + name = string + slot_enabled = optional(bool, false) + }))) + function_app_config = map(object({ + name_suffix = string + function_endpoint_name = string + app_service_plan_key = string + storage_account_env_var_name = optional(string, "") + storage_containers = optional(list(object + ({ + env_var_name = string + container_name = string + })), []) + db_connection_string = optional(string, "") + azuread_group_id = optional(list(string)) + event_grid_topic_producer = optional(string, "") + key_vault_url = optional(string, "") + env_vars = optional(object({ + static = optional(map(string), {}) + from_key_vault = optional(map(string), {}) + local_urls = optional(map(string), {}) + }), {}) + ip_restrictions = optional(map(object({ + headers = optional(list(object({ + x_azure_fdid = optional(list(string)) + x_fd_health_probe = optional(list(string)) + x_forwarded_for = optional(list(string)) + x_forwarded_host = optional(list(string)) + })), []) + ip_address = optional(string) + name = optional(string) + priority = optional(number) + action = optional(string) + service_tag = optional(string) + virtual_network_subnet_id = optional(string) + })), {}) + })) + }) +} + +variable "function_app_slots" { + description = "function app slots" + type = list(object({ + function_app_slots_name = optional(string, "staging") + function_app_slot_enabled = optional(bool, false) + })) +} + +variable "key_vault" { + description = "Configuration for the key vault" + type = object({ + disk_encryption = optional(bool, true) + soft_del_ret_days = optional(number, 7) + purge_prot = optional(bool, false) + sku_name = optional(string, "standard") + }) +} + +variable "network_security_group_rules" { + description = "The network security group rules." + default = {} + type = map(list(object({ + name = string + priority = number + direction = string + access = string + protocol = string + source_port_range = string + destination_port_range = string + source_address_prefix = string + destination_address_prefix = string + }))) +} + +/* + application_rule_collection = [ + { + name = "example-application-rule-collection-1" + priority = 600 + action = "Allow" + rule_name = "example-rule-1" + protocols = [ + { + type = "Http" + port = 80 + }, + { + type = "Https" + port = 443 + } + ] + source_addresses = ["0.0.0.0/0"] + destination_fqdns = ["example.com"] + }, +*/ + +variable "regions" { + type = map(object({ + address_space = optional(string) + is_primary_region = bool + connect_peering = optional(bool, false) + subnets = optional(map(object({ + cidr_newbits = string + cidr_offset = string + create_nsg = optional(bool, true) # defaults to true + name = optional(string) # Optional name override + delegation_name = optional(string) + service_delegation_name = optional(string) + service_delegation_actions = optional(list(string)) + }))) + })) +} + +variable "routes" { + description = "Routes configuration for different regions" + type = map(object({ + bgp_route_propagation_enabled = optional(bool, false) + firewall_policy_priority = number + application_rules = list(object({ + name = optional(string) + priority = optional(number) + action = optional(string) + rule_name = optional(string) + protocols = list(object({ + type = optional(string) + port = optional(number) + })) + source_addresses = optional(list(string)) + destination_fqdns = list(string) + })) + nat_rules = list(object({ + name = optional(string) + priority = optional(number) + action = optional(string) + rule_name = optional(string) + protocols = list(string) + source_addresses = list(string) + destination_address = optional(string) + destination_ports = list(string) + translated_address = optional(string) + translated_port = optional(string) + })) + network_rules = list(object({ + name = optional(string) + priority = optional(number) + action = optional(string) + rule_name = optional(string) + source_addresses = optional(list(string)) + destination_addresses = optional(list(string)) + protocols = optional(list(string)) + destination_ports = optional(list(string)) + })) + route_table_routes_to_audit = list(object({ + name = optional(string) + address_prefix = optional(string) + next_hop_type = optional(string) + next_hop_in_ip_address = optional(string) + })) + route_table_routes_from_audit = list(object({ + name = optional(string) + address_prefix = optional(string) + next_hop_type = optional(string) + next_hop_in_ip_address = optional(string) + })) + })) + default = {} +} + +variable "sqlserver" { + description = "Configuration for the Azure MSSQL server instance and a default database " + type = object({ + + sql_uai_name = optional(string) + sql_admin_group_name = optional(string) + ad_auth_only = optional(bool) + auditing_policy_retention_in_days = optional(number) + security_alert_policy_retention_days = optional(number) + + # Server Instance + server = optional(object({ + sqlversion = optional(string, "12.0") + tlsversion = optional(number, 1.2) + azure_services_access_enabled = optional(bool, true) + }), {}) + + # Database + dbs = optional(map(object({ + db_name_suffix = optional(string, "parman") + collation = optional(string, "SQL_Latin1_General_CP1_CI_AS") + licence_type = optional(string, "LicenseIncluded") + max_gb = optional(number, 5) + read_scale = optional(bool, false) + sku = optional(string, "S0") + })), {}) + + # FW Rules + fw_rules = optional(map(object({ + fw_rule_name = string + start_ip = string + end_ip = string + })), {}) + }) +} + +variable "storage_accounts" { + description = "Configuration for the Storage Account, currently used for Function Apps" + type = map(object({ + name_suffix = string + account_tier = optional(string, "Standard") + replication_type = optional(string, "LRS") + public_network_access_enabled = optional(bool, false) + containers = optional(map(object({ + container_name = string + container_access_type = optional(string, "private") + })), {}) + })) +} + +variable "tags" { + description = "Default tags to be applied to resources" + type = map(string) +} + +variable "wildcard_ssl_cert_key_vault_secret_id" { + type = string + description = "Wildcard SSL certificate Key Vault secret id, for App Services Custom Domain binding." + default = null +} + +variable "wildcard_ssl_cert_key_vault_id" { + type = string + description = "Wildcard SSL certificate Key Vault id, needed if the Key Vault is in a different subscription." + default = null +} From 3842c50f784ff5d9cdada5ac1136bc59307c4e7c Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Tue, 27 May 2025 18:46:58 +0100 Subject: [PATCH 04/24] test CI to ghcr.io --- .github/workflows/cicd-1-pull-request.yaml | 4 ++-- infrastructure/tf-core/function_app.tf | 1 - src/ServiceLayer.API/delme.txt | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 src/ServiceLayer.API/delme.txt diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index 6de28c6..0ed2fee 100644 --- a/.github/workflows/cicd-1-pull-request.yaml +++ b/.github/workflows/cicd-1-pull-request.yaml @@ -103,10 +103,10 @@ jobs: build-image-stage: # Recommended maximum execution time is 3 minutes name: Image build stage needs: [metadata, commit-stage, test-stage] - uses: NHSDigital/dtos-devops-templates/.github/workflows/stage-3-build-images.yaml@main + uses: NHSDigital/dtos-devops-templates/.github/workflows/stage-3-build.yaml@fix/function-app-acr-is-optional if: needs.metadata.outputs.does_pull_request_exist == 'true' || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')) with: - docker_compose_file: ./compose.yaml + docker_compose_file_csv_list: compose.yaml excluded_containers_csv_list: azurite,azurite-setup,sql-database,database-setup,db environment_tag: ${{ needs.metadata.outputs.environment_tag }} function_app_source_code_path: src diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf index 9f1013c..254d0f3 100644 --- a/infrastructure/tf-core/function_app.tf +++ b/infrastructure/tf-core/function_app.tf @@ -8,7 +8,6 @@ module "functionapp" { location = each.value.region acr_login_server = "ghcr.io/nhsdigital" - acr_mi_client_id = data.azurerm_user_assigned_identity.acr_mi.client_id ai_connstring = data.azurerm_application_insights.ai.connection_string always_on = var.function_apps.always_on app_service_logs_disk_quota_mb = var.function_apps.app_service_logs_disk_quota_mb diff --git a/src/ServiceLayer.API/delme.txt b/src/ServiceLayer.API/delme.txt new file mode 100644 index 0000000..308ec1d --- /dev/null +++ b/src/ServiceLayer.API/delme.txt @@ -0,0 +1 @@ +# comment From 66f0b01c97c9812e63faccc086a0b9c55b49799f Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Tue, 27 May 2025 18:58:51 +0100 Subject: [PATCH 05/24] test --- src/ServiceLayer.API/delme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceLayer.API/delme.txt b/src/ServiceLayer.API/delme.txt index 308ec1d..39079e0 100644 --- a/src/ServiceLayer.API/delme.txt +++ b/src/ServiceLayer.API/delme.txt @@ -1 +1 @@ -# comment +# comment2 From 321b09be4c41eb6366d0a113293ab08cd5902ff3 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Tue, 27 May 2025 19:07:28 +0100 Subject: [PATCH 06/24] test --- src/ServiceLayer.API/delme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceLayer.API/delme.txt b/src/ServiceLayer.API/delme.txt index 39079e0..e558797 100644 --- a/src/ServiceLayer.API/delme.txt +++ b/src/ServiceLayer.API/delme.txt @@ -1 +1 @@ -# comment2 +# comment3 From 45334e604590515415398eb11e50aa739e091824 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Tue, 27 May 2025 19:27:22 +0100 Subject: [PATCH 07/24] test --- src/ServiceLayer.API/delme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceLayer.API/delme.txt b/src/ServiceLayer.API/delme.txt index e558797..39d7316 100644 --- a/src/ServiceLayer.API/delme.txt +++ b/src/ServiceLayer.API/delme.txt @@ -1 +1 @@ -# comment3 +# comment4 From 300e3d2b54a31298c33e0cb36b2f655bff220e7c Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 12:04:31 +0100 Subject: [PATCH 08/24] templates repo path for new GitHub Actions --- .github/workflows/cicd-1-pull-request.yaml | 4 ++-- compose.yaml | 2 +- src/ServiceLayer.API/delme.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index 0ed2fee..fe2497d 100644 --- a/.github/workflows/cicd-1-pull-request.yaml +++ b/.github/workflows/cicd-1-pull-request.yaml @@ -5,9 +5,9 @@ name: "CI/CD pull request" on: push: branches: - - "**" + - main pull_request: - types: [opened, reopened] + types: [opened, reopened, synchronize] jobs: diff --git a/compose.yaml b/compose.yaml index 4663f36..777dc8b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ services: api: container_name: "api" build: - context: ./Src + context: ./src dockerfile: ServiceLayer.API/Dockerfile platform: linux/amd64 restart: always diff --git a/src/ServiceLayer.API/delme.txt b/src/ServiceLayer.API/delme.txt index 39d7316..cca9db5 100644 --- a/src/ServiceLayer.API/delme.txt +++ b/src/ServiceLayer.API/delme.txt @@ -1 +1 @@ -# comment4 +# comment5 From ee012dea95311527a62c7b2b02845bc113ab5b54 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 16:58:01 +0100 Subject: [PATCH 09/24] test-run --- src/ServiceLayer.API/delme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceLayer.API/delme.txt b/src/ServiceLayer.API/delme.txt index cca9db5..3122921 100644 --- a/src/ServiceLayer.API/delme.txt +++ b/src/ServiceLayer.API/delme.txt @@ -1 +1 @@ -# comment5 +# comment6 From 6273d97681c7805133f6b9883d732173adb4d724 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 19:58:56 +0100 Subject: [PATCH 10/24] initial function app config --- .../tf-core/environments/development.tfvars | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/infrastructure/tf-core/environments/development.tfvars b/infrastructure/tf-core/environments/development.tfvars index 7f8b76d..5a35324 100644 --- a/infrastructure/tf-core/environments/development.tfvars +++ b/infrastructure/tf-core/environments/development.tfvars @@ -165,8 +165,27 @@ function_apps = { function_app_config = { - - + ServiceLayer = { + name_suffix = "service-layer-api" + function_endpoint_name = "ServiceLayer" + app_service_plan_key = "Default" + db_connection_string = "DatabaseConnectionString" + env_vars = { + static = { + MeshSharedKey = "TestKey" + NbssMailboxId = "X26ABC1" + } + from_key_vault = { + # env_var_name = "key_vault_secret_name" + # FLAGSMITH_SERVER_SIDE_ENVIRONMENT_KEY = "flagsmith-server-side-environment-key" + # MeshPassword = "MeshPassword" + } + local_urls = { + # %s becomes the environment and region prefix (e.g. dev-uks) + # MeshApiBaseUrl = "https://%s-messageexchange.azurewebsites.net" + } + } + } } } From e9014000b01ef490364fbc9a1347e146b67899cf Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 20:07:22 +0100 Subject: [PATCH 11/24] initial function app config --- infrastructure/tf-core/function_app.tf | 2 -- 1 file changed, 2 deletions(-) diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf index 254d0f3..33c084a 100644 --- a/infrastructure/tf-core/function_app.tf +++ b/infrastructure/tf-core/function_app.tf @@ -14,8 +14,6 @@ module "functionapp" { app_service_logs_retention_period_days = var.function_apps.app_service_logs_retention_period_days app_settings = each.value.app_settings asp_id = module.app-service-plan["${each.value.app_service_plan_key}-${each.value.region}"].app_service_plan_id - assigned_identity_ids = var.function_apps.cont_registry_use_mi ? [data.azurerm_user_assigned_identity.acr_mi.id] : [] - cont_registry_use_mi = var.function_apps.cont_registry_use_mi # azuread_group_ids = each.value.azuread_group_ids function_app_slots = var.function_app_slots health_check_path = var.function_apps.health_check_path From b40bd0d1eea5a8816d656e3e984e7d73993bb882 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 20:10:11 +0100 Subject: [PATCH 12/24] initial function app config --- infrastructure/tf-core/function_app.tf | 2 ++ infrastructure/tf-core/variables.tf | 1 + 2 files changed, 3 insertions(+) diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf index 33c084a..254d0f3 100644 --- a/infrastructure/tf-core/function_app.tf +++ b/infrastructure/tf-core/function_app.tf @@ -14,6 +14,8 @@ module "functionapp" { app_service_logs_retention_period_days = var.function_apps.app_service_logs_retention_period_days app_settings = each.value.app_settings asp_id = module.app-service-plan["${each.value.app_service_plan_key}-${each.value.region}"].app_service_plan_id + assigned_identity_ids = var.function_apps.cont_registry_use_mi ? [data.azurerm_user_assigned_identity.acr_mi.id] : [] + cont_registry_use_mi = var.function_apps.cont_registry_use_mi # azuread_group_ids = each.value.azuread_group_ids function_app_slots = var.function_app_slots health_check_path = var.function_apps.health_check_path diff --git a/infrastructure/tf-core/variables.tf b/infrastructure/tf-core/variables.tf index d89edd9..ee00f7b 100644 --- a/infrastructure/tf-core/variables.tf +++ b/infrastructure/tf-core/variables.tf @@ -151,6 +151,7 @@ variable "function_apps" { always_on = bool app_service_logs_disk_quota_mb = optional(number) app_service_logs_retention_period_days = optional(number) + cont_registry_use_mi = optional(bool, false) docker_env_tag = string docker_img_prefix = string enable_appsrv_storage = bool From 3bec70113cd2ddb278a2b1f4499df1b5f52265f4 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 20:12:05 +0100 Subject: [PATCH 13/24] initial function app config --- infrastructure/tf-core/environments/development.tfvars | 1 + infrastructure/tf-core/variables.tf | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/infrastructure/tf-core/environments/development.tfvars b/infrastructure/tf-core/environments/development.tfvars index 5a35324..0611c57 100644 --- a/infrastructure/tf-core/environments/development.tfvars +++ b/infrastructure/tf-core/environments/development.tfvars @@ -153,6 +153,7 @@ function_apps = { app_service_logs_disk_quota_mb = 35 app_service_logs_retention_period_days = 7 always_on = true + cont_registry_use_mi = false docker_env_tag = "development" docker_img_prefix = "service-layer" enable_appsrv_storage = "false" diff --git a/infrastructure/tf-core/variables.tf b/infrastructure/tf-core/variables.tf index ee00f7b..606afde 100644 --- a/infrastructure/tf-core/variables.tf +++ b/infrastructure/tf-core/variables.tf @@ -151,7 +151,7 @@ variable "function_apps" { always_on = bool app_service_logs_disk_quota_mb = optional(number) app_service_logs_retention_period_days = optional(number) - cont_registry_use_mi = optional(bool, false) + cont_registry_use_mi = bool docker_env_tag = string docker_img_prefix = string enable_appsrv_storage = bool From 31e0bcbaadb9508453013b58e74353ffea9c4557 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 20:15:24 +0100 Subject: [PATCH 14/24] initial function app config --- infrastructure/tf-core/function_app.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf index 254d0f3..8f9cb7f 100644 --- a/infrastructure/tf-core/function_app.tf +++ b/infrastructure/tf-core/function_app.tf @@ -14,7 +14,7 @@ module "functionapp" { app_service_logs_retention_period_days = var.function_apps.app_service_logs_retention_period_days app_settings = each.value.app_settings asp_id = module.app-service-plan["${each.value.app_service_plan_key}-${each.value.region}"].app_service_plan_id - assigned_identity_ids = var.function_apps.cont_registry_use_mi ? [data.azurerm_user_assigned_identity.acr_mi.id] : [] + assigned_identity_ids = [] cont_registry_use_mi = var.function_apps.cont_registry_use_mi # azuread_group_ids = each.value.azuread_group_ids function_app_slots = var.function_app_slots From e778612d6f35dfb21ffd579c074a20cd38aa0f07 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 22:05:06 +0100 Subject: [PATCH 15/24] function app user-managed identity optional --- .azuredevops/pipelines/cd-infrastructure-dev-core.yaml | 2 +- infrastructure/tf-core/function_app.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml index a245059..9385bb6 100644 --- a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml +++ b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml @@ -14,7 +14,7 @@ resources: - repository: dtos-devops-templates type: github name: NHSDigital/dtos-devops-templates - ref: fix/function-app-acr-is-optional + ref: feat/DTOSS-9131-deploy-Service-Layer-infra endpoint: NHSDigital variables: diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf index 8f9cb7f..74c7e62 100644 --- a/infrastructure/tf-core/function_app.tf +++ b/infrastructure/tf-core/function_app.tf @@ -14,7 +14,6 @@ module "functionapp" { app_service_logs_retention_period_days = var.function_apps.app_service_logs_retention_period_days app_settings = each.value.app_settings asp_id = module.app-service-plan["${each.value.app_service_plan_key}-${each.value.region}"].app_service_plan_id - assigned_identity_ids = [] cont_registry_use_mi = var.function_apps.cont_registry_use_mi # azuread_group_ids = each.value.azuread_group_ids function_app_slots = var.function_app_slots @@ -50,6 +49,7 @@ module "functionapp" { /* ------------------------------------------------------------------------------------------------- Local variables used to create the Environment Variables for the Function Apps -------------------------------------------------------------------------------------------------- */ + locals { primary_region = [for k, v in var.regions : k if v.is_primary_region][0] From a264b1755d015560f1cfc9b3ab905f49e9c39b39 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 22:24:32 +0100 Subject: [PATCH 16/24] update templates branch ref --- .github/workflows/cicd-1-pull-request.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index fe2497d..dfbf910 100644 --- a/.github/workflows/cicd-1-pull-request.yaml +++ b/.github/workflows/cicd-1-pull-request.yaml @@ -103,7 +103,7 @@ jobs: build-image-stage: # Recommended maximum execution time is 3 minutes name: Image build stage needs: [metadata, commit-stage, test-stage] - uses: NHSDigital/dtos-devops-templates/.github/workflows/stage-3-build.yaml@fix/function-app-acr-is-optional + uses: NHSDigital/dtos-devops-templates/.github/workflows/stage-3-build.yaml@feat/DTOSS-9131-deploy-Service-Layer-infra if: needs.metadata.outputs.does_pull_request_exist == 'true' || github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')) with: docker_compose_file_csv_list: compose.yaml From a31ad15816b28d1d0a7e3ed5eff4f667af650903 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 23:00:59 +0100 Subject: [PATCH 17/24] don't pass acr_login_server when using ghcr.io --- infrastructure/tf-core/function_app.tf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf index 74c7e62..86952d5 100644 --- a/infrastructure/tf-core/function_app.tf +++ b/infrastructure/tf-core/function_app.tf @@ -7,7 +7,6 @@ module "functionapp" { resource_group_name = azurerm_resource_group.core[each.value.region].name location = each.value.region - acr_login_server = "ghcr.io/nhsdigital" ai_connstring = data.azurerm_application_insights.ai.connection_string always_on = var.function_apps.always_on app_service_logs_disk_quota_mb = var.function_apps.app_service_logs_disk_quota_mb @@ -18,7 +17,7 @@ module "functionapp" { # azuread_group_ids = each.value.azuread_group_ids function_app_slots = var.function_app_slots health_check_path = var.function_apps.health_check_path - image_name = "${var.function_apps.docker_img_prefix}-${lower(each.value.name_suffix)}" + image_name = "ghcr.io/nhsdigital/${var.function_apps.docker_img_prefix}-${lower(each.value.name_suffix)}" image_tag = var.function_apps.docker_env_tag ip_restriction_default_action = var.function_apps.ip_restriction_default_action ip_restrictions = each.value.ip_restrictions From c840efec39d837ce28ee5bda6422e163a5199b03 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 23:14:30 +0100 Subject: [PATCH 18/24] revert --- infrastructure/tf-core/function_app.tf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf index 86952d5..74c7e62 100644 --- a/infrastructure/tf-core/function_app.tf +++ b/infrastructure/tf-core/function_app.tf @@ -7,6 +7,7 @@ module "functionapp" { resource_group_name = azurerm_resource_group.core[each.value.region].name location = each.value.region + acr_login_server = "ghcr.io/nhsdigital" ai_connstring = data.azurerm_application_insights.ai.connection_string always_on = var.function_apps.always_on app_service_logs_disk_quota_mb = var.function_apps.app_service_logs_disk_quota_mb @@ -17,7 +18,7 @@ module "functionapp" { # azuread_group_ids = each.value.azuread_group_ids function_app_slots = var.function_app_slots health_check_path = var.function_apps.health_check_path - image_name = "ghcr.io/nhsdigital/${var.function_apps.docker_img_prefix}-${lower(each.value.name_suffix)}" + image_name = "${var.function_apps.docker_img_prefix}-${lower(each.value.name_suffix)}" image_tag = var.function_apps.docker_env_tag ip_restriction_default_action = var.function_apps.ip_restriction_default_action ip_restrictions = each.value.ip_restrictions From 2e5d988d020fb6c5c893f023bb2f1ebe186cfed1 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 23:23:01 +0100 Subject: [PATCH 19/24] test --- src/ServiceLayer.API/delme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceLayer.API/delme.txt b/src/ServiceLayer.API/delme.txt index 3122921..bd80632 100644 --- a/src/ServiceLayer.API/delme.txt +++ b/src/ServiceLayer.API/delme.txt @@ -1 +1 @@ -# comment6 +# comment7 From 45f23cc281968f3ad29372e65114c298d666f060 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Wed, 28 May 2025 23:47:17 +0100 Subject: [PATCH 20/24] test retagging logic --- src/ServiceLayer.API/delme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceLayer.API/delme.txt b/src/ServiceLayer.API/delme.txt index bd80632..b56e4d1 100644 --- a/src/ServiceLayer.API/delme.txt +++ b/src/ServiceLayer.API/delme.txt @@ -1 +1 @@ -# comment7 +# comment8 From a8ae98bb8b1c8ff617caf4bae23d4597f8d0185b Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Thu, 29 May 2025 00:02:40 +0100 Subject: [PATCH 21/24] registry_url must start with https:// --- infrastructure/tf-core/function_app.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf index 74c7e62..b0fe948 100644 --- a/infrastructure/tf-core/function_app.tf +++ b/infrastructure/tf-core/function_app.tf @@ -7,7 +7,7 @@ module "functionapp" { resource_group_name = azurerm_resource_group.core[each.value.region].name location = each.value.region - acr_login_server = "ghcr.io/nhsdigital" + acr_login_server = "https://ghcr.io/nhsdigital" ai_connstring = data.azurerm_application_insights.ai.connection_string always_on = var.function_apps.always_on app_service_logs_disk_quota_mb = var.function_apps.app_service_logs_disk_quota_mb From 59153009d02f9fd4a23af74f2b99b7f86cbc0056 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Thu, 29 May 2025 00:28:30 +0100 Subject: [PATCH 22/24] test-build --- src/ServiceLayer.API/delme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceLayer.API/delme.txt b/src/ServiceLayer.API/delme.txt index b56e4d1..b8680b0 100644 --- a/src/ServiceLayer.API/delme.txt +++ b/src/ServiceLayer.API/delme.txt @@ -1 +1 @@ -# comment8 +# comment9 From 4b51aa424ecad32a41dc9b8c8245f7d4b757d886 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Thu, 29 May 2025 10:01:25 +0100 Subject: [PATCH 23/24] function apps do not pull image over VNET --- infrastructure/tf-core/function_app.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf index b0fe948..3955296 100644 --- a/infrastructure/tf-core/function_app.tf +++ b/infrastructure/tf-core/function_app.tf @@ -56,7 +56,7 @@ locals { app_settings_common = { REMOTE_DEBUGGING_ENABLED = var.function_apps.remote_debugging_enabled WEBSITES_ENABLE_APP_SERVICE_STORAGE = var.function_apps.enable_appsrv_storage - WEBSITE_PULL_IMAGE_OVER_VNET = var.features.private_endpoints_enabled + WEBSITE_PULL_IMAGE_OVER_VNET = "false" FUNCTIONS_WORKER_RUNTIME = "dotnet-isolated" } From 25373d7de2ba328cf4254e1fedecf8838be86058 Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Thu, 29 May 2025 10:34:56 +0100 Subject: [PATCH 24/24] remove service-layer from container naming suffix --- infrastructure/tf-core/environments/development.tfvars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/tf-core/environments/development.tfvars b/infrastructure/tf-core/environments/development.tfvars index 0611c57..da23cfe 100644 --- a/infrastructure/tf-core/environments/development.tfvars +++ b/infrastructure/tf-core/environments/development.tfvars @@ -167,7 +167,7 @@ function_apps = { function_app_config = { ServiceLayer = { - name_suffix = "service-layer-api" + name_suffix = "api" function_endpoint_name = "ServiceLayer" app_service_plan_key = "Default" db_connection_string = "DatabaseConnectionString"