From 1bcbfd8f238c4d719260bf8da70f31251931a4c0 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:26:09 +0100 Subject: [PATCH 01/12] Add parameter and variable to pipeline --- .azuredevops/pipelines/cd-infrastructure-dev-core.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml index 2e987c0d..716a0b0a 100644 --- a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml +++ b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml @@ -17,6 +17,11 @@ resources: ref: f8141ab50ec0f3630044fa0f531952d2dbbd1e85 endpoint: NHSDigital +parameters: + - name: image-hash + type: string + default: '' + variables: - group: DEV_core_backend - group: DEV_audit_backend_remote_state @@ -29,6 +34,8 @@ variables: value: tf_plan_core_DEV - name: ENVIRONMENT value: development + - name: image-hash + value: ${{ parameters.image-hash }} stages: - stage: terraform_plan From 09b7afc3e4dbb4de5c4bb7ece786d95c1bfe50e4 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:15:45 +0100 Subject: [PATCH 02/12] Squashed commit from PR30 @ fe00cc8 --- .../cd-infrastructure-dev-audit.yaml | 4 +- .../pipelines/cd-infrastructure-dev-core.yaml | 4 +- .github/workflows/cicd-1-pull-request.yaml | 10 +- compose.yaml | 8 +- 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 ++++++ 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 | 278 ++++++++++++ .../tf-core/environments/integration.tfvars | 231 ++++++++++ .../tf-core/environments/nft.tfvars | 231 ++++++++++ infrastructure/tf-core/function_app.tf | 126 ++++++ 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 | 55 +++ infrastructure/tf-core/variables.tf | 400 ++++++++++++++++++ src/ServiceLayer.Shared/delme.txt | 1 + 36 files changed, 2567 insertions(+), 13 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 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 create mode 100644 src/ServiceLayer.Shared/delme.txt diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-audit.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-audit.yaml index 86710f46..3d6d8ad0 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 716a0b0a..8fee6611 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: feat/DTOSS-9131-deploy-Service-Layer-infra endpoint: NHSDigital parameters: @@ -29,7 +29,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 cbf106a7..dfbf9105 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: @@ -103,11 +103,11 @@ 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@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: ./compose.yaml - excluded_containers_csv_list: azurite,azurite-setup,sql-database,database-setup + 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 project_name: service-layer diff --git a/compose.yaml b/compose.yaml index 076592a9..1512cfc2 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,6 +1,6 @@ services: - api: - container_name: "api" + svclyr-api: + container_name: "svclyr-api" build: context: ./src dockerfile: ServiceLayer.API/Dockerfile @@ -30,8 +30,8 @@ services: networks: - backend - mesh-ingest: - container_name: "mesh-ingest" + svclyr-mesh-ingest: + container_name: "svclyr-mesh-ingest" build: context: ./src dockerfile: ServiceLayer.Mesh/Dockerfile diff --git a/infrastructure/tf-audit/app_insights.tf b/infrastructure/tf-audit/app_insights.tf new file mode 100644 index 00000000..cc0bfc66 --- /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 00000000..fee7f156 --- /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 00000000..442d60fb --- /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 00000000..f52e48ed --- /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 00000000..4b4a028b --- /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 00000000..cab25fc8 --- /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 00000000..bb6ad31c --- /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 00000000..79be9f7d --- /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 00000000..a117221a --- /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 00000000..1ac72f7f --- /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 00000000..a8d1c2a9 --- /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 00000000..35a10659 --- /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 00000000..b3f5d766 --- /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 00000000..dc7c6965 --- /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 00000000..530e0938 --- /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") + })), {}) + })) +} diff --git a/infrastructure/tf-core/app_service_plan.tf b/infrastructure/tf-core/app_service_plan.tf new file mode 100644 index 00000000..58a82d54 --- /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 00000000..68ac1a00 --- /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 00000000..805fc245 --- /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 00000000..41804b9f --- /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 00000000..28c3fb23 --- /dev/null +++ b/infrastructure/tf-core/environments/development.tfvars @@ -0,0 +1,278 @@ +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 + cont_registry_use_mi = false + 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 = { + + ServiceLayerAPI = { + name_suffix = "svclyr-api" + function_endpoint_name = "ServiceLayerAPI" + app_service_plan_key = "Default" + env_vars = { + static = { + # env_var_name = value + } + from_key_vault = { + # env_var_name = "key_vault_secret_name" + } + local_urls = { + # %s becomes the environment and region prefix (e.g. dev-uks) + } + } + } + + ServiceLayerMesh = { + name_suffix = "svclyr-mesh-ingest" + function_endpoint_name = "ServiceLayerMesh" + app_service_plan_key = "Default" + db_connection_string = "DatabaseConnectionString" + env_vars = { + static = { + MeshApiBaseUrl = "https://msg.intspineservices.nhs.uk" + FileDiscoveryTimerExpression = "0 */5 * * * *" + MeshHandshakeTimerExpression = "0 0 0 * * * " + FileRetryTimerExpression = "0 0 * * * *" + FileExtractQueueName = "file-extract" + FileTransformQueueName = "file-transform" + StaleHours = "12" + MeshBlobContainerName = "incoming-mesh-files" + MeshBlobStorageUrl = "https://stsvclyrdevuksmeshstor.blob.core.windows.net" + MeshQueueStorageUrl = "https://stsvclyrdevuksmeshstor.queue.core.windows.net" + } + from_key_vault = { + MeshPassword = "MeshPassword" + MeshSharedKey = "MeshSharedKey" + NbssMailboxId = "NbssMailboxId" + } + } + } + } +} + +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 + } + + # svclyr database + dbs = { + svclyr = { + 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 = "ZRS" + public_network_access_enabled = false + containers = {} + } + mesh = { + name_suffix = "meshstor" + account_tier = "Standard" + replication_type = "ZRS" + public_network_access_enabled = true + blob_properties_delete_retention_policy = 7 + blob_properties_versioning_enabled = false + containers = { + incoming = { + container_name = "incoming-mesh-files" + } + } + queues = [ + "file-extract", + "file-extract-poison", + "file-transform", + "file-transform-poison" + ] + } +} diff --git a/infrastructure/tf-core/environments/integration.tfvars b/infrastructure/tf-core/environments/integration.tfvars new file mode 100644 index 00000000..31a0c6f9 --- /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 00000000..51c5b687 --- /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 00000000..348b0760 --- /dev/null +++ b/infrastructure/tf-core/function_app.tf @@ -0,0 +1,126 @@ +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 = "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 + 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 + 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 = "false" + 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.svclyr.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 00000000..bb8eac79 --- /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 00000000..6ff76ed9 --- /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 00000000..c369ebc0 --- /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 00000000..451f0afe --- /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 00000000..433cdcc7 --- /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 00000000..7275866c --- /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.svclyr.db_name_suffix + collation = var.sqlserver.dbs.svclyr.collation + licence_type = var.sqlserver.dbs.svclyr.licence_type + max_gb = var.sqlserver.dbs.svclyr.max_gb + read_scale = var.sqlserver.dbs.svclyr.read_scale + sku = var.sqlserver.dbs.svclyr.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 00000000..ad7426ae --- /dev/null +++ b/infrastructure/tf-core/storage.tf @@ -0,0 +1,55 @@ +module "storage" { + for_each = local.storage_accounts_map + + source = "../../../dtos-devops-templates/infrastructure/modules/storage" + + name = substr("${module.regions_config[each.value.region].names.storage-account}${lower(each.value.name_suffix)}", 0, 24) + resource_group_name = azurerm_resource_group.core[each.value.region].name + location = each.value.region + + 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}-storage_blob"].id] + private_dns_zone_ids_queue = [data.terraform_remote_state.hub.outputs.private_dns_zones["${each.value.region}-storage_queue"].id] + private_endpoint_enabled = var.features.private_endpoints_enabled + private_endpoint_subnet_id = module.subnets["${module.regions_config[each.value.region].names.subnet}-pep"].id + private_endpoint_resource_group_name = azurerm_resource_group.rg_private_endpoints[each.value.region].name + private_service_connection_is_manual = var.features.private_service_connection_is_manual + } : null + + queues = each.value.queues + + tags = var.tags +} + +locals { + # There are multiple Storage Accounts and possibly multiple regions. + # We cannot nest for loops inside a map, so first iterate all permutations of both as a list of objects... + storage_accounts_object_list = flatten([ + for region in keys(var.regions) : [ + for storage_account, config in var.storage_accounts : merge( + { + region = region # 1st iterator + storage_account = storage_account # 2nd iterator + }, + config # the rest of the key/value pairs for a specific storage_account + ) + ] + ]) + # ...then project the list of objects into a map with unique keys (combining the iterators), for consumption by a for_each meta argument + storage_accounts_map = { + for object in local.storage_accounts_object_list : "${object.storage_account}-${object.region}" => object + } +} diff --git a/infrastructure/tf-core/variables.tf b/infrastructure/tf-core/variables.tf new file mode 100644 index 00000000..85155882 --- /dev/null +++ b/infrastructure/tf-core/variables.tf @@ -0,0 +1,400 @@ +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) + cont_registry_use_mi = bool + 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, "svclyr") + 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") + })), {}) + queues = optional(list(string)) + })) +} + +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 +} diff --git a/src/ServiceLayer.Shared/delme.txt b/src/ServiceLayer.Shared/delme.txt new file mode 100644 index 00000000..9aedc8b8 --- /dev/null +++ b/src/ServiceLayer.Shared/delme.txt @@ -0,0 +1 @@ +# test1 From e4cfa21432d673a78b569ad12bbe623f5b26e7a6 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:37:12 +0100 Subject: [PATCH 03/12] Update properties to receive data from CI pipeline --- .../pipelines/cd-infrastructure-dev-core.yaml | 6 +++++- .github/workflows/cicd-1-pull-request.yaml | 2 +- infrastructure/tf-core/function_app.tf | 6 +++--- infrastructure/tf-core/variables.tf | 11 +++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml index 8fee6611..8b5429bf 100644 --- a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml +++ b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml @@ -14,13 +14,15 @@ resources: - repository: dtos-devops-templates type: github name: NHSDigital/dtos-devops-templates - ref: feat/DTOSS-9131-deploy-Service-Layer-infra + ref: dtoss-9326-trigger-cd-pipeline-with-commit endpoint: NHSDigital parameters: - name: image-hash type: string default: '' + - name: registry-host + type: string variables: - group: DEV_core_backend @@ -36,6 +38,8 @@ variables: value: development - name: image-hash value: ${{ parameters.image-hash }} + - name: registry-host + value: ${{ parameters.registry-host }} stages: - stage: terraform_plan diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index dfbf9105..03460e2c 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@feat/DTOSS-9131-deploy-Service-Layer-infra + uses: NHSDigital/dtos-devops-templates/.github/workflows/stage-3-build.yaml@feat/dtoss-9326-trigger-cd-pipeline-with-commit 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 diff --git a/infrastructure/tf-core/function_app.tf b/infrastructure/tf-core/function_app.tf index 348b0760..4f27ec0f 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 = "https://ghcr.io/nhsdigital" + acr_login_server = "${var.registry_host}" 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 @@ -15,11 +15,11 @@ module "functionapp" { 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 cont_registry_use_mi = var.function_apps.cont_registry_use_mi - # azuread_group_ids = each.value.azuread_group_ids + # 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 + image_tag = "${var.image_commit_hash}" 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] diff --git a/infrastructure/tf-core/variables.tf b/infrastructure/tf-core/variables.tf index 85155882..c7200b88 100644 --- a/infrastructure/tf-core/variables.tf +++ b/infrastructure/tf-core/variables.tf @@ -211,6 +211,11 @@ variable "function_app_slots" { })) } +variable "image_commit_hash" { + description = "The commit SHA of the Docker image generated by the CI pipeline and applied to all functions" + type = string +} + variable "key_vault" { description = "Configuration for the key vault" type = object({ @@ -276,6 +281,12 @@ variable "regions" { })) } +variable "registry_host" { + description = "The URL of the container registry used by the CI pipeline. Default = " + type = string + default = "https://ghcr.io/nhsdigital" +} + variable "routes" { description = "Routes configuration for different regions" type = map(object({ From 98bf3f985166867a79ab924c41ae0c89912b374f Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:44:39 +0100 Subject: [PATCH 04/12] Trigger for pipeline run Makefile fixes for linting checks --- compose.yaml | 2 ++ scripts/terraform/terraform.mk | 23 +++++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/compose.yaml b/compose.yaml index 1512cfc2..480ce21e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -147,3 +147,5 @@ volumes: mesh-config-data: name: mesh-config-data driver: local + + diff --git a/scripts/terraform/terraform.mk b/scripts/terraform/terraform.mk index 120a0591..88fd6d8f 100644 --- a/scripts/terraform/terraform.mk +++ b/scripts/terraform/terraform.mk @@ -41,11 +41,14 @@ clean:: # Remove Terraform files (terraform) - optional: terraform_dir|dir=[path opts=$(or ${terraform_opts}, ${opts}) _terraform: # Terraform command wrapper - mandatory: cmd=[command to execute]; optional: dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], opts=[options to pass to the Terraform command, default is none/empty] - # 'TERRAFORM_STACK' is passed to the functions as environment variable - TERRAFORM_STACK=$(or ${TERRAFORM_STACK}, $(or ${terraform_stack}, $(or ${STACK}, $(or ${stack}, scripts/terraform/examples/terraform-state-aws-s3)))) - dir=$(or ${dir}, ${TERRAFORM_STACK}) - source scripts/terraform/terraform.lib.sh - terraform-${cmd} # 'dir' and 'opts' are accessible by the function as environment variables, if set +# 'TERRAFORM_STACK' is passed to the functions as environment variable + TERRAFORM_STACK="$${TERRAFORM_STACK:-$${terraform_stack:-$${STACK:-$${stack:-scripts/terraform/examples/terraform-state-aws-s3}}}}"; \ + dir="$${dir:-$${TERRAFORM_STACK}}"; \ + if [ ! -d "$$dir" ]; then \ + echo "[WARNING] Terraform directory not found: $$dir. Ensure TERRAFORM_STACK is set " >&2; \ + fi; \ + source scripts/terraform/terraform.lib.sh; \ + terraform-${cmd} \ # ============================================================================== # Quality checks - please DO NOT edit this section! @@ -67,10 +70,10 @@ terraform-example-destroy-aws-infrastructure: # Destroy example of AWS infrastru make terraform-destroy opts="-auto-approve" terraform-example-clean: # Remove Terraform example files @ExamplesAndTests - dir=$(or ${dir}, ${TERRAFORM_STACK}) - source scripts/terraform/terraform.lib.sh - terraform-clean - rm -f ${TERRAFORM_STACK}/.terraform.lock.hcl + dir="$${dir:-$${TERRAFORM_STACK}}"; \ + source scripts/terraform/terraform.lib.sh; \ + terraform-clean; \ + rm -f "$${TERRAFORM_STACK}/.terraform.lock.hcl" \ # ============================================================================== # Configuration - please DO NOT edit this section! @@ -93,4 +96,4 @@ ${VERBOSE}.SILENT: \ terraform-install \ terraform-plan \ terraform-shellscript-lint \ - terraform-validate \ + terraform-validate From 862996041411535512c22add93bc9cc255b71191 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Wed, 4 Jun 2025 12:25:53 +0100 Subject: [PATCH 05/12] Makefile updates --- compose.yaml | 2 ++ infrastructure/tf-core/variables.tf | 1 + scripts/terraform/examples/empty.tf | 14 ++++++++++++++ scripts/terraform/terraform.mk | 11 ++++------- 4 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 scripts/terraform/examples/empty.tf diff --git a/compose.yaml b/compose.yaml index 480ce21e..d6a8acee 100644 --- a/compose.yaml +++ b/compose.yaml @@ -131,6 +131,8 @@ services: DATABASE_NAME: "${DATABASE_NAME}" DATABASE_USER: "${DATABASE_USER}" DATABASE_PASSWORD: "${DATABASE_PASSWORD}" + env_file: + - ./.env.example networks: - backend diff --git a/infrastructure/tf-core/variables.tf b/infrastructure/tf-core/variables.tf index c7200b88..32b57044 100644 --- a/infrastructure/tf-core/variables.tf +++ b/infrastructure/tf-core/variables.tf @@ -262,6 +262,7 @@ variable "network_security_group_rules" { source_addresses = ["0.0.0.0/0"] destination_fqdns = ["example.com"] }, + ] */ variable "regions" { diff --git a/scripts/terraform/examples/empty.tf b/scripts/terraform/examples/empty.tf new file mode 100644 index 00000000..6cb7b04f --- /dev/null +++ b/scripts/terraform/examples/empty.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0.0" + + required_providers { + null = { + source = "hashicorp/null" + version = "~> 3.0" + } + } +} + +provider "null" { + # Does nothing, just here to satisfy provider requirement +} diff --git a/scripts/terraform/terraform.mk b/scripts/terraform/terraform.mk index 88fd6d8f..4348ef64 100644 --- a/scripts/terraform/terraform.mk +++ b/scripts/terraform/terraform.mk @@ -42,13 +42,10 @@ clean:: # Remove Terraform files (terraform) - optional: terraform_dir|dir=[path _terraform: # Terraform command wrapper - mandatory: cmd=[command to execute]; optional: dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], opts=[options to pass to the Terraform command, default is none/empty] # 'TERRAFORM_STACK' is passed to the functions as environment variable - TERRAFORM_STACK="$${TERRAFORM_STACK:-$${terraform_stack:-$${STACK:-$${stack:-scripts/terraform/examples/terraform-state-aws-s3}}}}"; \ - dir="$${dir:-$${TERRAFORM_STACK}}"; \ - if [ ! -d "$$dir" ]; then \ - echo "[WARNING] Terraform directory not found: $$dir. Ensure TERRAFORM_STACK is set " >&2; \ - fi; \ - source scripts/terraform/terraform.lib.sh; \ - terraform-${cmd} \ + TERRAFORM_STACK="$${TERRAFORM_STACK:-$${terraform_stack:-$${STACK:-$${stack:-scripts/terraform/examples}}}}"; + dir="$${dir:-$${TERRAFORM_STACK}}"; + source scripts/terraform/terraform.lib.sh; + terraform-${cmd} # ============================================================================== # Quality checks - please DO NOT edit this section! From a6e0e9c1fff8abf16b6953176d1ac05c05fba008 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:46:28 +0100 Subject: [PATCH 06/12] Update project name to match CD project name --- .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 03460e2c..e6188d26 100644 --- a/.github/workflows/cicd-1-pull-request.yaml +++ b/.github/workflows/cicd-1-pull-request.yaml @@ -110,7 +110,7 @@ jobs: 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 + project_name: dtos-service-layer secrets: inherit acceptance-stage: # Recommended maximum execution time is 10 minutes name: Acceptance stage From 3300384e6131f965df3d083d4218a62c5595c2bf Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Wed, 4 Jun 2025 17:20:22 +0100 Subject: [PATCH 07/12] Parameter usage update --- .azuredevops/pipelines/cd-infrastructure-dev-core.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml index 8b5429bf..2664107d 100644 --- a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml +++ b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml @@ -36,10 +36,6 @@ variables: value: tf_plan_core_DEV - name: ENVIRONMENT value: development - - name: image-hash - value: ${{ parameters.image-hash }} - - name: registry-host - value: ${{ parameters.registry-host }} stages: - stage: terraform_plan @@ -54,6 +50,9 @@ stages: - checkout: self - checkout: dtos-devops-templates - template: .azuredevops/templates/steps/tf_plan.yaml@dtos-devops-templates + parameters: + image-hash: ${{ parameters.image-hash }} + registry-host: ${{ parameters.registry-host }} - stage: terraform_apply displayName: Terraform Apply From 95a19fde13eea62f67d78ca9ba931210b6994728 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Wed, 4 Jun 2025 17:44:29 +0100 Subject: [PATCH 08/12] Update parameter name casing --- .../cd-infrastructure-dev-audit.yaml | 2 +- .../pipelines/cd-infrastructure-dev-core.yaml | 10 +-- compose.yaml | 2 - .../tf-core/environments/development.tfvars | 6 +- .../tf-core/environments/integration.tfvars | 79 +++++++++++++++---- .../tf-core/environments/nft.tfvars | 79 +++++++++++++++---- src/ServiceLayer.Shared/delme.txt | 1 - 7 files changed, 135 insertions(+), 44 deletions(-) delete mode 100644 src/ServiceLayer.Shared/delme.txt diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-audit.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-audit.yaml index 3d6d8ad0..7549be12 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: 9673ee4ef9770e80d0714c3966a699414b7b43c7 + ref: cf5e22fe4614b7d077a22301d29883e86ac3defc endpoint: NHSDigital variables: diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml index 2664107d..a9ef8f9c 100644 --- a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml +++ b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml @@ -14,14 +14,14 @@ resources: - repository: dtos-devops-templates type: github name: NHSDigital/dtos-devops-templates - ref: dtoss-9326-trigger-cd-pipeline-with-commit + ref: feat/dtoss-9326-trigger-cd-pipeline-with-commit endpoint: NHSDigital parameters: - - name: image-hash + - name: imageHash type: string default: '' - - name: registry-host + - name: registryHost type: string variables: @@ -51,8 +51,8 @@ stages: - checkout: dtos-devops-templates - template: .azuredevops/templates/steps/tf_plan.yaml@dtos-devops-templates parameters: - image-hash: ${{ parameters.image-hash }} - registry-host: ${{ parameters.registry-host }} + imageHash: ${{ parameters.imageHash }} + registryHost: ${{ parameters.registryHost }} - stage: terraform_apply displayName: Terraform Apply diff --git a/compose.yaml b/compose.yaml index d6a8acee..480ce21e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -131,8 +131,6 @@ services: DATABASE_NAME: "${DATABASE_NAME}" DATABASE_USER: "${DATABASE_USER}" DATABASE_PASSWORD: "${DATABASE_PASSWORD}" - env_file: - - ./.env.example networks: - backend diff --git a/infrastructure/tf-core/environments/development.tfvars b/infrastructure/tf-core/environments/development.tfvars index 28c3fb23..fe38f701 100644 --- a/infrastructure/tf-core/environments/development.tfvars +++ b/infrastructure/tf-core/environments/development.tfvars @@ -192,7 +192,7 @@ function_apps = { static = { MeshApiBaseUrl = "https://msg.intspineservices.nhs.uk" FileDiscoveryTimerExpression = "0 */5 * * * *" - MeshHandshakeTimerExpression = "0 0 0 * * * " + MeshHandshakeTimerExpression = "0 0 0 * * *" FileRetryTimerExpression = "0 0 * * * *" FileExtractQueueName = "file-extract" FileTransformQueueName = "file-transform" @@ -252,14 +252,14 @@ storage_accounts = { fnapp = { name_suffix = "fnappstor" account_tier = "Standard" - replication_type = "ZRS" + replication_type = "LRS" public_network_access_enabled = false containers = {} } mesh = { name_suffix = "meshstor" account_tier = "Standard" - replication_type = "ZRS" + replication_type = "LRS" public_network_access_enabled = true blob_properties_delete_retention_policy = 7 blob_properties_versioning_enabled = false diff --git a/infrastructure/tf-core/environments/integration.tfvars b/infrastructure/tf-core/environments/integration.tfvars index 31a0c6f9..ed685968 100644 --- a/infrastructure/tf-core/environments/integration.tfvars +++ b/infrastructure/tf-core/environments/integration.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 = "integration" docker_img_prefix = "service-layer" enable_appsrv_storage = "false" @@ -165,8 +166,48 @@ function_apps = { function_app_config = { + ServiceLayerAPI = { + name_suffix = "svclyr-api" + function_endpoint_name = "ServiceLayerAPI" + app_service_plan_key = "Default" + env_vars = { + static = { + # env_var_name = value + } + from_key_vault = { + # env_var_name = "key_vault_secret_name" + } + local_urls = { + # %s becomes the environment and region prefix (e.g. dev-uks) + } + } + } - + ServiceLayerMesh = { + name_suffix = "svclyr-mesh-ingest" + function_endpoint_name = "ServiceLayerMesh" + app_service_plan_key = "Default" + db_connection_string = "DatabaseConnectionString" + env_vars = { + static = { + MeshApiBaseUrl = "https://msg.intspineservices.nhs.uk" + FileDiscoveryTimerExpression = "0 */5 * * * *" + MeshHandshakeTimerExpression = "0 0 0 * * *" + FileRetryTimerExpression = "0 0 * * * *" + FileExtractQueueName = "file-extract" + FileTransformQueueName = "file-transform" + StaleHours = "12" + MeshBlobContainerName = "incoming-mesh-files" + MeshBlobStorageUrl = "https://stsvclyrintuksmeshstor.blob.core.windows.net" + MeshQueueStorageUrl = "https://stsvclyrintuksmeshstor.queue.core.windows.net" + } + from_key_vault = { + MeshPassword = "MeshPassword" + MeshSharedKey = "MeshSharedKey" + NbssMailboxId = "NbssMailboxId" + } + } + } } } @@ -192,9 +233,9 @@ sqlserver = { azure_services_access_enabled = true } - # parman database + # svclyr database dbs = { - parman = { + svclyr = { db_name_suffix = "service_layer_database" collation = "SQL_Latin1_General_CP1_CI_AS" licence_type = "LicenseIncluded" @@ -215,17 +256,23 @@ storage_accounts = { 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" - # } - # } - # } + mesh = { + name_suffix = "meshstor" + account_tier = "Standard" + replication_type = "LRS" + public_network_access_enabled = true + blob_properties_delete_retention_policy = 7 + blob_properties_versioning_enabled = false + containers = { + incoming = { + container_name = "incoming-mesh-files" + } + } + queues = [ + "file-extract", + "file-extract-poison", + "file-transform", + "file-transform-poison" + ] + } } diff --git a/infrastructure/tf-core/environments/nft.tfvars b/infrastructure/tf-core/environments/nft.tfvars index 51c5b687..3547bade 100644 --- a/infrastructure/tf-core/environments/nft.tfvars +++ b/infrastructure/tf-core/environments/nft.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 = "nft" docker_img_prefix = "service-layer" enable_appsrv_storage = "false" @@ -165,8 +166,48 @@ function_apps = { function_app_config = { + ServiceLayerAPI = { + name_suffix = "svclyr-api" + function_endpoint_name = "ServiceLayerAPI" + app_service_plan_key = "Default" + env_vars = { + static = { + # env_var_name = value + } + from_key_vault = { + # env_var_name = "key_vault_secret_name" + } + local_urls = { + # %s becomes the environment and region prefix (e.g. dev-uks) + } + } + } - + ServiceLayerMesh = { + name_suffix = "svclyr-mesh-ingest" + function_endpoint_name = "ServiceLayerMesh" + app_service_plan_key = "Default" + db_connection_string = "DatabaseConnectionString" + env_vars = { + static = { + MeshApiBaseUrl = "https://msg.intspineservices.nhs.uk" + FileDiscoveryTimerExpression = "0 */5 * * * *" + MeshHandshakeTimerExpression = "0 0 0 * * *" + FileRetryTimerExpression = "0 0 * * * *" + FileExtractQueueName = "file-extract" + FileTransformQueueName = "file-transform" + StaleHours = "12" + MeshBlobContainerName = "incoming-mesh-files" + MeshBlobStorageUrl = "https://stsvclyrnftuksmeshstor.blob.core.windows.net" + MeshQueueStorageUrl = "https://stsvclyrnftuksmeshstor.queue.core.windows.net" + } + from_key_vault = { + MeshPassword = "MeshPassword" + MeshSharedKey = "MeshSharedKey" + NbssMailboxId = "NbssMailboxId" + } + } + } } } @@ -192,9 +233,9 @@ sqlserver = { azure_services_access_enabled = true } - # parman database + # svclyr database dbs = { - parman = { + svclyr = { db_name_suffix = "service_layer_database" collation = "SQL_Latin1_General_CP1_CI_AS" licence_type = "LicenseIncluded" @@ -215,17 +256,23 @@ storage_accounts = { 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" - # } - # } - # } + mesh = { + name_suffix = "meshstor" + account_tier = "Standard" + replication_type = "LRS" + public_network_access_enabled = true + blob_properties_delete_retention_policy = 7 + blob_properties_versioning_enabled = false + containers = { + incoming = { + container_name = "incoming-mesh-files" + } + } + queues = [ + "file-extract", + "file-extract-poison", + "file-transform", + "file-transform-poison" + ] + } } diff --git a/src/ServiceLayer.Shared/delme.txt b/src/ServiceLayer.Shared/delme.txt deleted file mode 100644 index 9aedc8b8..00000000 --- a/src/ServiceLayer.Shared/delme.txt +++ /dev/null @@ -1 +0,0 @@ -# test1 From b76a464d74c8b75393fe7b894a3b3ae9ec14fa49 Mon Sep 17 00:00:00 2001 From: Ian Nelson Date: Tue, 3 Jun 2025 13:08:00 +0100 Subject: [PATCH 09/12] fix: remove TODO comment resolved by DTOSS-9159 (#44) --- .../FileTypes/NbssAppointmentEvents/Validation/IFileValidator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/IFileValidator.cs b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/IFileValidator.cs index 76b6e70c..dea70507 100644 --- a/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/IFileValidator.cs +++ b/src/ServiceLayer.Mesh/FileTypes/NbssAppointmentEvents/Validation/IFileValidator.cs @@ -2,7 +2,6 @@ namespace ServiceLayer.Mesh.FileTypes.NbssAppointmentEvents.Validation; -// TODO - create a whole bunch of implementations of this to perform the validation against NBSS Appointment events files public interface IFileValidator { IEnumerable Validate(ParsedFile file); From 13fd6081f9b99b35221b0596ff0d07e7c961a3cd Mon Sep 17 00:00:00 2001 From: patrickmoore-nc <94625903+patrickmoore-nc@users.noreply.github.com> Date: Thu, 5 Jun 2025 16:24:35 +0100 Subject: [PATCH 10/12] feat: DTOSS-9131 Terraform infrastructure creation (#30) --- .azuredevops/pipelines/cd-infrastructure-dev-core.yaml | 2 +- .github/workflows/cicd-1-pull-request.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml index a9ef8f9c..5ed7f678 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: feat/dtoss-9326-trigger-cd-pipeline-with-commit + ref: cf5e22fe4614b7d077a22301d29883e86ac3defc endpoint: NHSDigital parameters: diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index e6188d26..8dbf6636 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.yaml@feat/dtoss-9326-trigger-cd-pipeline-with-commit + uses: NHSDigital/dtos-devops-templates/.github/workflows/stage-3-build.yaml@main 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 + 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 From 6242c0a9af845fb3d4b290f5d13d54dcda2831e7 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:28:24 +0100 Subject: [PATCH 11/12] Update repository ref Prepare for PR --- .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 5ed7f678..eeb4f735 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: cf5e22fe4614b7d077a22301d29883e86ac3defc + ref: main endpoint: NHSDigital parameters: From c8243cc768eb63b7f727fe60ae89ae8462a1e512 Mon Sep 17 00:00:00 2001 From: Michael Justus <209924279+micjustus-nc@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:01:13 +0100 Subject: [PATCH 12/12] Testing default registry host value in pipeline --- .azuredevops/pipelines/cd-infrastructure-dev-core.yaml | 5 ++--- .github/workflows/cicd-1-pull-request.yaml | 2 +- src/ServiceLayer.API/Program.cs | 3 +++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml index eeb4f735..08362634 100644 --- a/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml +++ b/.azuredevops/pipelines/cd-infrastructure-dev-core.yaml @@ -43,6 +43,8 @@ stages: condition: eq(variables['Build.Reason'], 'Manual') variables: tfVarsFile: environments/$(ENVIRONMENT).tfvars + imageHash: ${{ parameters.imageHash }} + registryHost: ${{ parameters.registryHost }} jobs: - job: init_and_plan displayName: Init, plan, store artifact @@ -50,9 +52,6 @@ stages: - checkout: self - checkout: dtos-devops-templates - template: .azuredevops/templates/steps/tf_plan.yaml@dtos-devops-templates - parameters: - imageHash: ${{ parameters.imageHash }} - registryHost: ${{ parameters.registryHost }} - stage: terraform_apply displayName: Terraform Apply diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index 8dbf6636..c408785d 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@main + uses: NHSDigital/dtos-devops-templates/.github/workflows/stage-3-build.yaml 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 diff --git a/src/ServiceLayer.API/Program.cs b/src/ServiceLayer.API/Program.cs index 6bf7313f..83eacfcc 100644 --- a/src/ServiceLayer.API/Program.cs +++ b/src/ServiceLayer.API/Program.cs @@ -27,3 +27,6 @@ .Build(); await host.RunAsync(); + + +