├── functions ├── dist │ └── .gitkeep └── src │ └── health-api │ ├── function.json │ ├── requirements.txt │ ├── host.json │ └── function_app.py ├── .terraform-version ├── Brewfile ├── html └── waf-response.html ├── renovate.json ├── signalr.tf ├── .github └── workflows │ ├── continuous-integration-tflint.yml │ ├── continuous-integration-tfsec.yml │ └── continuous-integration-terraform.yml ├── moved.tf ├── versions.tf ├── resource-group.tf ├── .terraform-docs.yml ├── .gitignore ├── private-endpoints.tf ├── app-configuration.tf ├── LICENSE ├── container-registry.tf ├── schema └── common-alert-schema.json ├── key-vault.tf ├── identity.tf ├── function-app-service-bus.tf ├── postgres.tf ├── outputs.tf ├── webhook └── slack.json ├── app-insights.tf ├── script └── apply-tags-to-container-app-env-mc-resource-group ├── redis-cache.tf ├── data.tf ├── logging.tf ├── custom-container-apps.tf ├── network-watcher.tf ├── dns.tf ├── mssql.tf ├── container-app.tf ├── storage.tf ├── function-app.tf ├── cdn.tf ├── monitor.tf └── locals.tf /functions/dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.terraform-version: -------------------------------------------------------------------------------- 1 | 1.12.2 2 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "tfenv" 2 | brew "terraform-docs" 3 | brew "tfsec" 4 | brew "tflint" 5 | -------------------------------------------------------------------------------- /html/waf-response.html: -------------------------------------------------------------------------------- 1 | 2 |
Forbidden
3 | 4 | Forbidden 5 | 6 | 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /functions/src/health-api/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "type": "httpTrigger", 5 | "direction": "in", 6 | "route": "http_trigger", 7 | "authLevel": "function" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /functions/src/health-api/requirements.txt: -------------------------------------------------------------------------------- 1 | # DO NOT include azure-functions-worker in this file 2 | # The Python Worker is managed by Azure Functions platform 3 | # Manually managing azure-functions-worker may cause unexpected issues 4 | 5 | azure-functions 6 | azure-monitor-query 7 | azure-identity 8 | -------------------------------------------------------------------------------- /functions/src/health-api/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /signalr.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_signalr_service" "default" { 2 | count = local.enable_signalr ? 1 : 0 3 | 4 | name = "${local.resource_prefix}-signalr" 5 | location = local.resource_group.location 6 | resource_group_name = local.resource_group.name 7 | 8 | sku { 9 | name = local.signalr_sku 10 | capacity = 1 11 | } 12 | 13 | service_mode = local.signalr_service_mode 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration-tflint.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | on: 3 | pull_request: 4 | jobs: 5 | tflint: 6 | name: tflint 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Clone repo 10 | uses: actions/checkout@v4 11 | - name: Setup TFLint 12 | uses: terraform-linters/setup-tflint@v4 13 | with: 14 | tflint_version: v0.44.1 15 | - name: Run TFLint 16 | run: tflint -f compact 17 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration-tfsec.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | on: 3 | pull_request: 4 | jobs: 5 | tfsec-pr-commenter: 6 | name: tfsec PR commenter 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Clone repo 10 | uses: actions/checkout@v4 11 | - name: tfsec 12 | uses: aquasecurity/tfsec-pr-commenter-action@v1.3.1 13 | with: 14 | github_token: ${{ github.token }} 15 | working_directory: '' 16 | -------------------------------------------------------------------------------- /moved.tf: -------------------------------------------------------------------------------- 1 | moved { 2 | from = azurerm_private_dns_zone.storage_private_link[0] 3 | to = azurerm_private_dns_zone.storage_private_link_blob[0] 4 | } 5 | 6 | moved { 7 | from = azurerm_private_dns_zone_virtual_network_link.storage_private_link[0] 8 | to = azurerm_private_dns_zone_virtual_network_link.storage_private_link_blob[0] 9 | } 10 | 11 | moved { 12 | from = azurerm_private_dns_a_record.storage_private_link[0] 13 | to = azurerm_private_dns_a_record.storage_private_link_blob[0] 14 | } 15 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.9" 3 | 4 | required_providers { 5 | azurerm = { 6 | source = "hashicorp/azurerm" 7 | version = "~> 4.37" 8 | } 9 | 10 | azapi = { 11 | source = "Azure/azapi" 12 | version = "~> 1.13" 13 | } 14 | 15 | null = { 16 | source = "hashicorp/null" 17 | version = "~> 3.2" 18 | } 19 | 20 | archive = { 21 | source = "hashicorp/archive" 22 | version = "~> 2.6" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /resource-group.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_resource_group" "default" { 2 | count = local.existing_resource_group == "" ? 1 : 0 3 | 4 | name = local.resource_prefix 5 | location = local.azure_location 6 | tags = local.tags 7 | } 8 | 9 | resource "azurerm_management_lock" "default" { 10 | count = local.enable_resource_group_lock ? 1 : 0 11 | 12 | name = "${local.resource_prefix}-lock" 13 | scope = local.resource_group.id 14 | lock_level = "CanNotDelete" 15 | notes = "Resources in this Resource Group cannot be deleted. Please remove the lock first." 16 | } 17 | -------------------------------------------------------------------------------- /.terraform-docs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | formatter: "markdown table" 3 | version: "~> 0.16" 4 | sections: 5 | hide: 6 | - modules 7 | settings: 8 | anchor: true 9 | default: true 10 | description: false 11 | escape: true 12 | hide-empty: false 13 | html: true 14 | indent: 2 15 | lockfile: true 16 | read-comments: true 17 | required: true 18 | sensitive: true 19 | type: true 20 | sort: 21 | enabled: true 22 | by: name 23 | output: 24 | file: README.md 25 | mode: inject 26 | template: |- 27 | 28 | {{ .Content }} 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 12 | # .tfvars files are managed as part of configuration and so should be included in 13 | # version control. 14 | # 15 | *.tfvars 16 | 17 | # Ignore override files as they are usually used to override resources locally and so 18 | # are not checked in 19 | override.tf 20 | override.tf.json 21 | *_override.tf 22 | *_override.tf.json 23 | 24 | # Include override files you do wish to add to version control using negated pattern 25 | # 26 | # !example_override.tf 27 | 28 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 29 | # example: *tfplan* 30 | 31 | functions/dist/* 32 | .DS_Store 33 | -------------------------------------------------------------------------------- /private-endpoints.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_private_endpoint" "default" { 2 | for_each = local.private_endpoints 3 | 4 | name = "${local.resource_prefix}${each.key}" 5 | location = each.value.resource_group.location 6 | resource_group_name = each.value.resource_group.name 7 | subnet_id = each.value.subnet_id 8 | 9 | private_service_connection { 10 | name = "${local.resource_prefix}${each.key}connection" 11 | private_connection_resource_id = each.value.resource_id 12 | subresource_names = lookup(each.value, "subresource_names", []) 13 | is_manual_connection = lookup(each.value, "is_manual_connection", false) 14 | } 15 | 16 | private_dns_zone_group { 17 | name = "default" 18 | private_dns_zone_ids = [each.value.private_zone_id] 19 | } 20 | 21 | tags = local.tags 22 | } 23 | -------------------------------------------------------------------------------- /app-configuration.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_app_configuration" "default" { 2 | count = local.enable_app_configuration ? 1 : 0 3 | 4 | name = "${local.resource_prefix}appconfig" 5 | resource_group_name = local.resource_group.name 6 | location = local.resource_group.location 7 | sku = local.app_configuration_sku 8 | local_auth_enabled = true 9 | public_network_access = local.app_configuration_sku == "free" ? "Enabled" : "Disabled" 10 | 11 | tags = local.tags 12 | } 13 | 14 | resource "azurerm_role_assignment" "containerapp_appconfig_read" { 15 | count = local.enable_app_configuration && local.app_configuration_assign_role ? 1 : 0 16 | 17 | scope = azurerm_app_configuration.default[0].id 18 | role_definition_name = "App Configuration Data Reader" 19 | principal_id = azurerm_user_assigned_identity.containerapp[0].id 20 | description = "Allow Azure Container Apps to read data from App Configuration" 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 DFE-Digital 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /container-registry.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_container_registry" "acr" { 2 | count = local.enable_container_registry ? 1 : 0 3 | 4 | name = replace(local.resource_prefix, "-", "") 5 | resource_group_name = local.resource_group.name 6 | location = local.resource_group.location 7 | sku = local.registry_sku 8 | admin_enabled = local.registry_admin_enabled 9 | public_network_access_enabled = local.registry_public_access_enabled 10 | tags = local.tags 11 | retention_policy_in_days = local.registry_sku == "Premium" && local.enable_registry_retention_policy ? local.registry_retention_days : null 12 | network_rule_bypass_option = "None" 13 | 14 | dynamic "network_rule_set" { 15 | for_each = local.registry_sku == "Premium" && length(local.registry_ipv4_allow_list) > 0 ? { ip_rules : local.registry_ipv4_allow_list } : {} 16 | 17 | content { 18 | default_action = "Deny" 19 | 20 | dynamic "ip_rule" { 21 | for_each = network_rule_set.value 22 | 23 | content { 24 | action = "Allow" 25 | ip_range = ip_rule.value 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /schema/common-alert-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "data": { 4 | "properties": { 5 | "alertContext": { 6 | "properties": {}, 7 | "type": "object" 8 | }, 9 | "essentials": { 10 | "properties": { 11 | "alertContextVersion": { 12 | "type": "string" 13 | }, 14 | "alertId": { 15 | "type": "string" 16 | }, 17 | "alertRule": { 18 | "type": "string" 19 | }, 20 | "alertTargetIDs": { 21 | "items": { 22 | "type": "string" 23 | }, 24 | "type": "array" 25 | }, 26 | "description": { 27 | "type": "string" 28 | }, 29 | "essentialsVersion": { 30 | "type": "string" 31 | }, 32 | "firedDateTime": { 33 | "type": "string" 34 | }, 35 | "monitorCondition": { 36 | "type": "string" 37 | }, 38 | "monitoringService": { 39 | "type": "string" 40 | }, 41 | "originAlertId": { 42 | "type": "string" 43 | }, 44 | "resolvedDateTime": { 45 | "type": "string" 46 | }, 47 | "severity": { 48 | "type": "string" 49 | }, 50 | "signalType": { 51 | "type": "string" 52 | } 53 | }, 54 | "type": "object" 55 | } 56 | }, 57 | "type": "object" 58 | }, 59 | "schemaId": { 60 | "type": "string" 61 | } 62 | }, 63 | "type": "object" 64 | } 65 | -------------------------------------------------------------------------------- /key-vault.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_key_vault" "default" { 2 | count = local.escrow_container_app_secrets_in_key_vault && local.existing_key_vault == "" ? 1 : 0 3 | 4 | name = "${local.resource_prefix}-kv" 5 | location = local.azure_location 6 | resource_group_name = local.resource_group.name 7 | tenant_id = data.azurerm_subscription.current.tenant_id 8 | sku_name = "standard" 9 | soft_delete_retention_days = 7 10 | enable_rbac_authorization = true 11 | purge_protection_enabled = true 12 | 13 | network_acls { 14 | bypass = "AzureServices" 15 | default_action = "Deny" 16 | ip_rules = length(local.key_vault_access_ipv4) > 0 ? local.key_vault_access_ipv4 : null 17 | virtual_network_subnet_ids = local.launch_in_vnet ? [ 18 | azurerm_subnet.container_apps_infra_subnet[0].id 19 | ] : [] 20 | } 21 | 22 | tags = local.tags 23 | } 24 | 25 | resource "azurerm_key_vault_secret" "secret_app_setting" { 26 | for_each = local.escrow_container_app_secrets_in_key_vault ? nonsensitive(local.container_app_secrets) : {} 27 | 28 | name = each.value["name"] 29 | value = sensitive(each.value["value"]) 30 | key_vault_id = local.key_vault.id 31 | content_type = "Container App Environment Variable" 32 | } 33 | 34 | resource "azurerm_role_assignment" "kv_secret_reader" { 35 | count = local.escrow_container_app_secrets_in_key_vault && local.key_vault_managed_identity_assign_role ? 1 : 0 36 | 37 | scope = local.key_vault.id 38 | role_definition_name = "Key Vault Secret User" 39 | principal_id = azurerm_user_assigned_identity.containerapp[0].id 40 | description = "Allow Azure Container Apps to read secrets from an Azure Key Vault" 41 | } 42 | -------------------------------------------------------------------------------- /identity.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_user_assigned_identity" "containerapp" { 2 | count = local.enable_container_app_uami ? 1 : 0 3 | 4 | location = local.resource_group.location 5 | name = "${local.resource_prefix}-uami-containerapp" 6 | resource_group_name = local.resource_group.name 7 | tags = local.tags 8 | } 9 | 10 | resource "azurerm_role_assignment" "containerapp_acrpull" { 11 | count = local.registry_use_managed_identity && local.registry_managed_identity_assign_role ? 1 : 0 12 | 13 | scope = azurerm_container_registry.acr[0].id 14 | role_definition_name = "AcrPull" 15 | principal_id = azurerm_user_assigned_identity.containerapp[0].id 16 | description = "Allow Azure Container Apps to pull images from an Azure Container Registry" 17 | } 18 | 19 | resource "azurerm_user_assigned_identity" "mssql" { 20 | count = local.enable_mssql_database ? 1 : 0 21 | 22 | location = local.resource_group.location 23 | name = "${local.resource_prefix}-uami-mssql" 24 | resource_group_name = local.resource_group.name 25 | tags = local.tags 26 | } 27 | 28 | resource "azurerm_role_assignment" "mssql_storageblobdatacontributor" { 29 | count = local.enable_mssql_database && local.mssql_managed_identity_assign_role ? 1 : 0 30 | 31 | scope = azurerm_storage_account.mssql_security_storage[0].id 32 | role_definition_name = "Storage Blob Data Contributor" 33 | principal_id = azurerm_user_assigned_identity.mssql[0].id 34 | description = "Allow SQL Auditing to write reports and findings into the MSSQL Security Storage Account" 35 | } 36 | 37 | resource "azurerm_user_assigned_identity" "function_apps" { 38 | for_each = local.enable_linux_function_apps ? merge(local.linux_function_apps, local.linux_function_health_insights_api) : {} 39 | 40 | location = local.resource_group.location 41 | name = "${local.resource_prefix}-${each.key}-uami-function" 42 | resource_group_name = local.resource_group.name 43 | tags = local.tags 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration-terraform.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | terraform-validate: 10 | name: Terraform Validate 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v4 15 | 16 | - name: Check for terraform version mismatch 17 | run: | 18 | DOTFILE_VERSION=$(cat .terraform-version) 19 | TERRAFORM_IMAGE_REFERENCES=$(grep "uses: docker://hashicorp/terraform" .github/workflows/continuous-integration-terraform.yml | grep -v TERRAFORM_IMAGE_REFERENCES | wc -l | tr -d ' ') 20 | if [ "$(grep "docker://hashicorp/terraform:${DOTFILE_VERSION}" .github/workflows/continuous-integration-terraform.yml | wc -l | tr -d ' ')" != "$TERRAFORM_IMAGE_REFERENCES" ] 21 | then 22 | echo -e "\033[1;31mError: terraform version in .terraform-version file does not match docker://hashicorp/terraform versions in .github/workflows/continuous-integration-terraform.yml" 23 | exit 1 24 | fi 25 | 26 | - name: Run a Terraform init 27 | uses: docker://hashicorp/terraform:1.12.2 28 | with: 29 | entrypoint: terraform 30 | args: init 31 | 32 | - name: Run a Terraform validate 33 | uses: docker://hashicorp/terraform:1.12.2 34 | with: 35 | entrypoint: terraform 36 | args: validate 37 | 38 | - name: Run a Terraform format check 39 | uses: docker://hashicorp/terraform:1.12.2 40 | with: 41 | entrypoint: terraform 42 | args: fmt -check=true -diff=true 43 | terraform-docs-validation: 44 | name: Terraform Docs validation 45 | needs: terraform-validate 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Check out code 49 | uses: actions/checkout@v4 50 | with: 51 | ref: ${{ github.event.pull_request.head.ref }} 52 | 53 | - name: Generate Terraform docs 54 | uses: terraform-docs/gh-actions@v1.4.1 55 | with: 56 | working-dir: . 57 | config-file: .terraform-docs.yml 58 | output-file: README.md 59 | output-method: inject 60 | fail-on-diff: true 61 | -------------------------------------------------------------------------------- /function-app-service-bus.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_servicebus_namespace" "function_apps" { 2 | for_each = { 3 | for k, v in local.linux_function_apps : k => v if v["enable_service_bus"] 4 | } 5 | 6 | name = "${local.environment}${each.key}-function-app" 7 | location = local.resource_group.location 8 | resource_group_name = local.resource_group.name 9 | sku = "Standard" 10 | } 11 | 12 | resource "azurerm_servicebus_namespace_authorization_rule" "function_apps" { 13 | for_each = { 14 | for k, v in local.linux_function_apps : k => v if v["enable_service_bus"] 15 | } 16 | 17 | name = "${local.environment}${each.key}-function-app" 18 | namespace_id = azurerm_servicebus_namespace.function_apps[each.key].id 19 | listen = true 20 | send = true 21 | manage = false 22 | } 23 | 24 | resource "azurerm_servicebus_topic" "function_apps" { 25 | for_each = { 26 | for k, v in local.linux_function_apps : k => v if v["enable_service_bus"] 27 | } 28 | 29 | name = "${local.environment}${each.key}-function-app" 30 | namespace_id = azurerm_servicebus_namespace.function_apps[each.key].id 31 | partitioning_enabled = false 32 | max_size_in_megabytes = 1024 33 | } 34 | 35 | resource "azurerm_servicebus_subscription" "function_apps" { 36 | for_each = { 37 | for k, v in local.linux_function_apps : k => v if v["enable_service_bus"] 38 | } 39 | 40 | name = "${local.environment}${each.key}-function-app" 41 | topic_id = azurerm_servicebus_topic.function_apps[each.key].id 42 | max_delivery_count = 10 43 | lock_duration = "PT1M" 44 | dead_lettering_on_message_expiration = true 45 | } 46 | 47 | resource "azurerm_servicebus_subscription" "function_apps_additional" { 48 | for_each = toset(flatten([ 49 | for k, v in local.linux_function_apps : formatlist("${k}_%s", v["service_bus_additional_subscriptions"]) if v["enable_service_bus"] 50 | ])) 51 | 52 | name = "${local.environment}${each.key}-function-app" 53 | topic_id = azurerm_servicebus_topic.function_apps[split("_", each.key)[0]].id 54 | max_delivery_count = 10 55 | lock_duration = "PT1M" 56 | dead_lettering_on_message_expiration = true 57 | } 58 | -------------------------------------------------------------------------------- /postgres.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_postgresql_flexible_server" "default" { 2 | count = local.enable_postgresql_database ? 1 : 0 3 | 4 | name = "${local.resource_prefix}-pg-flexserv" 5 | resource_group_name = local.resource_group.name 6 | location = local.resource_group.location 7 | version = local.postgresql_server_version 8 | delegated_subnet_id = local.postgresql_network_connectivity_method == "private" ? azurerm_subnet.postgresql_subnet[0].id : null 9 | private_dns_zone_id = local.postgresql_network_connectivity_method == "private" ? azurerm_private_dns_zone.postgresql_private_link[0].id : null 10 | administrator_login = local.postgresql_administrator_login 11 | administrator_password = local.postgresql_administrator_password 12 | zone = local.postgresql_availability_zone 13 | storage_mb = local.postgresql_max_storage_mb 14 | sku_name = local.postgresql_sku_name 15 | tags = local.tags 16 | 17 | depends_on = [azurerm_private_dns_zone_virtual_network_link.postgresql_private_link[0]] 18 | } 19 | 20 | resource "azurerm_postgresql_flexible_server_database" "default" { 21 | count = local.enable_postgresql_database ? 1 : 0 22 | 23 | name = "${local.resource_prefix}-pg" 24 | server_id = azurerm_postgresql_flexible_server.default[0].id 25 | collation = local.postgresql_collation 26 | charset = local.postgresql_charset 27 | } 28 | 29 | # https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-extensions?WT.mc_id=Portal-Microsoft_Azure_OSSDatabases#postgres-13-extensions 30 | resource "azurerm_postgresql_flexible_server_configuration" "extensions" { 31 | count = local.enable_postgresql_database && local.postgresql_enabled_extensions != "" ? 1 : 0 32 | 33 | name = "azure.extensions" 34 | server_id = azurerm_postgresql_flexible_server.default[0].id 35 | value = local.postgresql_enabled_extensions 36 | } 37 | 38 | resource "azurerm_postgresql_flexible_server_firewall_rule" "firewall_rule" { 39 | for_each = local.enable_postgresql_database && local.postgresql_network_connectivity_method == "public" ? local.postgresql_firewall_ipv4_allow : {} 40 | 41 | name = each.key 42 | server_id = azurerm_postgresql_flexible_server.default[0].id 43 | start_ip_address = each.value.start_ip_address 44 | end_ip_address = each.value.end_ip_address 45 | } 46 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "azurerm_resource_group_default" { 2 | value = local.existing_resource_group == "" ? azurerm_resource_group.default[0] : null 3 | description = "Default Azure Resource Group" 4 | } 5 | 6 | output "azurerm_log_analytics_workspace_container_app" { 7 | value = azurerm_log_analytics_workspace.container_app 8 | description = "Container App Log Analytics Workspace" 9 | } 10 | 11 | output "azurerm_eventhub_container_app" { 12 | value = local.enable_event_hub ? azurerm_eventhub.container_app[0] : null 13 | description = "Container App Event Hub" 14 | } 15 | 16 | output "azurerm_dns_zone_name_servers" { 17 | value = local.enable_dns_zone ? azurerm_dns_zone.default[0].name_servers : null 18 | description = "Name servers of the DNS Zone" 19 | } 20 | 21 | output "azurerm_container_registry" { 22 | value = local.enable_container_registry ? azurerm_container_registry.acr[0] : null 23 | description = "Container Registry" 24 | } 25 | 26 | output "cdn_frontdoor_dns_records" { 27 | value = local.cdn_frontdoor_custom_domains_create_dns_records == false ? concat([ 28 | for domain in local.cdn_frontdoor_custom_domain_dns_names : { 29 | name = trim(domain, ".") == "" ? "@" : trim(domain, ".") 30 | type = "CNAME" 31 | ttl = 300 32 | value = azurerm_cdn_frontdoor_endpoint.endpoint[0].host_name 33 | } 34 | ], local.dns_zone_domain_name != "" ? [ 35 | for domain in local.cdn_frontdoor_custom_domain_dns_names : { 36 | name = trim(join(".", ["_dnsauth", domain]), ".") 37 | type = "TXT" 38 | ttl = 3600 39 | value = azurerm_cdn_frontdoor_custom_domain.custom_domain["${domain}${local.dns_zone_domain_name}"].validation_token 40 | } 41 | ] : [] 42 | ) : null 43 | description = "Azure Front Door DNS Records that must be created manually" 44 | } 45 | 46 | output "networking" { 47 | value = local.launch_in_vnet ? { 48 | vnet_id : local.existing_virtual_network == "" ? azurerm_virtual_network.default[0].id : null 49 | subnet_id : azurerm_subnet.container_apps_infra_subnet[0].id 50 | } : null 51 | description = "IDs for various VNet resources if created" 52 | } 53 | 54 | output "container_fqdn" { 55 | description = "FQDN for the Container App" 56 | value = local.container_fqdn 57 | } 58 | 59 | output "container_app_managed_identity" { 60 | description = "User-Assigned Managed Identity assigned to the Container App" 61 | value = local.registry_use_managed_identity ? azurerm_user_assigned_identity.containerapp[0] : null 62 | } 63 | 64 | output "container_app_environment_ingress_ip" { 65 | description = "Ingress IP address assigned to the Container App environment" 66 | value = local.container_app_environment.static_ip_address 67 | } 68 | -------------------------------------------------------------------------------- /webhook/slack.json: -------------------------------------------------------------------------------- 1 | { 2 | "channel": "${channel}", 3 | "text": "@{triggerBody()?['data']?['essentials']?['alertRule']}: @{triggerBody()?['data']?['essentials']?['monitorCondition']}", 4 | "blocks": [], 5 | "attachments": [ 6 | { 7 | "blocks": [ 8 | { 9 | "text": { 10 | "text": "", 11 | "type": "mrkdwn" 12 | }, 13 | "type": "section" 14 | }, 15 | { 16 | "text": { 17 | "text": "@{triggerBody()?['data']?['essentials']?['alertRule']}", 18 | "type": "plain_text" 19 | }, 20 | "type": "header" 21 | }, 22 | { 23 | "text": { 24 | "text": "_@{triggerBody()?['data']?['essentials']?['description']}_", 25 | "type": "mrkdwn" 26 | }, 27 | "type": "section" 28 | }, 29 | { 30 | "text": { 31 | "text": "*Alarm status:* @{triggerBody()?['data']?['essentials']?['monitorCondition']}", 32 | "type": "mrkdwn" 33 | }, 34 | "type": "section" 35 | }, 36 | { 37 | "fields": [ 38 | { 39 | "text": "*Resource Group*", 40 | "type": "mrkdwn" 41 | }, 42 | { 43 | "text": "@{variables('affectedResource')[4]} ", 44 | "type": "plain_text" 45 | }, 46 | { 47 | "text": "*Provider*", 48 | "type": "mrkdwn" 49 | }, 50 | { 51 | "text": "@{variables('AffectedResource')[6]} ", 52 | "type": "plain_text" 53 | }, 54 | { 55 | "text": "*Severity*", 56 | "type": "mrkdwn" 57 | }, 58 | { 59 | "text": "@{triggerBody()?['data']?['essentials']?['severity']} ", 60 | "type": "plain_text" 61 | }, 62 | { 63 | "text": "*Metric definition*", 64 | "type": "mrkdwn" 65 | }, 66 | { 67 | "text": "@{variables('alarmContext')['metricName']} @{variables('alarmContext')['timeAggregation']} @{variables('alarmContext')['operator']} @{variables('alarmContext')['threshold']}", 68 | "type": "plain_text" 69 | }, 70 | { 71 | "text": "*Recorded value*", 72 | "type": "mrkdwn" 73 | }, 74 | { 75 | "text": "@{variables('alarmContext')['metricValue']} ", 76 | "type": "plain_text" 77 | } 78 | ], 79 | "type": "section" 80 | } 81 | ], 82 | "color": "@{if(equals(triggerBody()?['data']?['essentials']?['monitorCondition'], 'Resolved'), '#50C878', '#D22B2B')}" 83 | } 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /app-insights.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_application_insights" "main" { 2 | count = local.enable_app_insights_integration ? 1 : 0 3 | 4 | name = "${local.resource_prefix}-insights" 5 | location = local.resource_group.location 6 | resource_group_name = local.resource_group.name 7 | application_type = "web" 8 | workspace_id = azurerm_log_analytics_workspace.app_insights[0].id 9 | retention_in_days = local.app_insights_retention_days 10 | tags = local.tags 11 | } 12 | 13 | resource "azurerm_application_insights" "function_apps" { 14 | for_each = local.enable_app_insights_integration ? merge(local.linux_function_apps, local.linux_function_health_insights_api) : {} 15 | 16 | name = "${local.resource_prefix}-${each.key}-insights" 17 | location = local.resource_group.location 18 | resource_group_name = local.resource_group.name 19 | application_type = "web" 20 | workspace_id = azurerm_log_analytics_workspace.app_insights[0].id 21 | retention_in_days = local.app_insights_retention_days 22 | tags = local.tags 23 | } 24 | 25 | resource "azurerm_log_analytics_workspace" "app_insights" { 26 | count = local.enable_app_insights_integration ? 1 : 0 27 | 28 | name = "${local.resource_prefix}-insights" 29 | location = local.resource_group.location 30 | resource_group_name = local.resource_group.name 31 | sku = "PerGB2018" 32 | retention_in_days = local.app_insights_retention_days 33 | tags = local.tags 34 | } 35 | 36 | resource "azurerm_application_insights_standard_web_test" "main" { 37 | count = local.enable_app_insights_integration && local.enable_monitoring ? 1 : 0 38 | 39 | name = "${local.resource_prefix}-http" 40 | resource_group_name = local.resource_group.name 41 | location = local.resource_group.location 42 | application_insights_id = azurerm_application_insights.main[0].id 43 | timeout = 10 44 | description = "Regional HTTP availability check" 45 | enabled = true 46 | retry_enabled = true 47 | 48 | geo_locations = [ 49 | "emea-nl-ams-azr", # West Europe 50 | "emea-se-sto-edge", # UK West 51 | "emea-ru-msa-edge" # UK South 52 | ] 53 | 54 | request { 55 | url = local.monitor_http_availability_url 56 | http_verb = local.monitor_http_availability_verb 57 | 58 | header { 59 | name = "X-AppInsights-HttpTest" 60 | value = azurerm_application_insights.main[0].name 61 | } 62 | } 63 | 64 | validation_rules { 65 | expected_status_code = 0 # 0 = response code < 400 66 | } 67 | 68 | tags = merge( 69 | local.tags, 70 | { "hidden-link:${azurerm_application_insights.main[0].id}" = "Resource" }, 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /script/apply-tags-to-container-app-env-mc-resource-group: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # exit on failures 4 | set -e 5 | set -o pipefail 6 | 7 | usage() { 8 | echo "Usage: $(basename "$0") [OPTIONS]" 1>&2 9 | echo " -h - help" 10 | echo " -n - container app environment name" 11 | echo " -r - container app environment resource group name" 12 | echo " -t - json encoded tags ({\"key\":\"value\")" 13 | exit 1 14 | } 15 | 16 | # if there are no arguments passed exit with usage 17 | if [ $# -lt 1 ]; 18 | then 19 | usage 20 | fi 21 | 22 | while getopts "n:r:t:h" opt; do 23 | case $opt in 24 | n) 25 | CONTAINER_APP_ENVIRONMENT_NAME=$OPTARG 26 | ;; 27 | r) 28 | CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME=$OPTARG 29 | ;; 30 | t) 31 | TAGS=$OPTARG 32 | ;; 33 | h) 34 | usage 35 | exit;; 36 | *) 37 | usage 38 | exit;; 39 | esac 40 | done 41 | 42 | if [[ 43 | -z "$CONTAINER_APP_ENVIRONMENT_NAME" || 44 | -z "$CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME" || 45 | -z "$TAGS" 46 | ]]; then 47 | usage 48 | fi 49 | 50 | CONTAINER_APP_PROVISIONING_STATE="InProgress" 51 | 52 | while [ "$CONTAINER_APP_PROVISIONING_STATE" == "InProgress" ] 53 | do 54 | CONTAINER_APP=$(az containerapp env show --name "$CONTAINER_APP_ENVIRONMENT_NAME" --resource-group "$CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME") 55 | CONTAINER_APP_PROVISIONING_STATE=$(echo "$CONTAINER_APP" | jq -r ".properties.provisioningState") 56 | if [[ 57 | "$CONTAINER_APP_PROVISIONING_STATE" != "InProgress" && 58 | "$CONTAINER_APP_PROVISIONING_STATE" != "Succeeded" 59 | ]] 60 | then 61 | echo "Failed to add tags. Container App Environment is '$CONTAINER_APP_PROVISIONING_STATE'" 62 | exit 1 63 | fi 64 | if [ "$CONTAINER_APP_PROVISIONING_STATE" == "Succeeded" ] 65 | then 66 | break 67 | fi 68 | echo "Waiting for container app environment to be provisioned ..." 69 | sleep 5 70 | done 71 | 72 | CONTAINER_APP_ENVIRONMENT_DEFAULT_DOMAIN=$(echo "$CONTAINER_APP" | jq -r ".properties.defaultDomain") 73 | CONTAINER_APP_ENVIRONMENT_LOCATION=$(echo "$CONTAINER_APP" | jq -r ".location" | awk '{gsub(" ", ""); print tolower($0)}') 74 | CONTAINER_APP_ENVIRONMENT_SLUG=$(echo "$CONTAINER_APP_ENVIRONMENT_DEFAULT_DOMAIN" | cut -d'.' -f1 ) 75 | MC_RESOURCE_GROUP_NAME="MC_${CONTAINER_APP_ENVIRONMENT_SLUG}-rg_${CONTAINER_APP_ENVIRONMENT_SLUG}_${CONTAINER_APP_ENVIRONMENT_LOCATION}" 76 | MC_RESOURCE_GROUP="$(az group show --name "$MC_RESOURCE_GROUP_NAME")" 77 | MC_RESOURCE_GROUP_ID="$(echo "$MC_RESOURCE_GROUP" | jq -r ".id")" 78 | TAGS="$(echo "$TAGS" | jq -r 'keys[] as $k | "\($k)=\(.[$k])"')" 79 | 80 | echo "Adding tags to $MC_RESOURCE_GROUP_ID ..." 81 | while read -r TAG 82 | do 83 | az tag update --operation "Merge" --resource-id "$MC_RESOURCE_GROUP_ID" --tags "$TAG" 84 | done <<< "$TAGS" 85 | -------------------------------------------------------------------------------- /redis-cache.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_redis_cache" "default" { 2 | count = local.enable_redis_cache ? 1 : 0 3 | 4 | name = "${local.resource_prefix}default" 5 | location = local.resource_group.location 6 | resource_group_name = local.resource_group.name 7 | capacity = local.redis_cache_capacity 8 | family = local.redis_cache_family 9 | sku_name = local.redis_cache_sku 10 | redis_version = local.redis_cache_version 11 | non_ssl_port_enabled = false 12 | minimum_tls_version = "1.2" 13 | public_network_access_enabled = local.launch_in_vnet ? ( 14 | local.redis_cache_sku == "Premium" ? false : true 15 | ) : true 16 | subnet_id = local.launch_in_vnet ? ( 17 | local.redis_cache_sku == "Premium" ? azurerm_subnet.redis_cache_subnet[0].id : null 18 | ) : null 19 | 20 | redis_configuration { 21 | maxmemory_reserved = local.redis_config.maxmemory_reserved 22 | maxmemory_delta = local.redis_config.maxmemory_delta 23 | maxmemory_policy = local.redis_config.maxfragmentationmemory_reserved 24 | maxfragmentationmemory_reserved = local.redis_config.maxmemory_policy 25 | } 26 | 27 | patch_schedule { 28 | day_of_week = local.redis_cache_patch_schedule_day 29 | start_hour_utc = local.redis_cache_patch_schedule_hour 30 | } 31 | 32 | tags = local.tags 33 | } 34 | 35 | resource "azurerm_redis_firewall_rule" "container_app_default_static_ip" { 36 | for_each = local.enable_redis_cache ? azurerm_container_app.container_apps : {} 37 | 38 | name = "${replace(local.resource_prefix, "-", "")}fw${each.key}" 39 | redis_cache_name = azurerm_redis_cache.default[0].name 40 | resource_group_name = local.resource_group.name 41 | start_ip = each.value.outbound_ip_addresses[0] 42 | end_ip = each.value.outbound_ip_addresses[0] 43 | } 44 | 45 | resource "azurerm_redis_firewall_rule" "default" { 46 | for_each = local.enable_redis_cache ? toset(local.redis_cache_firewall_ipv4_allow_list) : [] 47 | 48 | name = "${replace(local.resource_prefix, "-", "")}fw${each.key}" 49 | redis_cache_name = azurerm_redis_cache.default[0].name 50 | resource_group_name = local.resource_group.name 51 | start_ip = each.value 52 | end_ip = each.value 53 | } 54 | 55 | resource "azurerm_monitor_diagnostic_setting" "default_redis_cache" { 56 | count = local.enable_monitoring ? ( 57 | local.enable_redis_cache ? 1 : 0 58 | ) : 0 59 | 60 | name = "${local.resource_prefix}-default-redis-diag" 61 | target_resource_id = azurerm_redis_cache.default[0].id 62 | log_analytics_workspace_id = azurerm_log_analytics_workspace.container_app.id 63 | eventhub_name = local.enable_event_hub ? azurerm_eventhub.container_app[0].name : null 64 | 65 | enabled_log { 66 | category = "ConnectedClientList" 67 | } 68 | 69 | # The below metrics are kept in to avoid a diff in the Terraform Plan output 70 | metric { 71 | category = "AllMetrics" 72 | enabled = false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /functions/src/health-api/function_app.py: -------------------------------------------------------------------------------- 1 | from azure.identity import DefaultAzureCredential 2 | from azure.monitor.query import LogsQueryClient, LogsQueryStatus 3 | from azure.core.exceptions import HttpResponseError 4 | from datetime import timedelta 5 | import azure.functions as func 6 | import logging 7 | import os 8 | import json 9 | 10 | credential = DefaultAzureCredential() 11 | client = LogsQueryClient(credential) 12 | 13 | response_headers = { "Content-Type": "application/json" } 14 | query = """ availabilityResults | project location, tobool(success), timestamp | order by timestamp desc | take 3""" 15 | 16 | app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION) 17 | 18 | @app.function_name(name="http_trigger") 19 | @app.route(route="http_trigger") 20 | def http_trigger(req: func.HttpRequest) -> func.HttpResponse: 21 | logging.info('Python HTTP trigger function processed a request.') 22 | 23 | key = "TARGET_LOG_ANALYTICS_RESOURCE_ID" 24 | 25 | if key in os.environ.keys(): 26 | log_resource_id = os.environ[key] 27 | else: 28 | message = "The key {} is not present in os.environ".format(key) 29 | logging.error(message) 30 | 31 | return func.HttpResponse( 32 | json.dumps({ 33 | 'message': message, 34 | 'body': [] 35 | }), 36 | status_code=500, 37 | headers=response_headers 38 | ) 39 | 40 | try: 41 | response = client.query_resource( 42 | log_resource_id, 43 | query, 44 | timespan=timedelta(minutes=15) 45 | ) 46 | 47 | if response.status == LogsQueryStatus.SUCCESS: 48 | data = response.tables 49 | struct = [] 50 | 51 | for table in data: 52 | for row in table.rows: 53 | struct.append({ 54 | "location": row[0], 55 | "success": bool(row[1]), 56 | "timestamp": str(row[2]) 57 | }) 58 | 59 | logging.info(struct) 60 | 61 | return func.HttpResponse( 62 | json.dumps({ 63 | 'message': response.status, 64 | 'body': struct 65 | }), 66 | status_code=200, 67 | headers=response_headers 68 | ) 69 | else: 70 | error = response.partial_error 71 | data = response.partial_data 72 | logging.error(error) 73 | 74 | return func.HttpResponse( 75 | json.dumps({ 76 | 'message': response.status, 77 | 'body': data 78 | }), 79 | status_code=400, 80 | headers=response_headers 81 | ) 82 | 83 | except HttpResponseError as err: 84 | logging.error("something fatal happened") 85 | logging.error(err) 86 | 87 | return func.HttpResponse( 88 | json.dumps({ 89 | 'message': err, 90 | 'body': [] 91 | }), 92 | status_code=500, 93 | headers=response_headers 94 | ) 95 | -------------------------------------------------------------------------------- /data.tf: -------------------------------------------------------------------------------- 1 | data "azurerm_virtual_network" "existing_virtual_network" { 2 | count = local.existing_virtual_network == "" ? 0 : 1 3 | 4 | name = local.existing_virtual_network 5 | resource_group_name = local.existing_resource_group 6 | } 7 | 8 | data "azurerm_resource_group" "existing_resource_group" { 9 | count = local.existing_resource_group == "" ? 0 : 1 10 | 11 | name = local.existing_resource_group 12 | } 13 | 14 | data "azurerm_container_app_environment" "existing_container_app_environment" { 15 | count = local.existing_container_app_environment.name == "" ? 0 : 1 16 | 17 | name = local.existing_container_app_environment.name 18 | resource_group_name = local.existing_container_app_environment.resource_group 19 | } 20 | 21 | data "azurerm_subscription" "current" {} 22 | 23 | data "azurerm_logic_app_workflow" "existing_logic_app_workflow" { 24 | count = local.existing_logic_app_workflow.name == "" ? 0 : 1 25 | 26 | name = local.existing_logic_app_workflow.name 27 | resource_group_name = local.existing_logic_app_workflow.resource_group_name 28 | } 29 | 30 | # There is not currently a way to get the full HTTP Trigger callback URL from a Logic App 31 | # so we have to use AzAPI to query the Logic App Workflow for the value instead. 32 | # https://github.com/hashicorp/terraform-provider-azurerm/issues/18866 33 | data "azapi_resource_action" "existing_logic_app_workflow_callback_url" { 34 | count = local.existing_logic_app_workflow.name == "" ? 0 : 1 35 | 36 | resource_id = "${data.azurerm_logic_app_workflow.existing_logic_app_workflow[0].id}/triggers/http-request-trigger" 37 | action = "listCallbackUrl" 38 | type = "Microsoft.Logic/workflows/triggers@2019-05-01" 39 | 40 | depends_on = [ 41 | data.azurerm_logic_app_workflow.existing_logic_app_workflow[0] 42 | ] 43 | 44 | response_export_values = ["value"] 45 | } 46 | 47 | data "azurerm_key_vault" "existing_key_vault" { 48 | count = local.existing_key_vault == "" ? 0 : 1 49 | 50 | name = local.existing_key_vault 51 | resource_group_name = local.existing_resource_group 52 | } 53 | 54 | data "archive_file" "azure_function" { 55 | for_each = local.linux_function_health_insights_api 56 | 57 | type = "zip" 58 | output_path = "${path.module}/functions/dist/${each.key}.zip" 59 | source_dir = "${path.module}/functions/src/${each.key}/" 60 | } 61 | 62 | data "azurerm_application_gateway" "existing_agw" { 63 | count = local.launch_in_vnet && local.restrict_container_apps_to_agw_inbound_only && local.container_apps_allow_agw_resource.name != "" ? 1 : 0 64 | 65 | name = local.container_apps_allow_agw_resource.name 66 | resource_group_name = local.container_apps_allow_agw_resource.resource_group_name 67 | } 68 | 69 | data "azurerm_virtual_network" "existing_agw_vnet" { 70 | count = local.launch_in_vnet && local.restrict_container_apps_to_agw_inbound_only && local.container_apps_allow_agw_resource.vnet_name != "" ? 1 : 0 71 | 72 | name = local.container_apps_allow_agw_resource.vnet_name 73 | resource_group_name = local.container_apps_allow_agw_resource.resource_group_name 74 | } 75 | 76 | data "azurerm_public_ip" "existing_agw_ip" { 77 | count = local.container_apps_allow_agw_pip_resource_id != null ? 1 : 0 78 | 79 | name = element(local.container_apps_allow_agw_pip_resource_id, 8) 80 | resource_group_name = element(local.container_apps_allow_agw_pip_resource_id, 4) 81 | } 82 | 83 | resource "terraform_data" "function_app_package_sha" { 84 | for_each = local.linux_function_health_insights_api 85 | 86 | input = filesha256(data.archive_file.azure_function[each.key].output_path) 87 | } 88 | -------------------------------------------------------------------------------- /logging.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_log_analytics_workspace" "container_app" { 2 | name = "${local.resource_prefix}containerapp" 3 | resource_group_name = local.resource_group.name 4 | location = local.resource_group.location 5 | sku = "PerGB2018" 6 | retention_in_days = 30 7 | tags = local.tags 8 | } 9 | 10 | resource "azurerm_log_analytics_workspace" "function_app" { 11 | count = local.enable_linux_function_apps ? 1 : 0 12 | name = "${local.resource_prefix}functionapp" 13 | resource_group_name = local.resource_group.name 14 | location = local.resource_group.location 15 | sku = "PerGB2018" 16 | retention_in_days = 30 17 | tags = local.tags 18 | } 19 | 20 | resource "azurerm_log_analytics_data_export_rule" "container_app" { 21 | count = local.enable_event_hub ? 1 : 0 22 | name = "${local.resource_prefix}containerapp" 23 | resource_group_name = local.resource_group.name 24 | workspace_resource_id = azurerm_log_analytics_workspace.container_app.id 25 | destination_resource_id = azurerm_eventhub.container_app[0].id 26 | table_names = local.eventhub_export_log_analytics_table_names 27 | enabled = true 28 | } 29 | 30 | resource "azurerm_log_analytics_query_pack" "container_app" { 31 | name = "${local.resource_prefix}containerapp" 32 | resource_group_name = local.resource_group.name 33 | location = local.resource_group.location 34 | tags = local.tags 35 | } 36 | 37 | resource "azurerm_eventhub_namespace" "container_app" { 38 | count = local.enable_event_hub ? 1 : 0 39 | name = "${local.resource_prefix}eventhubnamespace" 40 | location = local.resource_group.location 41 | resource_group_name = local.resource_group.name 42 | sku = "Standard" 43 | capacity = 1 44 | tags = local.tags 45 | } 46 | 47 | resource "azurerm_monitor_diagnostic_setting" "event_hub" { 48 | count = local.enable_event_hub ? 1 : 0 49 | 50 | name = "${local.resource_prefix}-eventhub-diag" 51 | target_resource_id = azurerm_eventhub_namespace.container_app[0].id 52 | log_analytics_workspace_id = azurerm_log_analytics_workspace.container_app.id 53 | eventhub_name = azurerm_eventhub.container_app[0].name 54 | 55 | enabled_log { 56 | category_group = "Audit" 57 | } 58 | } 59 | 60 | resource "azurerm_eventhub" "container_app" { 61 | count = local.enable_event_hub ? 1 : 0 62 | name = "${local.resource_prefix}containerapp" 63 | namespace_name = azurerm_eventhub_namespace.container_app[0].name 64 | resource_group_name = local.resource_group.name 65 | partition_count = 2 66 | message_retention = 7 67 | } 68 | 69 | resource "azurerm_eventhub_consumer_group" "logstash" { 70 | count = local.enable_event_hub && local.enable_logstash_consumer ? 1 : 0 71 | name = "${local.resource_prefix}eventhubconsumergroup" 72 | namespace_name = azurerm_eventhub_namespace.container_app[0].name 73 | eventhub_name = azurerm_eventhub.container_app[0].name 74 | resource_group_name = local.resource_group.name 75 | user_metadata = "Logstash" 76 | } 77 | 78 | resource "azurerm_eventhub_authorization_rule" "listen_only" { 79 | count = local.enable_event_hub && local.enable_logstash_consumer ? 1 : 0 80 | name = "${local.resource_prefix}eventhublistenrule" 81 | namespace_name = azurerm_eventhub_namespace.container_app[0].name 82 | eventhub_name = azurerm_eventhub.container_app[0].name 83 | resource_group_name = local.resource_group.name 84 | listen = true 85 | send = false 86 | manage = false 87 | } 88 | -------------------------------------------------------------------------------- /custom-container-apps.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_container_app" "custom_container_apps" { 2 | for_each = local.custom_container_apps 3 | 4 | name = each.key 5 | container_app_environment_id = each.value.container_app_environment_id == "" ? local.container_app_environment.id : each.value.container_app_environment_id 6 | resource_group_name = each.value.resource_group_name == "" ? local.resource_group.name : each.value.resource_group_name 7 | revision_mode = each.value.revision_mode 8 | workload_profile_name = each.value.container_app_environment_id == "" && local.container_app_environment_workload_profile_type != "Consumption" ? local.container_app_environment_workload_profile_type : null 9 | 10 | dynamic "ingress" { 11 | for_each = each.value.ingress != null ? [1] : [] 12 | 13 | content { 14 | external_enabled = each.value.ingress.external_enabled 15 | target_port = each.value.ingress.target_port 16 | traffic_weight { 17 | percentage = each.value.ingress.traffic_weight.percentage 18 | latest_revision = true 19 | } 20 | } 21 | } 22 | 23 | dynamic "secret" { 24 | for_each = { for i, v in concat( 25 | local.enable_app_insights_integration ? [ 26 | { 27 | name = "applicationinsights--connectionstring", 28 | value = azurerm_application_insights.main[0].connection_string 29 | }, 30 | { 31 | name = "applicationinsights--instrumentationkey", 32 | value = azurerm_application_insights.main[0].instrumentation_key 33 | } 34 | ] : [], 35 | each.value.secrets 36 | ) : v.name => v } 37 | 38 | content { 39 | name = secret.value["name"] 40 | value = secret.value["value"] 41 | } 42 | } 43 | 44 | dynamic "identity" { 45 | for_each = each.value.identity 46 | 47 | content { 48 | type = identity.value.type 49 | identity_ids = identity.value.identity_ids 50 | } 51 | } 52 | 53 | dynamic "registry" { 54 | for_each = each.value.registry != null ? [1] : [] 55 | 56 | content { 57 | server = each.value.registry.server != "" ? each.value.registry.server : local.registry_server 58 | username = each.value.registry.identity == "" ? each.value.registry.username != "" ? each.value.registry.username : local.registry_username : null 59 | password_secret_name = each.value.registry.identity == "" ? each.value.registry.password_secret_name != "" ? each.value.registry.password_secret_name : "acr-password" : null 60 | identity = each.value.registry.identity != "" ? each.value.registry.identity : null 61 | } 62 | } 63 | 64 | template { 65 | container { 66 | name = each.key 67 | image = each.value.image 68 | cpu = each.value.cpu 69 | memory = "${each.value.memory}Gi" 70 | command = each.value.command 71 | dynamic "liveness_probe" { 72 | for_each = each.value.liveness_probes 73 | 74 | content { 75 | interval_seconds = liveness_probe.interval_seconds 76 | transport = liveness_probe.transport 77 | port = liveness_probe.port 78 | path = liveness_probe.path 79 | } 80 | } 81 | dynamic "env" { 82 | for_each = { for i, v in concat( 83 | local.enable_app_insights_integration ? [ 84 | { 85 | "name" : "ApplicationInsights__ConnectionString", 86 | "secretRef" : "applicationinsights--connectionstring" 87 | }, 88 | { 89 | "name" : "ApplicationInsights__InstrumentationKey", 90 | "secretRef" : "applicationinsights--instrumentationkey" 91 | } 92 | ] : [], 93 | each.value.env, 94 | ) : v.name => v } 95 | 96 | content { 97 | name = env.value["name"] 98 | secret_name = lookup(env.value, "secretRef", null) 99 | value = lookup(env.value, "value", null) 100 | } 101 | } 102 | } 103 | min_replicas = each.value.min_replicas 104 | max_replicas = each.value.max_replicas 105 | } 106 | 107 | tags = local.tags 108 | } 109 | -------------------------------------------------------------------------------- /network-watcher.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_network_watcher" "default" { 2 | count = local.enable_network_watcher ? 1 : 0 3 | 4 | name = "${local.resource_prefix}default" 5 | location = local.resource_group.location 6 | resource_group_name = local.resource_group.name 7 | 8 | tags = local.tags 9 | } 10 | 11 | resource "azurerm_storage_account" "default_network_watcher_nsg_flow_logs" { 12 | count = local.network_watcher_name != "" ? 1 : 0 13 | 14 | name = "${replace(local.resource_prefix, "-", "")}nwnsgd" 15 | resource_group_name = local.resource_group.name 16 | location = local.resource_group.location 17 | account_tier = "Standard" 18 | account_kind = "StorageV2" 19 | account_replication_type = "LRS" 20 | min_tls_version = "TLS1_2" 21 | https_traffic_only_enabled = true 22 | public_network_access_enabled = true 23 | allow_nested_items_to_be_public = false 24 | cross_tenant_replication_enabled = false 25 | 26 | blob_properties { 27 | delete_retention_policy { 28 | days = 7 29 | } 30 | container_delete_retention_policy { 31 | days = 7 32 | } 33 | } 34 | 35 | sas_policy { 36 | expiration_period = local.storage_account_sas_expiration_period 37 | } 38 | 39 | tags = local.tags 40 | } 41 | 42 | resource "azapi_update_resource" "default_network_watcher_nsg_storage_key_rotation_reminder" { 43 | count = local.network_watcher_name != "" ? 1 : 0 44 | 45 | type = "Microsoft.Storage/storageAccounts@2023-01-01" 46 | resource_id = azurerm_storage_account.default_network_watcher_nsg_flow_logs[0].id 47 | body = jsonencode({ 48 | properties = { 49 | keyPolicy : { 50 | keyExpirationPeriodInDays : local.network_watcher_nsg_storage_access_key_rotation_reminder_days 51 | } 52 | } 53 | }) 54 | 55 | depends_on = [ 56 | azurerm_storage_account.default_network_watcher_nsg_flow_logs[0] 57 | ] 58 | } 59 | 60 | resource "azurerm_log_analytics_workspace" "default_network_watcher_nsg_flow_logs" { 61 | count = local.network_watcher_name != "" && local.enable_network_watcher_traffic_analytics ? 1 : 0 62 | 63 | name = "${local.resource_prefix}nwnsgdefault" 64 | location = local.resource_group.location 65 | resource_group_name = local.resource_group.name 66 | sku = "PerGB2018" 67 | retention_in_days = 30 68 | 69 | tags = local.tags 70 | } 71 | 72 | resource "azurerm_network_watcher_flow_log" "default_network_watcher_nsg" { 73 | count = local.network_watcher_name != "" ? 1 : 0 74 | 75 | network_watcher_name = local.network_watcher_name 76 | resource_group_name = local.network_watcher_resource_group_name 77 | name = "${local.resource_prefix}nsg${local.virtual_network.name}" 78 | 79 | target_resource_id = local.virtual_network.id 80 | storage_account_id = azurerm_storage_account.default_network_watcher_nsg_flow_logs[0].id 81 | enabled = true 82 | 83 | retention_policy { 84 | enabled = local.network_watcher_flow_log_retention == 0 ? false : true 85 | days = local.network_watcher_flow_log_retention 86 | } 87 | 88 | dynamic "traffic_analytics" { 89 | for_each = local.network_watcher_name != "" && local.enable_network_watcher_traffic_analytics ? [0] : [] 90 | content { 91 | enabled = true 92 | workspace_id = azurerm_log_analytics_workspace.default_network_watcher_nsg_flow_logs[0].workspace_id 93 | workspace_region = azurerm_log_analytics_workspace.default_network_watcher_nsg_flow_logs[0].location 94 | workspace_resource_id = azurerm_log_analytics_workspace.default_network_watcher_nsg_flow_logs[0].id 95 | interval_in_minutes = local.network_watcher_traffic_analytics_interval 96 | } 97 | } 98 | 99 | tags = local.tags 100 | } 101 | 102 | resource "azurerm_storage_account_network_rules" "default_network_watcher_nsg_flow_logs" { 103 | count = local.network_watcher_name != "" ? 1 : 0 104 | 105 | storage_account_id = azurerm_storage_account.default_network_watcher_nsg_flow_logs[0].id 106 | default_action = "Deny" 107 | bypass = ["AzureServices"] 108 | virtual_network_subnet_ids = [] 109 | ip_rules = [] 110 | } 111 | 112 | resource "azurerm_monitor_diagnostic_setting" "nsg_flow_logs" { 113 | count = local.network_watcher_name != "" ? 1 : 0 114 | 115 | name = "${local.resource_prefix}-storage-nwnsgd-diag" 116 | target_resource_id = "${azurerm_storage_account.default_network_watcher_nsg_flow_logs[0].id}/blobServices/default" 117 | log_analytics_workspace_id = azurerm_log_analytics_workspace.default_network_watcher_nsg_flow_logs[0].id 118 | 119 | enabled_log { 120 | category_group = "Audit" 121 | } 122 | 123 | # The below metrics are kept in to avoid a diff in the Terraform Plan output 124 | metric { 125 | category = "Capacity" 126 | enabled = false 127 | } 128 | 129 | metric { 130 | category = "Transaction" 131 | enabled = false 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /dns.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_dns_zone" "default" { 2 | count = local.enable_dns_zone ? 1 : 0 3 | 4 | name = local.dns_zone_domain_name 5 | resource_group_name = local.resource_group.name 6 | 7 | dynamic "soa_record" { 8 | for_each = lookup(local.dns_zone_soa_record, "email", "") == "" ? [] : [1] 9 | 10 | content { 11 | email = lookup(local.dns_zone_soa_record, "email", "hello.example.com") 12 | host_name = lookup(local.dns_zone_soa_record, "host_name", "ns1-03.azure-dns.com.") 13 | expire_time = lookup(local.dns_zone_soa_record, "expire_time", "2419200") 14 | minimum_ttl = lookup(local.dns_zone_soa_record, "minimum_ttl", "300") 15 | refresh_time = lookup(local.dns_zone_soa_record, "refresh_time", "3600") 16 | retry_time = lookup(local.dns_zone_soa_record, "retry_time", "300") 17 | serial_number = lookup(local.dns_zone_soa_record, "serial_number", "1") 18 | ttl = lookup(local.dns_zone_soa_record, "ttl", "3600") 19 | } 20 | } 21 | 22 | tags = local.tags 23 | } 24 | 25 | resource "azurerm_dns_txt_record" "frontdoor_custom_domain" { 26 | for_each = local.enable_dns_zone && local.cdn_frontdoor_custom_domains_create_dns_records ? local.cdn_frontdoor_custom_domain_dns_names : [] 27 | 28 | name = trim(join(".", ["_dnsauth", each.value]), ".") 29 | zone_name = azurerm_dns_zone.default[0].name 30 | resource_group_name = local.resource_group.name 31 | ttl = 3600 32 | 33 | record { 34 | value = azurerm_cdn_frontdoor_custom_domain.custom_domain["${each.value}${local.dns_zone_domain_name}"].validation_token 35 | } 36 | 37 | tags = local.tags 38 | } 39 | 40 | resource "azurerm_dns_a_record" "frontdoor_custom_domain" { 41 | for_each = local.enable_dns_zone && local.cdn_frontdoor_custom_domains_create_dns_records ? local.cdn_frontdoor_custom_domain_dns_names : [] 42 | 43 | name = trim(each.value, ".") == "" ? "@" : trim(each.value, ".") 44 | zone_name = azurerm_dns_zone.default[0].name 45 | resource_group_name = local.resource_group.name 46 | ttl = 60 47 | target_resource_id = azurerm_cdn_frontdoor_endpoint.endpoint[0].id 48 | 49 | tags = local.tags 50 | } 51 | 52 | resource "azurerm_dns_txt_record" "custom_container_frontdoor_custom_domain" { 53 | for_each = local.enable_dns_zone && local.cdn_frontdoor_custom_domains_create_dns_records ? local.custom_container_apps_cdn_frontdoor_custom_domain_dns_names : {} 54 | 55 | name = trim(join(".", ["_dnsauth", each.value]), ".") 56 | zone_name = azurerm_dns_zone.default[0].name 57 | resource_group_name = local.resource_group.name 58 | ttl = 3600 59 | 60 | record { 61 | value = azurerm_cdn_frontdoor_custom_domain.custom_container_apps[each.key].validation_token 62 | } 63 | 64 | tags = local.tags 65 | } 66 | 67 | resource "azurerm_dns_a_record" "custom_container_frontdoor_custom_domain" { 68 | for_each = local.enable_dns_zone && local.cdn_frontdoor_custom_domains_create_dns_records ? local.custom_container_apps_cdn_frontdoor_custom_domain_dns_names : {} 69 | 70 | name = trim(each.value, ".") == "" ? "@" : trim(each.value, ".") 71 | zone_name = azurerm_dns_zone.default[0].name 72 | resource_group_name = local.resource_group.name 73 | ttl = 60 74 | target_resource_id = azurerm_cdn_frontdoor_endpoint.custom_container_apps[each.key].id 75 | 76 | tags = local.tags 77 | } 78 | 79 | resource "azurerm_dns_a_record" "dns_a_records" { 80 | for_each = local.enable_dns_zone ? local.dns_a_records : {} 81 | 82 | name = each.key 83 | zone_name = azurerm_dns_zone.default[0].name 84 | resource_group_name = local.resource_group.name 85 | ttl = lookup(each.value, "ttl", "300") 86 | records = each.value["records"] 87 | 88 | tags = local.tags 89 | } 90 | 91 | resource "azurerm_dns_a_record" "dns_alias_records" { 92 | for_each = local.enable_dns_zone ? local.dns_alias_records : {} 93 | 94 | name = each.key 95 | zone_name = azurerm_dns_zone.default[0].name 96 | resource_group_name = local.resource_group.name 97 | ttl = lookup(each.value, "ttl", "300") 98 | target_resource_id = each.value["target_resource_id"] 99 | 100 | tags = local.tags 101 | } 102 | 103 | resource "azurerm_dns_aaaa_record" "dns_aaaa_records" { 104 | for_each = local.enable_dns_zone ? local.dns_aaaa_records : {} 105 | 106 | name = each.key 107 | zone_name = azurerm_dns_zone.default[0].name 108 | resource_group_name = local.resource_group.name 109 | ttl = lookup(each.value, "ttl", "300") 110 | records = each.value["records"] 111 | 112 | tags = local.tags 113 | } 114 | 115 | resource "azurerm_dns_caa_record" "dns_caa_records" { 116 | for_each = local.enable_dns_zone ? local.dns_caa_records : {} 117 | 118 | name = each.key 119 | zone_name = azurerm_dns_zone.default[0].name 120 | resource_group_name = local.resource_group.name 121 | ttl = lookup(each.value, "ttl", "300") 122 | 123 | dynamic "record" { 124 | for_each = each.value["records"] 125 | content { 126 | flags = record.flags 127 | tag = record.tag 128 | value = record.value 129 | } 130 | } 131 | 132 | tags = local.tags 133 | } 134 | 135 | resource "azurerm_dns_cname_record" "dns_cname_records" { 136 | for_each = local.enable_dns_zone ? local.dns_cname_records : {} 137 | 138 | name = each.key 139 | zone_name = azurerm_dns_zone.default[0].name 140 | resource_group_name = local.resource_group.name 141 | ttl = lookup(each.value, "ttl", "300") 142 | record = each.value["record"] 143 | 144 | tags = local.tags 145 | } 146 | 147 | resource "azurerm_dns_mx_record" "dns_mx_records" { 148 | for_each = local.enable_dns_zone ? local.dns_mx_records : {} 149 | 150 | name = each.key 151 | zone_name = azurerm_dns_zone.default[0].name 152 | resource_group_name = local.resource_group.name 153 | ttl = lookup(each.value, "ttl", "300") 154 | 155 | dynamic "record" { 156 | for_each = each.value["records"] 157 | content { 158 | preference = record.value.preference 159 | exchange = record.value.exchange 160 | } 161 | } 162 | 163 | tags = local.tags 164 | } 165 | 166 | resource "azurerm_dns_ns_record" "dns_ns_records" { 167 | for_each = local.enable_dns_zone ? local.dns_ns_records : {} 168 | 169 | name = each.key 170 | zone_name = azurerm_dns_zone.default[0].name 171 | resource_group_name = local.resource_group.name 172 | ttl = lookup(each.value, "ttl", "300") 173 | records = each.value["records"] 174 | 175 | tags = local.tags 176 | } 177 | 178 | resource "azurerm_dns_ptr_record" "dns_ptr_records" { 179 | for_each = local.enable_dns_zone ? local.dns_ptr_records : {} 180 | 181 | name = each.key 182 | zone_name = azurerm_dns_zone.default[0].name 183 | resource_group_name = local.resource_group.name 184 | ttl = lookup(each.value, "ttl", "300") 185 | records = each.value["records"] 186 | 187 | tags = local.tags 188 | } 189 | 190 | resource "azurerm_dns_srv_record" "dns_srv_records" { 191 | for_each = local.enable_dns_zone ? local.dns_srv_records : {} 192 | 193 | name = each.key 194 | zone_name = azurerm_dns_zone.default[0].name 195 | resource_group_name = local.resource_group.name 196 | ttl = lookup(each.value, "ttl", "300") 197 | 198 | dynamic "record" { 199 | for_each = each.value["records"] 200 | content { 201 | priority = record.priority 202 | weight = record.weight 203 | port = record.port 204 | target = record.target 205 | } 206 | } 207 | 208 | tags = local.tags 209 | } 210 | 211 | resource "azurerm_dns_txt_record" "dns_txt_records" { 212 | for_each = local.enable_dns_zone ? local.dns_txt_records : {} 213 | 214 | name = each.key 215 | zone_name = azurerm_dns_zone.default[0].name 216 | resource_group_name = local.resource_group.name 217 | ttl = lookup(each.value, "ttl", "300") 218 | 219 | dynamic "record" { 220 | for_each = each.value["records"] 221 | content { 222 | value = record.value 223 | } 224 | } 225 | 226 | tags = local.tags 227 | } 228 | -------------------------------------------------------------------------------- /mssql.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_storage_account" "mssql_security_storage" { 2 | count = local.enable_mssql_database ? 1 : 0 3 | 4 | name = "${replace(local.resource_prefix, "-", "")}mssqlsec" 5 | resource_group_name = local.resource_group.name 6 | location = local.resource_group.location 7 | account_tier = "Standard" 8 | account_replication_type = "LRS" 9 | min_tls_version = "TLS1_2" 10 | tags = local.tags 11 | https_traffic_only_enabled = true 12 | public_network_access_enabled = local.enable_mssql_vulnerability_assessment ? true : false 13 | shared_access_key_enabled = local.mssql_security_storage_shared_access_key_enabled 14 | allow_nested_items_to_be_public = false 15 | cross_tenant_replication_enabled = local.mssql_security_storage_cross_tenant_replication_enabled 16 | 17 | blob_properties { 18 | delete_retention_policy { 19 | days = 7 20 | } 21 | container_delete_retention_policy { 22 | days = 7 23 | } 24 | } 25 | 26 | sas_policy { 27 | expiration_period = local.storage_account_sas_expiration_period 28 | } 29 | } 30 | 31 | resource "azurerm_storage_account_network_rules" "mssql_security_storage" { 32 | count = local.enable_mssql_database ? 1 : 0 33 | 34 | storage_account_id = azurerm_storage_account.mssql_security_storage[0].id 35 | # If Vulnerability Assessment is enabled, then there is not currently a way to 36 | # store reports in a Storage Account that is protected by a Firewall. 37 | # Inbound traffic must be permitted to the Storage Account 38 | default_action = local.enable_mssql_vulnerability_assessment ? "Allow" : "Deny" 39 | bypass = ["AzureServices"] 40 | virtual_network_subnet_ids = [] 41 | ip_rules = local.mssql_security_storage_firewall_ipv4_allow_list 42 | } 43 | 44 | resource "azurerm_storage_container" "mssql_security_storage" { 45 | count = local.enable_mssql_database ? 1 : 0 46 | 47 | name = "${local.resource_prefix}-mssqlsec" 48 | storage_account_name = azurerm_storage_account.mssql_security_storage[0].name 49 | } 50 | 51 | resource "azurerm_storage_management_policy" "mssql_security_storage" { 52 | count = local.enable_mssql_database ? 1 : 0 53 | 54 | storage_account_id = azurerm_storage_account.mssql_security_storage[0].id 55 | 56 | rule { 57 | name = "object-lifecycle-policy" 58 | enabled = true 59 | 60 | filters { 61 | prefix_match = ["${azurerm_storage_container.mssql_security_storage[0].name}/*", "sqldbauditlogs/*", "sqldbtdlogs/*"] 62 | blob_types = ["blockBlob"] 63 | } 64 | 65 | actions { 66 | base_blob { 67 | tier_to_cool_after_days_since_creation_greater_than = 3 68 | tier_to_archive_after_days_since_creation_greater_than = 7 69 | delete_after_days_since_creation_greater_than = 30 70 | } 71 | } 72 | } 73 | } 74 | 75 | resource "azurerm_monitor_diagnostic_setting" "mssql_security_storage" { 76 | count = local.enable_mssql_database ? 1 : 0 77 | 78 | name = "${local.resource_prefix}-mssql-blob-diag" 79 | target_resource_id = "${azurerm_storage_account.mssql_security_storage[0].id}/blobServices/default" 80 | log_analytics_workspace_id = azurerm_log_analytics_workspace.container_app.id 81 | eventhub_name = local.enable_event_hub ? azurerm_eventhub.container_app[0].name : null 82 | 83 | enabled_log { 84 | category_group = "Audit" 85 | } 86 | 87 | # The below metrics are kept in to avoid a diff in the Terraform Plan output 88 | metric { 89 | category = "Capacity" 90 | enabled = false 91 | } 92 | 93 | metric { 94 | category = "Transaction" 95 | enabled = false 96 | } 97 | } 98 | 99 | resource "azurerm_mssql_server" "default" { 100 | count = local.enable_mssql_database ? 1 : 0 101 | 102 | name = local.resource_prefix 103 | resource_group_name = local.resource_group.name 104 | location = local.resource_group.location 105 | version = local.mssql_version 106 | administrator_login = local.mssql_server_admin_password != "" ? "${local.resource_prefix}-admin" : null 107 | administrator_login_password = local.mssql_server_admin_password != "" ? local.mssql_server_admin_password : null 108 | express_vulnerability_assessment_enabled = local.enable_mssql_vulnerability_assessment 109 | public_network_access_enabled = local.mssql_server_public_access_enabled 110 | minimum_tls_version = "1.2" 111 | 112 | dynamic "azuread_administrator" { 113 | for_each = local.mssql_azuread_admin_username != "" ? [1] : [] 114 | 115 | content { 116 | object_id = local.mssql_azuread_admin_object_id 117 | login_username = local.mssql_azuread_admin_username 118 | tenant_id = data.azurerm_subscription.current.tenant_id 119 | azuread_authentication_only = local.mssql_azuread_auth_only 120 | } 121 | } 122 | 123 | identity { 124 | type = "UserAssigned" 125 | identity_ids = [azurerm_user_assigned_identity.mssql[0].id] 126 | } 127 | 128 | primary_user_assigned_identity_id = azurerm_user_assigned_identity.mssql[0].id 129 | 130 | tags = local.tags 131 | } 132 | 133 | resource "azurerm_mssql_server_extended_auditing_policy" "default" { 134 | count = local.enable_mssql_database ? 1 : 0 135 | 136 | server_id = azurerm_mssql_server.default[0].id 137 | storage_endpoint = azurerm_storage_account.mssql_security_storage[0].primary_blob_endpoint 138 | retention_in_days = 90 139 | } 140 | 141 | resource "azurerm_mssql_database" "default" { 142 | count = local.enable_mssql_database ? 1 : 0 143 | 144 | name = local.mssql_database_name 145 | server_id = azurerm_mssql_server.default[0].id 146 | collation = "SQL_Latin1_General_CP1_CI_AS" 147 | sku_name = local.mssql_sku_name 148 | max_size_gb = local.mssql_max_size_gb 149 | maintenance_configuration_name = local.mssql_maintenance_configuration_name != "" ? local.mssql_maintenance_configuration_name : "SQL_Default" 150 | 151 | threat_detection_policy { 152 | state = "Enabled" 153 | email_account_admins = "Enabled" 154 | retention_days = 90 155 | } 156 | 157 | tags = local.tags 158 | } 159 | 160 | resource "azurerm_mssql_database_extended_auditing_policy" "default" { 161 | count = local.enable_mssql_database ? 1 : 0 162 | 163 | database_id = azurerm_mssql_database.default[0].id 164 | storage_endpoint = azurerm_storage_account.mssql_security_storage[0].primary_blob_endpoint 165 | retention_in_days = 90 166 | } 167 | 168 | resource "azurerm_mssql_firewall_rule" "default_mssql" { 169 | for_each = local.enable_mssql_database ? local.mssql_firewall_ipv4_allow_list : {} 170 | 171 | name = each.key 172 | server_id = azurerm_mssql_server.default[0].id 173 | start_ip_address = each.value.start_ip_range 174 | end_ip_address = lookup(each.value, "end_ip_range", "") != "" ? each.value.end_ip_range : each.value.start_ip_range 175 | } 176 | 177 | resource "azapi_update_resource" "mssql_threat_protection" { 178 | count = local.enable_mssql_database ? 1 : 0 179 | 180 | type = "Microsoft.Sql/servers/advancedThreatProtectionSettings@2023-05-01-preview" 181 | name = azurerm_mssql_server.default[0].name 182 | parent_id = azurerm_mssql_server.default[0].id 183 | body = jsonencode({ 184 | properties = { 185 | state = local.enable_mssql_vulnerability_assessment ? "Enabled" : "Disabled" 186 | } 187 | }) 188 | 189 | depends_on = [ 190 | azurerm_mssql_server.default[0] 191 | ] 192 | } 193 | 194 | resource "azapi_update_resource" "mssql_security_storage_key_rotation_reminder" { 195 | count = local.enable_mssql_database ? 1 : 0 196 | 197 | type = "Microsoft.Storage/storageAccounts@2023-01-01" 198 | resource_id = azurerm_storage_account.mssql_security_storage[0].id 199 | body = jsonencode({ 200 | properties = { 201 | keyPolicy : { 202 | keyExpirationPeriodInDays : local.mssql_security_storage_access_key_rotation_reminder_days 203 | } 204 | } 205 | }) 206 | 207 | depends_on = [ 208 | azurerm_storage_account.mssql_security_storage[0] 209 | ] 210 | } 211 | -------------------------------------------------------------------------------- /container-app.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_container_app_environment" "container_app_env" { 2 | count = local.existing_container_app_environment.name == "" ? 1 : 0 3 | 4 | name = "${local.resource_prefix}containerapp" 5 | location = local.resource_group.location 6 | resource_group_name = local.resource_group.name 7 | log_analytics_workspace_id = azurerm_log_analytics_workspace.container_app.id 8 | infrastructure_subnet_id = local.launch_in_vnet ? azurerm_subnet.container_apps_infra_subnet[0].id : null 9 | internal_load_balancer_enabled = local.launch_in_vnet ? local.container_app_environment_internal_load_balancer_enabled : false 10 | logs_destination = "log-analytics" 11 | 12 | dynamic "workload_profile" { 13 | for_each = local.container_app_environment_workload_profile_type != "Consumption" ? [1] : [] 14 | 15 | content { 16 | name = local.container_app_environment_workload_profile_type 17 | workload_profile_type = local.container_app_environment_workload_profile_type 18 | minimum_count = local.container_app_environment_min_host_count 19 | maximum_count = local.container_app_environment_max_host_count 20 | } 21 | } 22 | 23 | tags = local.tags 24 | } 25 | 26 | resource "azurerm_monitor_diagnostic_setting" "container_app_env" { 27 | count = local.existing_container_app_environment.name == "" ? 1 : 0 28 | 29 | name = "${local.resource_prefix}-containerapp-diag" 30 | target_resource_id = local.container_app_environment.id 31 | log_analytics_workspace_id = azurerm_log_analytics_workspace.container_app.id 32 | eventhub_name = local.enable_event_hub ? azurerm_eventhub.container_app[0].name : null 33 | 34 | enabled_log { 35 | category_group = "Audit" 36 | } 37 | } 38 | 39 | resource "azurerm_container_app_environment_storage" "container_app_env" { 40 | count = local.enable_container_app_file_share ? 1 : 0 41 | 42 | name = "h${local.resource_prefix_sha_short}-storage" 43 | container_app_environment_id = local.container_app_environment.id 44 | account_name = azurerm_storage_account.container_app[0].name 45 | share_name = azurerm_storage_share.container_app[0].name 46 | access_key = azurerm_storage_account.container_app[0].primary_access_key 47 | access_mode = "ReadWrite" 48 | } 49 | 50 | resource "azurerm_container_app" "container_apps" { 51 | for_each = toset(concat( 52 | ["main"], 53 | local.enable_worker_container ? ["worker"] : [], 54 | )) 55 | 56 | name = each.value == "worker" ? "${local.container_app_name}-worker" : local.container_app_name 57 | container_app_environment_id = local.container_app_environment.id 58 | resource_group_name = local.resource_group.name 59 | revision_mode = "Single" 60 | workload_profile_name = local.container_app_environment_workload_profile_type != "Consumption" ? local.container_app_environment_workload_profile_type : null 61 | 62 | dynamic "ingress" { 63 | for_each = each.value == "main" ? [1] : [] 64 | 65 | content { 66 | external_enabled = true 67 | target_port = local.container_port 68 | allow_insecure_connections = false 69 | 70 | traffic_weight { 71 | percentage = 100 72 | latest_revision = true 73 | } 74 | } 75 | } 76 | 77 | dynamic "secret" { 78 | for_each = local.escrow_container_app_secrets_in_key_vault ? {} : local.container_app_secrets 79 | 80 | content { 81 | name = secret.value["name"] 82 | value = secret.value["value"] 83 | } 84 | } 85 | 86 | dynamic "secret" { 87 | for_each = local.container_app_secrets_in_key_vault 88 | 89 | content { 90 | name = secret.value["name"] 91 | key_vault_secret_id = secret.value["key_vault_secret_id"] 92 | identity = azurerm_user_assigned_identity.containerapp[0].id 93 | } 94 | } 95 | 96 | dynamic "identity" { 97 | for_each = length(local.container_app_identity_ids) > 0 ? [1] : [] 98 | 99 | content { 100 | type = "UserAssigned" 101 | identity_ids = local.container_app_identity_ids 102 | } 103 | } 104 | 105 | registry { 106 | server = local.registry_server 107 | username = local.registry_use_managed_identity == false ? local.registry_username : null 108 | password_secret_name = local.registry_use_managed_identity == false ? "acr-password" : null 109 | identity = local.registry_use_managed_identity ? azurerm_user_assigned_identity.containerapp[0].id : null 110 | } 111 | 112 | template { 113 | dynamic "init_container" { 114 | for_each = each.value == "main" && local.enable_init_container ? [1] : [] 115 | 116 | content { 117 | name = "${each.value}-init" 118 | image = local.init_container_image != "" ? local.init_container_image : "${local.registry_server}/${local.image_name}:${local.image_tag}" 119 | cpu = local.container_cpu 120 | memory = "${local.container_memory}Gi" 121 | command = local.init_container_command 122 | 123 | dynamic "env" { 124 | for_each = local.container_app_env_vars 125 | 126 | content { 127 | name = env.value["name"] 128 | secret_name = lookup(env.value, "secretRef", null) 129 | value = lookup(env.value, "value", null) 130 | } 131 | } 132 | } 133 | } 134 | 135 | container { 136 | name = each.value 137 | image = "${local.registry_server}/${local.image_name}:${local.image_tag}" 138 | cpu = local.container_cpu 139 | memory = "${local.container_memory}Gi" 140 | command = each.value == "worker" ? local.worker_container_command : local.container_command 141 | 142 | dynamic "volume_mounts" { 143 | for_each = local.enable_container_app_file_share ? [1] : [] 144 | 145 | content { 146 | name = azurerm_container_app_environment_storage.container_app_env[0].name 147 | path = local.container_app_file_share_mount_path 148 | } 149 | } 150 | 151 | dynamic "liveness_probe" { 152 | for_each = each.value == "main" && local.enable_container_health_probe ? [1] : [] 153 | 154 | content { 155 | interval_seconds = lookup(local.container_health_probe, "interval_seconds", null) 156 | transport = lookup(local.container_health_probe, "transport", null) 157 | port = lookup(local.container_health_probe, "port", local.container_port) 158 | path = lookup(local.container_health_probe, "path", null) 159 | } 160 | } 161 | 162 | dynamic "env" { 163 | for_each = local.container_app_env_vars 164 | 165 | content { 166 | name = env.value["name"] 167 | secret_name = lookup(env.value, "secretRef", null) 168 | value = lookup(env.value, "value", null) 169 | } 170 | } 171 | } 172 | 173 | min_replicas = each.value == "worker" ? local.worker_container_min_replicas : local.container_min_replicas 174 | max_replicas = each.value == "worker" ? local.worker_container_max_replicas : local.container_max_replicas 175 | 176 | http_scale_rule { 177 | name = "scale-up-down-http-requests" 178 | concurrent_requests = local.container_scale_http_concurrency 179 | } 180 | 181 | dynamic "custom_scale_rule" { 182 | for_each = local.container_scale_out_at_defined_time ? [1] : [] 183 | 184 | content { 185 | name = "scale-down-out-of-hours" 186 | custom_rule_type = "cron" 187 | metadata = { 188 | timezone = "Europe/London" 189 | start = local.container_scale_out_rule_start 190 | end = local.container_scale_out_rule_end 191 | desiredReplicas = each.value == "worker" ? local.worker_container_max_replicas : local.container_max_replicas 192 | } 193 | } 194 | } 195 | 196 | dynamic "volume" { 197 | for_each = local.enable_container_app_file_share ? [1] : [] 198 | 199 | content { 200 | name = azurerm_container_app_environment_storage.container_app_env[0].name 201 | storage_name = azurerm_container_app_environment_storage.container_app_env[0].name 202 | storage_type = "AzureFile" 203 | } 204 | } 205 | } 206 | 207 | tags = local.tags 208 | } 209 | -------------------------------------------------------------------------------- /storage.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_storage_account" "container_app" { 2 | count = local.enable_storage_account ? 1 : 0 3 | 4 | name = "${replace(local.resource_prefix, "-", "")}storage" 5 | resource_group_name = local.resource_group.name 6 | location = local.resource_group.location 7 | account_tier = "Standard" 8 | account_replication_type = "LRS" 9 | min_tls_version = "TLS1_2" 10 | https_traffic_only_enabled = true 11 | public_network_access_enabled = local.storage_account_public_access_enabled 12 | shared_access_key_enabled = local.container_app_storage_account_shared_access_key_enabled 13 | allow_nested_items_to_be_public = local.container_app_blob_storage_public_access_enabled 14 | cross_tenant_replication_enabled = local.container_app_storage_cross_tenant_replication_enabled 15 | 16 | blob_properties { 17 | delete_retention_policy { 18 | days = 7 19 | } 20 | container_delete_retention_policy { 21 | days = 7 22 | } 23 | } 24 | 25 | share_properties { 26 | retention_policy { 27 | days = 7 28 | } 29 | 30 | dynamic "smb" { 31 | for_each = lower(local.container_app_file_share_security_profile) == "security" ? [1] : [] 32 | 33 | content { 34 | versions = ["SMB3.1.1"] 35 | authentication_types = ["NTLMv2", "Kerberos"] 36 | kerberos_ticket_encryption_type = ["AES-256"] 37 | channel_encryption_type = ["AES-128-GCM", "AES-256-GCM"] 38 | } 39 | } 40 | } 41 | 42 | sas_policy { 43 | expiration_period = local.storage_account_sas_expiration_period 44 | } 45 | 46 | tags = local.tags 47 | } 48 | 49 | resource "azapi_update_resource" "container_app_storage_key_rotation_reminder" { 50 | count = local.enable_storage_account ? 1 : 0 51 | 52 | type = "Microsoft.Storage/storageAccounts@2023-01-01" 53 | resource_id = azurerm_storage_account.container_app[0].id 54 | body = jsonencode({ 55 | properties = { 56 | keyPolicy : { 57 | keyExpirationPeriodInDays : local.storage_account_access_key_rotation_reminder_days 58 | } 59 | } 60 | }) 61 | 62 | depends_on = [ 63 | azurerm_storage_account.container_app[0] 64 | ] 65 | } 66 | 67 | resource "azurerm_storage_account_network_rules" "container_app" { 68 | count = local.enable_storage_account ? 1 : 0 69 | 70 | storage_account_id = azurerm_storage_account.container_app[0].id 71 | default_action = "Deny" 72 | bypass = ["AzureServices"] 73 | virtual_network_subnet_ids = local.launch_in_vnet ? [azurerm_subnet.container_apps_infra_subnet[0].id] : null 74 | ip_rules = local.storage_account_ipv4_allow_list 75 | } 76 | 77 | resource "azurerm_storage_container" "container_app" { 78 | count = local.enable_container_app_blob_storage ? 1 : 0 79 | 80 | name = "${local.resource_prefix}-storage" 81 | storage_account_name = azurerm_storage_account.container_app[0].name 82 | } 83 | 84 | resource "azurerm_storage_share" "container_app" { 85 | count = local.enable_container_app_file_share ? 1 : 0 86 | 87 | name = "${local.resource_prefix}-storage" 88 | storage_account_name = azurerm_storage_account.container_app[0].name 89 | quota = local.storage_account_file_share_quota_gb 90 | } 91 | 92 | resource "azurerm_monitor_diagnostic_setting" "blobs" { 93 | count = local.enable_container_app_blob_storage ? 1 : 0 94 | 95 | name = "${local.resource_prefix}-storage-blobs-diag" 96 | target_resource_id = "${azurerm_storage_account.container_app[0].id}/blobServices/default" 97 | log_analytics_workspace_id = azurerm_log_analytics_workspace.container_app.id 98 | eventhub_name = local.enable_event_hub ? azurerm_eventhub.container_app[0].name : null 99 | 100 | enabled_log { 101 | category_group = "Audit" 102 | } 103 | 104 | # The below metrics are kept in to avoid a diff in the Terraform Plan output 105 | metric { 106 | category = "Capacity" 107 | enabled = false 108 | } 109 | 110 | metric { 111 | category = "Transaction" 112 | enabled = false 113 | } 114 | } 115 | 116 | resource "azurerm_monitor_diagnostic_setting" "files" { 117 | count = local.enable_container_app_file_share ? 1 : 0 118 | 119 | name = "${local.resource_prefix}-storage-files-diag" 120 | target_resource_id = "${azurerm_storage_account.container_app[0].id}/fileServices/default" 121 | log_analytics_workspace_id = azurerm_log_analytics_workspace.container_app.id 122 | eventhub_name = local.enable_event_hub ? azurerm_eventhub.container_app[0].name : null 123 | 124 | enabled_log { 125 | category_group = "Audit" 126 | } 127 | 128 | # The below metrics are kept in to avoid a diff in the Terraform Plan output 129 | metric { 130 | category = "Capacity" 131 | enabled = false 132 | } 133 | 134 | metric { 135 | category = "Transaction" 136 | enabled = false 137 | } 138 | } 139 | 140 | data "azurerm_storage_account_blob_container_sas" "container_app" { 141 | count = local.enable_container_app_blob_storage && local.create_container_app_blob_storage_sas ? 1 : 0 142 | 143 | connection_string = azurerm_storage_account.container_app[0].primary_connection_string 144 | container_name = azurerm_storage_container.container_app[0].name 145 | https_only = true 146 | start = formatdate("YYYY-MM-DD'T'hh:mm:ssZ", timestamp()) 147 | expiry = formatdate("YYYY-MM-DD'T'hh:mm:ssZ", timeadd(timestamp(), "+4380h")) # +6 months 148 | 149 | permissions { 150 | read = true 151 | add = true 152 | create = true 153 | write = true 154 | delete = true 155 | list = true 156 | } 157 | } 158 | 159 | resource "azurerm_storage_account" "function_app_backing" { 160 | count = local.enable_linux_function_apps ? 1 : 0 161 | 162 | name = "s${local.resource_prefix_sha_short}functions" 163 | resource_group_name = local.resource_group.name 164 | location = local.resource_group.location 165 | account_tier = "Standard" 166 | account_replication_type = "LRS" 167 | min_tls_version = "TLS1_2" 168 | https_traffic_only_enabled = true 169 | allow_nested_items_to_be_public = false 170 | public_network_access_enabled = true 171 | cross_tenant_replication_enabled = false 172 | 173 | sas_policy { 174 | expiration_period = local.storage_account_sas_expiration_period 175 | } 176 | 177 | blob_properties { 178 | delete_retention_policy { 179 | days = 7 180 | } 181 | container_delete_retention_policy { 182 | days = 7 183 | } 184 | } 185 | 186 | tags = local.tags 187 | } 188 | 189 | resource "azurerm_storage_account_network_rules" "function_app_backing" { 190 | count = local.enable_linux_function_apps ? 1 : 0 191 | 192 | storage_account_id = azurerm_storage_account.function_app_backing[0].id 193 | default_action = "Allow" 194 | bypass = ["AzureServices"] 195 | ip_rules = local.storage_account_ipv4_allow_list 196 | } 197 | 198 | resource "azapi_update_resource" "function_app_storage_key_rotation_reminder" { 199 | count = local.enable_linux_function_apps ? 1 : 0 200 | 201 | type = "Microsoft.Storage/storageAccounts@2023-01-01" 202 | resource_id = azurerm_storage_account.function_app_backing[0].id 203 | body = jsonencode({ 204 | properties = { 205 | keyPolicy : { 206 | keyExpirationPeriodInDays : local.storage_account_access_key_rotation_reminder_days 207 | } 208 | } 209 | }) 210 | 211 | depends_on = [ 212 | azurerm_storage_account.function_app_backing[0] 213 | ] 214 | } 215 | 216 | resource "azurerm_monitor_diagnostic_setting" "function_app_storage" { 217 | count = local.enable_linux_function_apps ? 1 : 0 218 | 219 | name = "${azurerm_storage_account.function_app_backing[0].name}-storage-blobs-diag" 220 | target_resource_id = "${azurerm_storage_account.function_app_backing[0].id}/blobServices/default" 221 | log_analytics_workspace_id = azurerm_log_analytics_workspace.function_app[0].id 222 | 223 | enabled_log { 224 | category_group = "Audit" 225 | } 226 | 227 | # The below metrics are kept in to avoid a diff in the Terraform Plan output 228 | metric { 229 | category = "Capacity" 230 | enabled = false 231 | } 232 | 233 | metric { 234 | category = "Transaction" 235 | enabled = false 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /function-app.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_service_plan" "function_apps" { 2 | count = local.enable_linux_function_apps ? 1 : 0 3 | 4 | name = "${local.resource_prefix}-linux-serviceplan" 5 | resource_group_name = local.resource_group.name 6 | location = local.resource_group.location 7 | os_type = "Linux" 8 | sku_name = "Y1" // PAYG/Consumption plan 9 | 10 | tags = local.tags 11 | } 12 | 13 | resource "azurerm_service_plan" "function_apps_flex" { 14 | count = local.enable_linux_function_apps ? 1 : 0 15 | 16 | name = "${local.resource_prefix}-linux-serviceplan-flex" 17 | resource_group_name = local.resource_group.name 18 | location = local.resource_group.location 19 | os_type = "Linux" 20 | sku_name = "FC1" // Flex Consumption Plan 21 | 22 | tags = local.tags 23 | } 24 | 25 | resource "azurerm_linux_function_app" "health_api" { 26 | for_each = local.linux_function_health_insights_api 27 | 28 | name = "${local.resource_prefix}-${each.key}" 29 | resource_group_name = local.resource_group.name 30 | location = local.resource_group.location 31 | storage_account_name = azurerm_storage_account.function_app_backing[0].name 32 | storage_account_access_key = azurerm_storage_account.function_app_backing[0].primary_access_key 33 | service_plan_id = azurerm_service_plan.function_apps[0].id 34 | ftp_publish_basic_authentication_enabled = each.value.ftp_publish_basic_authentication_enabled 35 | webdeploy_publish_basic_authentication_enabled = each.value.webdeploy_publish_basic_authentication_enabled 36 | https_only = true 37 | key_vault_reference_identity_id = azurerm_user_assigned_identity.function_apps[each.key].id 38 | zip_deploy_file = data.archive_file.azure_function[each.key].output_path 39 | 40 | app_settings = merge(each.value.app_settings, { 41 | "AZURE_CLIENT_ID" = azurerm_user_assigned_identity.function_apps[each.key].client_id 42 | }) 43 | 44 | site_config { 45 | always_on = false 46 | application_insights_connection_string = local.enable_app_insights_integration ? azurerm_application_insights.function_apps[each.key].connection_string : null 47 | application_insights_key = local.enable_app_insights_integration ? azurerm_application_insights.function_apps[each.key].instrumentation_key : null 48 | app_scale_limit = 1 49 | http2_enabled = true 50 | ftps_state = each.value.ftp_publish_basic_authentication_enabled ? "FtpsOnly" : "Disabled" 51 | ip_restriction_default_action = length(each.value.ipv4_access) > 0 ? "Deny" : "Allow" 52 | scm_ip_restriction_default_action = length(each.value.ipv4_access) > 0 ? "Deny" : "Allow" 53 | scm_use_main_ip_restriction = true 54 | minimum_tls_version = "1.3" 55 | 56 | cors { 57 | allowed_origins = each.value.allowed_origins 58 | support_credentials = contains(each.value.allowed_origins, "*") ? false : true 59 | } 60 | 61 | dynamic "ip_restriction" { 62 | for_each = each.value.ipv4_access 63 | 64 | content { 65 | action = "Allow" 66 | name = "AllowIPInbound${ip_restriction.value}" 67 | ip_address = ip_restriction.value 68 | } 69 | } 70 | 71 | application_stack { 72 | python_version = lower(each.value.runtime) == "python" ? each.value.runtime_version : null 73 | dotnet_version = lower(each.value.runtime) == "dotnet" ? each.value.runtime_version : null 74 | java_version = lower(each.value.runtime) == "java" ? each.value.runtime_version : null 75 | node_version = lower(each.value.runtime) == "node" ? each.value.runtime_version : null 76 | } 77 | } 78 | 79 | identity { 80 | type = "UserAssigned" 81 | identity_ids = [ 82 | azurerm_user_assigned_identity.function_apps[each.key].id 83 | ] 84 | } 85 | 86 | tags = merge(local.tags, { 87 | "hidden-link: /app-insights-conn-string" : azurerm_application_insights.function_apps[each.key].connection_string, 88 | "hidden-link: /app-insights-instrumentation-key" : azurerm_application_insights.function_apps[each.key].instrumentation_key, 89 | "hidden-link: /app-insights-resource-id" : azurerm_application_insights.function_apps[each.key].id, 90 | }) 91 | 92 | lifecycle { 93 | replace_triggered_by = [terraform_data.function_app_package_sha[each.key]] 94 | } 95 | } 96 | 97 | resource "azurerm_storage_container" "function_app_backing" { 98 | for_each = local.linux_function_apps 99 | 100 | name = each.key 101 | storage_account_name = azurerm_storage_account.function_app_backing[0].name 102 | } 103 | 104 | resource "azurerm_function_app_flex_consumption" "function_apps" { 105 | for_each = local.linux_function_apps 106 | 107 | name = "${local.environment}${each.key}" 108 | resource_group_name = local.resource_group.name 109 | location = local.resource_group.location 110 | storage_container_type = "blobContainer" 111 | storage_authentication_type = "StorageAccountConnectionString" 112 | storage_access_key = azurerm_storage_account.function_app_backing[0].primary_access_key 113 | storage_container_endpoint = "${azurerm_storage_account.function_app_backing[0].primary_blob_endpoint}${azurerm_storage_container.function_app_backing[each.key].name}" 114 | runtime_name = each.value["runtime"] 115 | runtime_version = each.value["runtime_version"] 116 | service_plan_id = azurerm_service_plan.function_apps_flex[0].id 117 | webdeploy_publish_basic_authentication_enabled = each.value.webdeploy_publish_basic_authentication_enabled 118 | https_only = true 119 | virtual_network_subnet_id = local.launch_in_vnet ? azurerm_subnet.function_app_subnet[0].id : null 120 | 121 | app_settings = merge(each.value.app_settings, { 122 | "AZURE_CLIENT_ID" = azurerm_user_assigned_identity.function_apps[each.key].client_id 123 | }, 124 | each.value["enable_service_bus"] ? { 125 | "SERVICEBUS_TOPIC_NAME" = azurerm_servicebus_topic.function_apps[each.key].name 126 | "SERVICEBUS_SUBSCRIPTION" = azurerm_servicebus_subscription.function_apps[each.key].name 127 | } : {} 128 | ) 129 | 130 | dynamic "connection_string" { 131 | for_each = each.value["enable_service_bus"] ? [1] : [] 132 | 133 | content { 134 | name = "ServiceBus" 135 | type = "ServiceBus" 136 | value = azurerm_servicebus_namespace_authorization_rule.function_apps[each.key].primary_connection_string 137 | } 138 | } 139 | 140 | dynamic "connection_string" { 141 | for_each = each.value["connection_strings"] 142 | 143 | content { 144 | name = connection_string.key 145 | type = connection_string.value["type"] 146 | value = connection_string.value["value"] 147 | } 148 | } 149 | 150 | site_config { 151 | application_insights_connection_string = local.enable_app_insights_integration ? azurerm_application_insights.function_apps[each.key].connection_string : null 152 | application_insights_key = local.enable_app_insights_integration ? azurerm_application_insights.function_apps[each.key].instrumentation_key : null 153 | http2_enabled = true 154 | ip_restriction_default_action = length(each.value.ipv4_access) > 0 ? "Deny" : "Allow" 155 | scm_ip_restriction_default_action = length(each.value.ipv4_access) > 0 ? "Deny" : "Allow" 156 | scm_use_main_ip_restriction = true 157 | minimum_tls_version = each.value.minimum_tls_version 158 | 159 | cors { 160 | allowed_origins = each.value.allowed_origins 161 | support_credentials = contains(each.value.allowed_origins, "*") ? false : true 162 | } 163 | 164 | dynamic "ip_restriction" { 165 | for_each = each.value.ipv4_access 166 | 167 | content { 168 | action = "Allow" 169 | name = "AllowIPInbound${ip_restriction.value}" 170 | ip_address = ip_restriction.value 171 | } 172 | } 173 | } 174 | 175 | identity { 176 | type = "UserAssigned" 177 | identity_ids = [ 178 | azurerm_user_assigned_identity.function_apps[each.key].id 179 | ] 180 | } 181 | 182 | tags = merge(local.tags, local.enable_app_insights_integration ? { 183 | "hidden-link: /app-insights-conn-string" : azurerm_application_insights.function_apps[each.key].connection_string, 184 | "hidden-link: /app-insights-instrumentation-key" : azurerm_application_insights.function_apps[each.key].instrumentation_key, 185 | "hidden-link: /app-insights-resource-id" : azurerm_application_insights.function_apps[each.key].id, 186 | } : {}) 187 | } 188 | 189 | resource "azurerm_monitor_diagnostic_setting" "function_apps" { 190 | for_each = local.linux_function_apps 191 | 192 | name = "${azurerm_function_app_flex_consumption.function_apps[each.key].name}-diagnostics" 193 | target_resource_id = azurerm_function_app_flex_consumption.function_apps[each.key].id 194 | log_analytics_workspace_id = azurerm_log_analytics_workspace.function_app[0].id 195 | 196 | enabled_log { 197 | category = "FunctionAppLogs" 198 | } 199 | } 200 | 201 | resource "azurerm_monitor_diagnostic_setting" "function_apps_health_api" { 202 | for_each = local.linux_function_health_insights_api 203 | 204 | name = "${azurerm_linux_function_app.health_api[each.key].name}-diagnostics" 205 | target_resource_id = azurerm_linux_function_app.health_api[each.key].id 206 | log_analytics_workspace_id = azurerm_log_analytics_workspace.function_app[0].id 207 | 208 | enabled_log { 209 | category = "FunctionAppLogs" 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /cdn.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_cdn_frontdoor_profile" "cdn" { 2 | count = local.enable_cdn_frontdoor ? 1 : 0 3 | 4 | name = "${local.resource_prefix}cdn" 5 | resource_group_name = local.resource_group.name 6 | sku_name = local.cdn_frontdoor_sku 7 | response_timeout_seconds = local.cdn_frontdoor_response_timeout 8 | tags = local.tags 9 | } 10 | 11 | resource "azurerm_cdn_frontdoor_origin_group" "group" { 12 | count = local.enable_cdn_frontdoor ? 1 : 0 13 | 14 | name = "${local.resource_prefix}origingroup" 15 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 16 | 17 | load_balancing {} 18 | 19 | dynamic "health_probe" { 20 | for_each = local.enable_cdn_frontdoor_health_probe ? [0] : [] 21 | 22 | content { 23 | protocol = local.cdn_frontdoor_health_probe_protocol 24 | interval_in_seconds = local.cdn_frontdoor_health_probe_interval 25 | request_type = local.cdn_frontdoor_health_probe_request_type 26 | path = local.cdn_frontdoor_health_probe_path 27 | } 28 | } 29 | } 30 | 31 | resource "azurerm_cdn_frontdoor_origin_group" "custom_container_apps" { 32 | for_each = local.enable_cdn_frontdoor ? { for name, container in local.custom_container_apps : name => container 33 | if container.ingress.external_enabled 34 | } : {} 35 | 36 | name = "${local.resource_prefix}origingroup${replace(each.key, "-", "")}" 37 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 38 | 39 | load_balancing {} 40 | 41 | dynamic "health_probe" { 42 | for_each = each.value.ingress.enable_cdn_frontdoor_health_probe ? [0] : [] 43 | 44 | content { 45 | protocol = each.value.ingress.cdn_frontdoor_health_probe_protocol 46 | interval_in_seconds = each.value.ingress.cdn_frontdoor_health_probe_interval 47 | request_type = each.value.ingress.cdn_frontdoor_health_probe_request_type 48 | path = each.value.ingress.cdn_frontdoor_health_probe_path 49 | } 50 | } 51 | } 52 | 53 | resource "azurerm_cdn_frontdoor_origin" "origin" { 54 | count = local.enable_cdn_frontdoor ? 1 : 0 55 | 56 | name = "${local.resource_prefix}origin" 57 | cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.group[0].id 58 | enabled = true 59 | certificate_name_check_enabled = true 60 | host_name = local.cdn_frontdoor_origin_fqdn_override 61 | origin_host_header = local.cdn_frontdoor_origin_host_header_override 62 | http_port = local.cdn_frontdoor_origin_http_port 63 | https_port = local.cdn_frontdoor_origin_https_port 64 | } 65 | 66 | resource "azurerm_cdn_frontdoor_origin" "custom_container_apps" { 67 | for_each = local.enable_cdn_frontdoor ? { for name, container in local.custom_container_apps : name => container 68 | if container.ingress.external_enabled 69 | } : {} 70 | 71 | name = "${local.resource_prefix}origin${replace(each.key, "-", "")}" 72 | cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.custom_container_apps[each.key].id 73 | enabled = true 74 | certificate_name_check_enabled = true 75 | host_name = each.value.ingress.cdn_frontdoor_origin_fqdn_override != "" ? each.value.ingress.cdn_frontdoor_origin_fqdn_override : azurerm_container_app.custom_container_apps[each.key].ingress[0].fqdn 76 | origin_host_header = each.value.ingress.cdn_frontdoor_origin_host_header_override != "" ? each.value.ingress.cdn_frontdoor_origin_host_header_override : azurerm_container_app.custom_container_apps[each.key].ingress[0].fqdn 77 | http_port = local.cdn_frontdoor_origin_http_port 78 | https_port = local.cdn_frontdoor_origin_https_port 79 | 80 | depends_on = [azurerm_container_app.custom_container_apps] 81 | } 82 | 83 | resource "azurerm_cdn_frontdoor_endpoint" "endpoint" { 84 | count = local.enable_cdn_frontdoor ? 1 : 0 85 | 86 | name = "${local.resource_prefix}cdnendpoint" 87 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 88 | tags = local.tags 89 | } 90 | 91 | resource "azurerm_cdn_frontdoor_endpoint" "custom_container_apps" { 92 | for_each = local.enable_cdn_frontdoor ? { for name, container in local.custom_container_apps : name => container 93 | if container.ingress.external_enabled 94 | } : {} 95 | 96 | name = "${local.resource_prefix}cdnendpoint-${replace(each.key, "-", "")}" 97 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 98 | tags = local.tags 99 | } 100 | 101 | resource "azurerm_cdn_frontdoor_custom_domain" "custom_domain" { 102 | for_each = local.enable_cdn_frontdoor ? toset(local.cdn_frontdoor_custom_domains) : [] 103 | 104 | name = "${local.resource_prefix}custom-domain${index(local.cdn_frontdoor_custom_domains, each.value)}" 105 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 106 | dns_zone_id = local.enable_dns_zone && endswith(each.value, local.dns_zone_domain_name) ? azurerm_dns_zone.default[0].id : null 107 | host_name = each.value 108 | 109 | tls { 110 | certificate_type = "ManagedCertificate" 111 | minimum_tls_version = "TLS12" 112 | } 113 | } 114 | 115 | resource "azurerm_cdn_frontdoor_custom_domain" "custom_container_apps" { 116 | for_each = local.enable_cdn_frontdoor ? { for name, container in local.custom_container_apps : name => container 117 | if container.ingress.external_enabled && container.ingress.cdn_frontdoor_custom_domain != "" 118 | } : {} 119 | 120 | name = "${local.resource_prefix}custom-domain-${each.key}" 121 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 122 | dns_zone_id = local.enable_dns_zone && endswith(each.value.ingress.cdn_frontdoor_custom_domain, local.dns_zone_domain_name) ? azurerm_dns_zone.default[0].id : null 123 | host_name = each.value.ingress.cdn_frontdoor_custom_domain 124 | 125 | tls { 126 | certificate_type = "ManagedCertificate" 127 | minimum_tls_version = "TLS12" 128 | } 129 | } 130 | 131 | resource "azurerm_cdn_frontdoor_route" "route" { 132 | count = local.enable_cdn_frontdoor ? 1 : 0 133 | 134 | name = "${local.resource_prefix}route" 135 | cdn_frontdoor_endpoint_id = azurerm_cdn_frontdoor_endpoint.endpoint[0].id 136 | cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.group[0].id 137 | cdn_frontdoor_origin_ids = [azurerm_cdn_frontdoor_origin.origin[0].id] 138 | cdn_frontdoor_rule_set_ids = local.ruleset_ids 139 | enabled = true 140 | 141 | forwarding_protocol = local.cdn_frontdoor_forwarding_protocol 142 | https_redirect_enabled = true 143 | patterns_to_match = ["/*"] 144 | supported_protocols = ["Http", "Https"] 145 | 146 | cdn_frontdoor_custom_domain_ids = [ 147 | for custom_domain in azurerm_cdn_frontdoor_custom_domain.custom_domain : custom_domain.id 148 | ] 149 | 150 | link_to_default_domain = true 151 | } 152 | 153 | resource "azurerm_cdn_frontdoor_route" "custom_container_apps" { 154 | for_each = local.enable_cdn_frontdoor ? { for name, container in local.custom_container_apps : name => container 155 | if container.ingress.external_enabled 156 | } : {} 157 | 158 | name = "${local.resource_prefix}route-${replace(each.key, "-", "")}" 159 | cdn_frontdoor_endpoint_id = azurerm_cdn_frontdoor_endpoint.custom_container_apps[each.key].id 160 | cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.custom_container_apps[each.key].id 161 | cdn_frontdoor_origin_ids = [azurerm_cdn_frontdoor_origin.custom_container_apps[each.key].id] 162 | cdn_frontdoor_rule_set_ids = local.ruleset_ids 163 | enabled = true 164 | 165 | forwarding_protocol = each.value.ingress.cdn_frontdoor_forwarding_protocol_override != "" ? each.value.ingress.cdn_frontdoor_forwarding_protocol_override : local.cdn_frontdoor_forwarding_protocol 166 | https_redirect_enabled = true 167 | patterns_to_match = ["/*"] 168 | supported_protocols = ["Http", "Https"] 169 | 170 | cdn_frontdoor_custom_domain_ids = each.value.ingress.cdn_frontdoor_custom_domain != "" ? [azurerm_cdn_frontdoor_custom_domain.custom_container_apps[each.key].id] : [] 171 | } 172 | 173 | resource "azurerm_cdn_frontdoor_custom_domain_association" "custom_domain_association" { 174 | for_each = local.enable_cdn_frontdoor ? [] : toset(local.cdn_frontdoor_custom_domains) 175 | 176 | cdn_frontdoor_custom_domain_id = azurerm_cdn_frontdoor_custom_domain.custom_domain[each.value].id 177 | cdn_frontdoor_route_ids = [azurerm_cdn_frontdoor_route.route[0].id] 178 | } 179 | 180 | resource "azurerm_cdn_frontdoor_rule_set" "redirects" { 181 | count = local.enable_cdn_frontdoor && length(local.cdn_frontdoor_host_redirects) > 0 ? 1 : 0 182 | 183 | name = "${replace(local.resource_prefix, "-", "")}redirects" 184 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 185 | } 186 | 187 | resource "azurerm_cdn_frontdoor_rule" "redirect" { 188 | for_each = local.enable_cdn_frontdoor ? { for index, host_redirect in local.cdn_frontdoor_host_redirects : index => { "from" : host_redirect.from, "to" : host_redirect.to } } : {} 189 | 190 | depends_on = [azurerm_cdn_frontdoor_origin_group.group, azurerm_cdn_frontdoor_origin.origin] 191 | 192 | name = "redirect${each.key}" 193 | cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.redirects[0].id 194 | order = each.key 195 | behavior_on_match = "Continue" 196 | 197 | actions { 198 | url_redirect_action { 199 | redirect_type = "Moved" 200 | redirect_protocol = "Https" 201 | destination_hostname = each.value.to 202 | } 203 | } 204 | 205 | conditions { 206 | host_name_condition { 207 | operator = "Equal" 208 | negate_condition = false 209 | match_values = [each.value.from] 210 | transforms = ["Lowercase", "Trim"] 211 | } 212 | } 213 | } 214 | 215 | resource "azurerm_cdn_frontdoor_rule_set" "vdp" { 216 | count = local.enable_cdn_frontdoor && local.enable_cdn_frontdoor_vdp_redirects ? 1 : 0 217 | 218 | name = "${replace(local.resource_prefix, "-", "")}vdp" 219 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 220 | } 221 | 222 | resource "azurerm_cdn_frontdoor_rule" "vdp_security_txt" { 223 | count = local.enable_cdn_frontdoor && local.enable_cdn_frontdoor_vdp_redirects ? 1 : 0 224 | 225 | depends_on = [azurerm_cdn_frontdoor_origin_group.group, azurerm_cdn_frontdoor_origin.origin] 226 | name = "securitytxtredirect" 227 | cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.vdp[0].id 228 | order = 1 229 | behavior_on_match = "Continue" 230 | 231 | actions { 232 | url_redirect_action { 233 | redirect_type = "PermanentRedirect" 234 | redirect_protocol = "Https" 235 | destination_hostname = local.cdn_frontdoor_vdp_destination_hostname 236 | destination_path = "/.well-known/security.txt" 237 | } 238 | } 239 | 240 | conditions { 241 | url_filename_condition { 242 | operator = "Equal" 243 | match_values = ["security.txt", "/.well-known/security.txt"] 244 | transforms = ["Lowercase", "RemoveNulls", "Trim"] 245 | } 246 | } 247 | } 248 | 249 | resource "azurerm_cdn_frontdoor_rule" "vdp_thanks_txt" { 250 | count = local.enable_cdn_frontdoor && local.enable_cdn_frontdoor_vdp_redirects ? 1 : 0 251 | 252 | depends_on = [azurerm_cdn_frontdoor_origin_group.group, azurerm_cdn_frontdoor_origin.origin] 253 | name = "thankstxtredirect" 254 | cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.vdp[0].id 255 | order = 2 256 | behavior_on_match = "Continue" 257 | 258 | actions { 259 | url_redirect_action { 260 | redirect_type = "PermanentRedirect" 261 | redirect_protocol = "Https" 262 | destination_hostname = local.cdn_frontdoor_vdp_destination_hostname 263 | destination_path = "/thanks.txt" 264 | } 265 | } 266 | 267 | conditions { 268 | url_filename_condition { 269 | operator = "Equal" 270 | match_values = ["thanks.txt", "/.well-known/thanks.txt"] 271 | transforms = ["Lowercase", "RemoveNulls", "Trim"] 272 | } 273 | } 274 | } 275 | 276 | resource "azurerm_cdn_frontdoor_firewall_policy" "waf" { 277 | count = local.cdn_frontdoor_enable_waf ? 1 : 0 278 | 279 | name = "${replace(local.resource_prefix, "-", "")}waf" 280 | resource_group_name = local.resource_group.name 281 | sku_name = azurerm_cdn_frontdoor_profile.cdn[0].sku_name 282 | enabled = true 283 | mode = local.cdn_frontdoor_waf_mode 284 | custom_block_response_status_code = 403 285 | custom_block_response_body = filebase64("${path.module}/html/waf-response.html") 286 | js_challenge_cookie_expiration_in_minutes = lower(local.cdn_frontdoor_sku) == "premium" ? 30 : null 287 | 288 | dynamic "custom_rule" { 289 | for_each = local.cdn_frontdoor_enable_rate_limiting ? [0] : [] 290 | 291 | content { 292 | name = "RateLimiting" 293 | enabled = true 294 | priority = 1000 295 | rate_limit_duration_in_minutes = local.cdn_frontdoor_rate_limiting_duration_in_minutes 296 | rate_limit_threshold = local.cdn_frontdoor_rate_limiting_threshold 297 | type = "RateLimitRule" 298 | action = "Block" 299 | 300 | dynamic "match_condition" { 301 | for_each = length(local.cdn_frontdoor_rate_limiting_bypass_ip_list) > 0 ? [0] : [] 302 | 303 | content { 304 | match_variable = "RemoteAddr" 305 | operator = "IPMatch" 306 | negation_condition = true 307 | match_values = local.cdn_frontdoor_rate_limiting_bypass_ip_list 308 | } 309 | } 310 | 311 | match_condition { 312 | match_variable = "RequestUri" 313 | operator = "Any" 314 | negation_condition = false 315 | match_values = [] 316 | } 317 | } 318 | } 319 | 320 | dynamic "custom_rule" { 321 | for_each = local.cdn_frontdoor_waf_custom_rules 322 | 323 | content { 324 | name = custom_rule.key 325 | enabled = true 326 | priority = custom_rule.value["priority"] 327 | type = "MatchRule" 328 | action = custom_rule.value["action"] 329 | 330 | dynamic "match_condition" { 331 | for_each = custom_rule.value["match_conditions"] 332 | 333 | content { 334 | match_variable = match_condition.value["match_variable"] 335 | match_values = match_condition.value["match_values"] 336 | operator = match_condition.value["operator"] 337 | selector = match_condition.value["selector"] 338 | } 339 | } 340 | } 341 | } 342 | 343 | dynamic "managed_rule" { 344 | for_each = local.cdn_frontdoor_waf_managed_rulesets 345 | 346 | content { 347 | type = managed_rule.key 348 | version = managed_rule.value["version"] 349 | action = managed_rule.value["action"] 350 | 351 | dynamic "exclusion" { 352 | for_each = managed_rule.value["exclusions"] 353 | 354 | content { 355 | match_variable = exclusion.value["match_variable"] 356 | operator = exclusion.value["operator"] 357 | selector = exclusion.value["selector"] 358 | } 359 | } 360 | 361 | dynamic "override" { 362 | for_each = managed_rule.value["overrides"] 363 | 364 | content { 365 | rule_group_name = override.key 366 | 367 | dynamic "rule" { 368 | for_each = override.value 369 | 370 | content { 371 | rule_id = rule.key 372 | enabled = rule.value["enabled"] 373 | action = rule.value["action"] 374 | 375 | dynamic "exclusion" { 376 | for_each = rule.value["exclusions"] 377 | 378 | content { 379 | match_variable = exclusion.value["match_variable"] 380 | operator = exclusion.value["operator"] 381 | selector = exclusion.value["selector"] 382 | } 383 | } 384 | } 385 | } 386 | } 387 | } 388 | } 389 | } 390 | 391 | tags = local.tags 392 | } 393 | 394 | resource "azurerm_cdn_frontdoor_security_policy" "waf" { 395 | count = local.cdn_frontdoor_enable_waf ? 1 : 0 396 | 397 | name = "${replace(local.resource_prefix, "-", "")}wafsecuritypolicy" 398 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 399 | 400 | security_policies { 401 | firewall { 402 | cdn_frontdoor_firewall_policy_id = azurerm_cdn_frontdoor_firewall_policy.waf[0].id 403 | 404 | association { 405 | patterns_to_match = ["/*"] 406 | 407 | domain { 408 | cdn_frontdoor_domain_id = azurerm_cdn_frontdoor_endpoint.endpoint[0].id 409 | } 410 | 411 | dynamic "domain" { 412 | for_each = local.enable_cdn_frontdoor ? { for name, container in local.custom_container_apps : name => container 413 | if container.ingress.external_enabled 414 | } : {} 415 | 416 | content { 417 | cdn_frontdoor_domain_id = azurerm_cdn_frontdoor_endpoint.custom_container_apps[domain.key].id 418 | } 419 | } 420 | 421 | dynamic "domain" { 422 | for_each = toset(local.cdn_frontdoor_custom_domains) 423 | 424 | content { 425 | cdn_frontdoor_domain_id = azurerm_cdn_frontdoor_custom_domain.custom_domain[domain.value].id 426 | } 427 | } 428 | 429 | dynamic "domain" { 430 | for_each = local.enable_cdn_frontdoor ? { for name, container in local.custom_container_apps : name => container 431 | if container.ingress.external_enabled && container.ingress.cdn_frontdoor_custom_domain != "" 432 | } : {} 433 | 434 | content { 435 | cdn_frontdoor_domain_id = azurerm_cdn_frontdoor_custom_domain.custom_container_apps[domain.key].id 436 | } 437 | } 438 | } 439 | } 440 | } 441 | } 442 | 443 | resource "azurerm_cdn_frontdoor_rule_set" "add_response_headers" { 444 | count = local.enable_cdn_frontdoor && length(local.cdn_frontdoor_host_add_response_headers) > 0 ? 1 : 0 445 | 446 | name = "${replace(local.resource_prefix, "-", "")}addresponseheaders" 447 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 448 | } 449 | 450 | resource "azurerm_cdn_frontdoor_rule" "add_response_headers" { 451 | for_each = local.enable_cdn_frontdoor ? { for index, response_header in local.cdn_frontdoor_host_add_response_headers : index => { "name" : response_header.name, "value" : response_header.value } } : {} 452 | 453 | depends_on = [azurerm_cdn_frontdoor_origin_group.group, azurerm_cdn_frontdoor_origin.origin] 454 | 455 | name = replace("addresponseheaders${each.key}", "-", "") 456 | cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.add_response_headers[0].id 457 | order = each.key 458 | behavior_on_match = "Continue" 459 | 460 | actions { 461 | response_header_action { 462 | header_action = "Overwrite" 463 | header_name = each.value.name 464 | value = each.value.value 465 | } 466 | } 467 | } 468 | 469 | resource "azurerm_cdn_frontdoor_rule_set" "remove_response_headers" { 470 | count = local.enable_cdn_frontdoor && length(local.cdn_frontdoor_remove_response_headers) > 0 ? 1 : 0 471 | 472 | name = "${replace(local.resource_prefix, "-", "")}removeresponseheaders" 473 | cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.cdn[0].id 474 | } 475 | 476 | resource "azurerm_cdn_frontdoor_rule" "remove_response_header" { 477 | for_each = local.enable_cdn_frontdoor ? toset(local.cdn_frontdoor_remove_response_headers) : [] 478 | 479 | depends_on = [azurerm_cdn_frontdoor_origin_group.group, azurerm_cdn_frontdoor_origin.origin] 480 | 481 | name = replace("removeresponseheader${each.value}", "-", "") 482 | cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.remove_response_headers[0].id 483 | order = index(local.cdn_frontdoor_remove_response_headers, each.value) 484 | behavior_on_match = "Continue" 485 | 486 | actions { 487 | response_header_action { 488 | header_action = "Delete" 489 | header_name = each.value 490 | } 491 | } 492 | } 493 | 494 | resource "azurerm_monitor_diagnostic_setting" "cdn" { 495 | count = local.enable_cdn_frontdoor ? 1 : 0 496 | 497 | name = "${local.resource_prefix}cdn" 498 | target_resource_id = azurerm_cdn_frontdoor_profile.cdn[0].id 499 | log_analytics_workspace_id = azurerm_log_analytics_workspace.container_app.id 500 | 501 | dynamic "enabled_log" { 502 | for_each = local.cdn_frontdoor_enable_waf_logs ? [1] : [] 503 | 504 | content { 505 | category = "FrontdoorWebApplicationFirewallLog" 506 | } 507 | } 508 | 509 | dynamic "enabled_log" { 510 | for_each = local.cdn_frontdoor_enable_access_logs ? [1] : [] 511 | 512 | content { 513 | category = "FrontdoorAccessLog" 514 | } 515 | } 516 | 517 | dynamic "enabled_log" { 518 | for_each = local.cdn_frontdoor_enable_health_probe_logs ? [1] : [] 519 | 520 | content { 521 | category = "FrontdoorHealthProbeLog" 522 | } 523 | } 524 | 525 | # The below metrics are kept in to avoid a diff in the Terraform Plan output 526 | metric { 527 | category = "AllMetrics" 528 | enabled = false 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /monitor.tf: -------------------------------------------------------------------------------- 1 | resource "azurerm_monitor_action_group" "main" { 2 | count = local.enable_monitoring ? 1 : 0 3 | 4 | name = "${local.resource_prefix}-actiongroup" 5 | resource_group_name = local.resource_group.name 6 | short_name = local.project_name 7 | tags = local.tags 8 | 9 | dynamic "email_receiver" { 10 | for_each = local.monitor_email_receivers 11 | 12 | content { 13 | name = "Email ${email_receiver.value}" 14 | email_address = email_receiver.value 15 | use_common_alert_schema = true 16 | } 17 | } 18 | 19 | dynamic "event_hub_receiver" { 20 | for_each = local.enable_event_hub ? [0] : [] 21 | 22 | content { 23 | name = "Event Hub" 24 | event_hub_name = azurerm_eventhub.container_app[0].name 25 | event_hub_namespace = azurerm_eventhub_namespace.container_app[0].id 26 | subscription_id = data.azurerm_subscription.current.subscription_id 27 | use_common_alert_schema = true 28 | } 29 | } 30 | 31 | dynamic "logic_app_receiver" { 32 | for_each = local.existing_logic_app_workflow.name != "" ? [0] : [] 33 | 34 | content { 35 | name = local.monitor_logic_app_receiver.name 36 | resource_id = local.monitor_logic_app_receiver.resource_id 37 | callback_url = local.monitor_logic_app_receiver.callback_url 38 | use_common_alert_schema = true 39 | } 40 | } 41 | } 42 | 43 | resource "azurerm_monitor_metric_alert" "cpu" { 44 | for_each = local.enable_monitoring ? local.monitor_containers : {} 45 | 46 | name = "Container App CPU - ${each.value.name}" 47 | resource_group_name = local.resource_group.name 48 | scopes = [each.value.id] 49 | description = "Container App ${each.value.name} is consuming more than ${local.alarm_cpu_threshold_percentage}% of CPU" 50 | window_size = "PT5M" 51 | frequency = "PT5M" 52 | severity = 2 # Warning 53 | 54 | criteria { 55 | metric_namespace = "microsoft.app/containerapps" 56 | metric_name = "UsageNanoCores" 57 | aggregation = "Average" 58 | operator = "GreaterThan" 59 | # CPU usage in nanocores (1,000,000,000 nanocores = 1 core) 60 | threshold = ((each.value.template[0].container[0].cpu * 10000000) * local.alarm_cpu_threshold_percentage) 61 | } 62 | 63 | action { 64 | action_group_id = azurerm_monitor_action_group.main[0].id 65 | } 66 | 67 | tags = local.tags 68 | } 69 | 70 | resource "azurerm_monitor_metric_alert" "memory" { 71 | for_each = local.enable_monitoring ? local.monitor_containers : {} 72 | 73 | name = "Container App Memory - ${each.value.name}" 74 | resource_group_name = local.resource_group.name 75 | scopes = [each.value.id] 76 | description = "Container App ${each.value.name} is consuming more than ${local.alarm_memory_threshold_percentage}% of Memory" 77 | window_size = "PT5M" 78 | frequency = "PT5M" 79 | severity = 2 # Warning 80 | 81 | criteria { 82 | metric_namespace = "microsoft.app/containerapps" 83 | metric_name = "WorkingSetBytes" 84 | aggregation = "Average" 85 | operator = "GreaterThan" 86 | # Memory usage in bytes (1,000,000,000 bytes = 1 GB) 87 | threshold = ((replace(each.value.template[0].container[0].memory, "Gi", "") * 10000000) * local.alarm_memory_threshold_percentage) 88 | } 89 | 90 | action { 91 | action_group_id = azurerm_monitor_action_group.main[0].id 92 | } 93 | 94 | tags = local.tags 95 | } 96 | 97 | resource "azurerm_monitor_metric_alert" "count" { 98 | for_each = local.enable_monitoring ? local.monitor_containers : {} 99 | 100 | name = "Container App Replica Count - ${each.value.name}" 101 | resource_group_name = local.resource_group.name 102 | scopes = [each.value.id] 103 | description = "Container App ${each.value.name} replica count is less than the expected average" 104 | window_size = "PT5M" 105 | frequency = "PT1M" 106 | severity = 1 # Error 107 | 108 | dynamic_criteria { 109 | metric_namespace = "microsoft.app/containerapps" 110 | metric_name = "Replicas" 111 | aggregation = "Average" 112 | operator = "LessThan" 113 | alert_sensitivity = "Medium" 114 | } 115 | 116 | action { 117 | action_group_id = azurerm_monitor_action_group.main[0].id 118 | } 119 | 120 | tags = local.tags 121 | } 122 | 123 | resource "azurerm_monitor_metric_alert" "sql_cpu" { 124 | count = local.enable_monitoring && local.enable_mssql_database ? 1 : 0 125 | 126 | name = "SQL Database CPU - ${azurerm_mssql_database.default[0].name}" 127 | resource_group_name = local.resource_group.name 128 | scopes = [azurerm_mssql_database.default[0].id] 129 | description = "SQL Database ${azurerm_mssql_server.default[0].name}/${azurerm_mssql_database.default[0].name} is consuming more than 80% of CPU" 130 | window_size = "PT5M" 131 | frequency = "PT5M" 132 | severity = 2 # Warning 133 | 134 | criteria { 135 | metric_namespace = "Microsoft.Sql/servers/databases" 136 | metric_name = "cpu_percent" 137 | aggregation = "Average" 138 | operator = "GreaterThan" 139 | threshold = 80 140 | } 141 | 142 | action { 143 | action_group_id = azurerm_monitor_action_group.main[0].id 144 | } 145 | 146 | tags = local.tags 147 | } 148 | 149 | resource "azurerm_monitor_metric_alert" "sql_dtu" { 150 | count = local.enable_monitoring && local.enable_mssql_database ? 1 : 0 151 | 152 | name = "SQL Database DTU - ${azurerm_mssql_database.default[0].name}" 153 | resource_group_name = local.resource_group.name 154 | scopes = [azurerm_mssql_database.default[0].id] 155 | description = "SQL Database ${azurerm_mssql_server.default[0].name}/${azurerm_mssql_database.default[0].name} is consuming more than 80% of available DTUs" 156 | window_size = "PT5M" 157 | frequency = "PT5M" 158 | severity = 2 # Warning 159 | 160 | criteria { 161 | metric_namespace = "Microsoft.Sql/servers/databases" 162 | metric_name = "dtu_consumption_percent" 163 | aggregation = "Average" 164 | operator = "GreaterThan" 165 | threshold = 80 166 | } 167 | 168 | action { 169 | action_group_id = azurerm_monitor_action_group.main[0].id 170 | } 171 | 172 | tags = local.tags 173 | } 174 | 175 | resource "azurerm_monitor_smart_detector_alert_rule" "ai_smart_failures" { 176 | count = local.enable_monitoring && local.enable_app_insights_integration && local.app_insights_smart_detection_enabled ? 1 : 0 177 | 178 | name = "Failure Anomalies - ${azurerm_application_insights.main[0].name}" 179 | description = "Failure Anomalies notifies you of an unusual rise in the rate of failed HTTP requests or dependency calls." 180 | resource_group_name = local.resource_group.name 181 | severity = "Sev2" # Warning 182 | scope_resource_ids = [azurerm_application_insights.main[0].id] 183 | frequency = "PT1M" 184 | detector_type = "FailureAnomaliesDetector" 185 | 186 | action_group { 187 | ids = [azurerm_monitor_action_group.main[0].id] 188 | } 189 | 190 | tags = local.tags 191 | } 192 | 193 | resource "azurerm_monitor_smart_detector_alert_rule" "ai_smart_performance_degradation" { 194 | count = local.enable_monitoring && local.enable_app_insights_integration && local.app_insights_smart_detection_enabled ? 1 : 0 195 | 196 | name = "Request Performance Degradation - ${azurerm_application_insights.main[0].name}" 197 | description = "Request Performance Degradation notifies you when your app has started responding to requests more slowly than it used to." 198 | resource_group_name = local.resource_group.name 199 | severity = "Sev2" # Warning 200 | scope_resource_ids = [azurerm_application_insights.main[0].id] 201 | frequency = "P1D" 202 | detector_type = "RequestPerformanceDegradationDetector" 203 | 204 | action_group { 205 | ids = [azurerm_monitor_action_group.main[0].id] 206 | } 207 | 208 | tags = local.tags 209 | } 210 | 211 | resource "azurerm_monitor_smart_detector_alert_rule" "ai_smart_dependency_degradation" { 212 | count = local.enable_monitoring && local.enable_app_insights_integration && local.app_insights_smart_detection_enabled ? 1 : 0 213 | 214 | name = "Dependency Performance Degradation - ${azurerm_application_insights.main[0].name}" 215 | description = "Dependency Performance Degradation notifies you when your app makes calls to a REST API, database, or other dependency. The dependency is responding more slowly than it used to." 216 | resource_group_name = local.resource_group.name 217 | severity = "Sev2" # Warning 218 | scope_resource_ids = [azurerm_application_insights.main[0].id] 219 | frequency = "P1D" 220 | detector_type = "DependencyPerformanceDegradationDetector" 221 | 222 | action_group { 223 | ids = [azurerm_monitor_action_group.main[0].id] 224 | } 225 | 226 | tags = local.tags 227 | } 228 | 229 | resource "azurerm_monitor_smart_detector_alert_rule" "ai_smart_exception_volume" { 230 | count = local.enable_monitoring && local.enable_app_insights_integration && local.app_insights_smart_detection_enabled ? 1 : 0 231 | 232 | name = "Exception Volume Changed - ${azurerm_application_insights.main[0].name}" 233 | description = "Exception Volume Changed notifies you when your app is showing an abnormal rise in the number of exceptions of a specific type, during a day." 234 | resource_group_name = local.resource_group.name 235 | severity = "Sev2" # Warning 236 | scope_resource_ids = [azurerm_application_insights.main[0].id] 237 | frequency = "P1D" 238 | detector_type = "ExceptionVolumeChangedDetector" 239 | 240 | action_group { 241 | ids = [azurerm_monitor_action_group.main[0].id] 242 | } 243 | 244 | tags = local.tags 245 | } 246 | 247 | resource "azurerm_monitor_smart_detector_alert_rule" "ai_smart_trace_severity" { 248 | count = local.enable_monitoring && local.enable_app_insights_integration && local.app_insights_smart_detection_enabled ? 1 : 0 249 | 250 | name = "Trace Severity - ${azurerm_application_insights.main[0].name}" 251 | description = "Trace Severity notifies you if the ratio between “good” traces (traces logged with a level of Info or Verbose) and “bad” traces (traces logged with a level of Warning, Error, or Fatal) is degrading in a specific day." 252 | resource_group_name = local.resource_group.name 253 | severity = "Sev2" # Warning 254 | scope_resource_ids = [azurerm_application_insights.main[0].id] 255 | frequency = "P1D" 256 | detector_type = "TraceSeverityDetector" 257 | 258 | action_group { 259 | ids = [azurerm_monitor_action_group.main[0].id] 260 | } 261 | 262 | tags = local.tags 263 | } 264 | 265 | resource "azurerm_monitor_smart_detector_alert_rule" "ai_smart_memory_leak" { 266 | count = local.enable_monitoring && local.enable_app_insights_integration && local.app_insights_smart_detection_enabled ? 1 : 0 267 | 268 | name = "Memory Leak - ${azurerm_application_insights.main[0].name}" 269 | description = "Memory Leak analyzes the memory consumption of each process in your application. It can warn you about potential memory leaks or increased memory consumption." 270 | resource_group_name = local.resource_group.name 271 | severity = "Sev2" # Warning 272 | scope_resource_ids = [azurerm_application_insights.main[0].id] 273 | frequency = "P1D" 274 | detector_type = "MemoryLeakDetector" 275 | 276 | action_group { 277 | ids = [azurerm_monitor_action_group.main[0].id] 278 | } 279 | 280 | tags = local.tags 281 | } 282 | 283 | resource "azurerm_application_insights_smart_detection_rule" "ai_slow_page" { 284 | count = local.enable_app_insights_integration ? 1 : 0 285 | 286 | name = "Slow page load time" 287 | application_insights_id = azurerm_application_insights.main[0].id 288 | additional_email_recipients = local.monitor_email_receivers 289 | send_emails_to_subscription_owners = false 290 | enabled = local.app_insights_smart_detection_enabled 291 | } 292 | 293 | resource "azurerm_application_insights_smart_detection_rule" "ai_slow_server" { 294 | count = local.enable_app_insights_integration ? 1 : 0 295 | 296 | name = "Slow server response time" 297 | application_insights_id = azurerm_application_insights.main[0].id 298 | additional_email_recipients = local.monitor_email_receivers 299 | send_emails_to_subscription_owners = false 300 | enabled = local.app_insights_smart_detection_enabled 301 | } 302 | 303 | resource "azurerm_application_insights_smart_detection_rule" "ai_memory" { 304 | count = local.enable_app_insights_integration ? 1 : 0 305 | 306 | name = "Potential memory leak detected" 307 | application_insights_id = azurerm_application_insights.main[0].id 308 | additional_email_recipients = local.monitor_email_receivers 309 | send_emails_to_subscription_owners = false 310 | enabled = local.app_insights_smart_detection_enabled 311 | } 312 | 313 | resource "azurerm_application_insights_smart_detection_rule" "ai_security" { 314 | count = local.enable_app_insights_integration ? 1 : 0 315 | 316 | name = "Potential security issue detected" 317 | application_insights_id = azurerm_application_insights.main[0].id 318 | additional_email_recipients = local.monitor_email_receivers 319 | send_emails_to_subscription_owners = false 320 | enabled = local.app_insights_smart_detection_enabled 321 | } 322 | 323 | resource "azurerm_application_insights_smart_detection_rule" "ai_trace" { 324 | count = local.enable_app_insights_integration ? 1 : 0 325 | 326 | name = "Degradation in trace severity ratio" 327 | application_insights_id = azurerm_application_insights.main[0].id 328 | additional_email_recipients = local.monitor_email_receivers 329 | send_emails_to_subscription_owners = false 330 | enabled = local.app_insights_smart_detection_enabled 331 | } 332 | 333 | resource "azurerm_application_insights_smart_detection_rule" "ai_long_dependency_duration" { 334 | count = local.enable_app_insights_integration ? 1 : 0 335 | 336 | name = "Long dependency duration" 337 | application_insights_id = azurerm_application_insights.main[0].id 338 | additional_email_recipients = local.monitor_email_receivers 339 | send_emails_to_subscription_owners = false 340 | enabled = local.app_insights_smart_detection_enabled 341 | } 342 | 343 | resource "azurerm_application_insights_smart_detection_rule" "ai_response_time" { 344 | count = local.enable_app_insights_integration ? 1 : 0 345 | 346 | name = "Degradation in server response time" 347 | application_insights_id = azurerm_application_insights.main[0].id 348 | additional_email_recipients = local.monitor_email_receivers 349 | send_emails_to_subscription_owners = false 350 | enabled = local.app_insights_smart_detection_enabled 351 | } 352 | 353 | resource "azurerm_application_insights_smart_detection_rule" "ai_exception_volume" { 354 | count = local.enable_app_insights_integration ? 1 : 0 355 | 356 | name = "Abnormal rise in exception volume" 357 | application_insights_id = azurerm_application_insights.main[0].id 358 | additional_email_recipients = local.monitor_email_receivers 359 | send_emails_to_subscription_owners = false 360 | enabled = local.app_insights_smart_detection_enabled 361 | } 362 | 363 | resource "azurerm_application_insights_smart_detection_rule" "ai_dependency_duration" { 364 | count = local.enable_app_insights_integration ? 1 : 0 365 | 366 | name = "Degradation in dependency duration" 367 | application_insights_id = azurerm_application_insights.main[0].id 368 | additional_email_recipients = local.monitor_email_receivers 369 | send_emails_to_subscription_owners = false 370 | enabled = local.app_insights_smart_detection_enabled 371 | } 372 | 373 | resource "azurerm_application_insights_smart_detection_rule" "ai_data_volume" { 374 | count = local.enable_app_insights_integration ? 1 : 0 375 | 376 | name = "Abnormal rise in daily data volume" 377 | application_insights_id = azurerm_application_insights.main[0].id 378 | additional_email_recipients = local.monitor_email_receivers 379 | send_emails_to_subscription_owners = false 380 | enabled = local.app_insights_smart_detection_enabled 381 | } 382 | 383 | resource "azurerm_monitor_scheduled_query_rules_alert_v2" "exceptions" { 384 | count = local.enable_monitoring && local.enable_app_insights_integration ? 1 : 0 385 | 386 | name = "Exceptions Count - ${azurerm_application_insights.main[0].name}" 387 | resource_group_name = local.resource_group.name 388 | location = local.resource_group.location 389 | evaluation_frequency = "PT5M" 390 | window_duration = "PT5M" 391 | scopes = [azurerm_application_insights.main[0].id] 392 | severity = 2 # Warning 393 | description = "Action will be triggered when an Exception is raised in App Insights" 394 | 395 | criteria { 396 | query = <<-QUERY 397 | exceptions 398 | | where isnotempty(operation_Name) 399 | | join requests on $left.operation_Id == $right.operation_Id 400 | | where toint(severityLevel) >= 2 401 | | extend severity = case( 402 | severityLevel == 4, "Fatal", 403 | severityLevel == 3, "Error", 404 | severityLevel == 2, "Warning", 405 | "Unknown" // Default case 406 | ) 407 | | extend message = strcat(type, ": ", outerMessage) 408 | | extend linkToAppInsights = strcat( 409 | "https://portal.azure.com/#blade/AppInsightsExtension/DetailsV2Blade/DataModel/", 410 | url_encode(strcat('{"eventId":"', itemId, '","timestamp":"', timestamp, '"}')), 411 | "/ComponentId/", 412 | url_encode(strcat('{"Name":"', split(appName, "/", 8)[0], '","ResourceGroup":"', split(appName, "/", 4)[0], '","SubscriptionId":"', split(appName, "/", 2)[0], '"}')) 413 | ) 414 | | project operation_Id, timestamp, operation_Name, message, severity, url, resultCode, linkToAppInsights 415 | | order by timestamp desc 416 | QUERY 417 | 418 | time_aggregation_method = "Count" 419 | threshold = 1 420 | operator = "GreaterThanOrEqual" 421 | 422 | // Keep dimensions ordered by 'name' as that is how they will be presented 423 | // in the Common Alert Schema 424 | dimension { 425 | name = "linkToAppInsights" 426 | operator = "Include" 427 | values = ["*"] 428 | } 429 | 430 | dimension { 431 | name = "message" 432 | operator = "Include" 433 | values = ["*"] 434 | } 435 | 436 | dimension { 437 | name = "operation_Id" 438 | operator = "Include" 439 | values = ["*"] 440 | } 441 | 442 | dimension { 443 | name = "operation_Name" 444 | operator = "Include" 445 | values = ["*"] 446 | } 447 | 448 | dimension { 449 | name = "resultCode" 450 | operator = "Include" 451 | values = ["*"] 452 | } 453 | 454 | dimension { 455 | name = "severity" 456 | operator = "Include" 457 | values = ["*"] 458 | } 459 | 460 | dimension { 461 | name = "url" 462 | operator = "Include" 463 | values = ["*"] 464 | } 465 | 466 | failing_periods { 467 | minimum_failing_periods_to_trigger_alert = 1 468 | number_of_evaluation_periods = 1 469 | } 470 | } 471 | 472 | auto_mitigation_enabled = false 473 | 474 | action { 475 | action_groups = [azurerm_monitor_action_group.main[0].id] 476 | } 477 | 478 | tags = local.tags 479 | } 480 | 481 | resource "azurerm_monitor_scheduled_query_rules_alert_v2" "traces" { 482 | count = local.enable_monitoring && local.enable_monitoring_traces && local.enable_app_insights_integration ? 1 : 0 483 | 484 | name = "Error Count - ${azurerm_application_insights.main[0].name}" 485 | resource_group_name = local.resource_group.name 486 | location = local.resource_group.location 487 | evaluation_frequency = "PT5M" 488 | window_duration = "PT15M" 489 | scopes = [azurerm_application_insights.main[0].id] 490 | severity = 2 # Warning 491 | description = "Action will be triggered when ${local.enable_monitoring_traces_include_warnings ? "warnings or " : ""}errors are detected in App Insights traces" 492 | 493 | criteria { 494 | query = <<-QUERY 495 | traces 496 | | where 497 | isempty(customDimensions.StatusCode) or 498 | (isnotempty(customDimensions.StatusCode) and customDimensions.StatusCode >= 500) 499 | | where isnotempty(operation_Name) 500 | | where severityLevel >= ${local.enable_monitoring_traces_include_warnings ? 2 : 3} 501 | | extend linkToAppInsights = strcat( 502 | "https://portal.azure.com/#blade/AppInsightsExtension/DetailsV2Blade/DataModel/", 503 | url_encode(strcat('{"eventId":"', itemId, '","timestamp":"', timestamp, '"}')), 504 | "/ComponentId/", 505 | url_encode(strcat('{"Name":"', split(appName, "/", 8)[0], '","ResourceGroup":"', split(appName, "/", 4)[0], '","SubscriptionId":"', split(appName, "/", 2)[0], '"}')) 506 | ) 507 | | extend severity = case( 508 | severityLevel == 4, "Fatal", 509 | severityLevel == 3, "Error", 510 | severityLevel == 2, "Warning", 511 | "Unknown" // Default case 512 | ) 513 | | join requests on $left.operation_Id == $right.operation_Id 514 | | project operation_Id, timestamp, operation_Name, message, severity, url, resultCode, linkToAppInsights 515 | | order by timestamp desc 516 | QUERY 517 | 518 | time_aggregation_method = "Count" 519 | threshold = 1 520 | operator = "GreaterThanOrEqual" 521 | 522 | // Keep dimensions ordered by 'name' as that is how they will be presented 523 | // in the Common Alert Schema 524 | dimension { 525 | name = "linkToAppInsights" 526 | operator = "Include" 527 | values = ["*"] 528 | } 529 | 530 | dimension { 531 | name = "message" 532 | operator = "Include" 533 | values = ["*"] 534 | } 535 | 536 | dimension { 537 | name = "operation_Id" 538 | operator = "Include" 539 | values = ["*"] 540 | } 541 | 542 | dimension { 543 | name = "operation_Name" 544 | operator = "Include" 545 | values = ["*"] 546 | } 547 | 548 | dimension { 549 | name = "resultCode" 550 | operator = "Include" 551 | values = ["*"] 552 | } 553 | 554 | dimension { 555 | name = "severity" 556 | operator = "Include" 557 | values = ["*"] 558 | } 559 | 560 | dimension { 561 | name = "url" 562 | operator = "Include" 563 | values = ["*"] 564 | } 565 | 566 | failing_periods { 567 | minimum_failing_periods_to_trigger_alert = 1 568 | number_of_evaluation_periods = 1 569 | } 570 | } 571 | 572 | auto_mitigation_enabled = false 573 | 574 | action { 575 | action_groups = [azurerm_monitor_action_group.main[0].id] 576 | } 577 | 578 | tags = local.tags 579 | } 580 | 581 | resource "azurerm_monitor_metric_alert" "http" { 582 | count = local.enable_monitoring && local.enable_app_insights_integration ? 1 : 0 583 | 584 | name = "HTTP Availability Test - ${azurerm_application_insights.main[0].name}" 585 | resource_group_name = local.resource_group.name 586 | # Scope requires web test to come first 587 | # https://github.com/hashicorp/terraform-provider-azurerm/issues/8551 588 | scopes = [azurerm_application_insights_standard_web_test.main[0].id, azurerm_application_insights.main[0].id] 589 | description = "HTTP URL ${local.monitor_http_availability_url} could not be reached by 2 out of 3 locations" 590 | severity = 0 # Critical 591 | 592 | application_insights_web_test_location_availability_criteria { 593 | web_test_id = azurerm_application_insights_standard_web_test.main[0].id 594 | component_id = azurerm_application_insights.main[0].id 595 | failed_location_count = 2 # 2 out of 3 locations 596 | } 597 | 598 | action { 599 | action_group_id = azurerm_monitor_action_group.main[0].id 600 | } 601 | 602 | tags = local.tags 603 | } 604 | 605 | resource "azurerm_monitor_metric_alert" "redis" { 606 | count = local.enable_monitoring && local.enable_redis_cache ? 1 : 0 607 | 608 | name = "Azure Cache for Redis CPU - ${azurerm_redis_cache.default[0].name}" 609 | resource_group_name = local.resource_group.name 610 | scopes = [azurerm_redis_cache.default[0].id] 611 | description = "Azure Cache for Redis ${azurerm_redis_cache.default[0].name} is consuming more than 80% of CPU" 612 | window_size = "PT5M" 613 | frequency = "PT1M" 614 | severity = 2 # Warning 615 | 616 | criteria { 617 | metric_namespace = "Microsoft.Cache/Redis" 618 | metric_name = "allserverLoad" 619 | aggregation = "Average" 620 | operator = "GreaterThan" 621 | # Number used as % 622 | threshold = 80 623 | } 624 | 625 | action { 626 | action_group_id = azurerm_monitor_action_group.main[0].id 627 | } 628 | 629 | tags = local.tags 630 | } 631 | 632 | resource "azurerm_monitor_metric_alert" "latency" { 633 | count = local.enable_monitoring && local.enable_cdn_frontdoor ? 1 : 0 634 | 635 | name = "Azure Front Door Total Latency - ${azurerm_cdn_frontdoor_profile.cdn[0].name}" 636 | resource_group_name = local.resource_group.name 637 | scopes = [azurerm_cdn_frontdoor_profile.cdn[0].id] 638 | description = "Azure Front Door ${azurerm_cdn_frontdoor_profile.cdn[0].name} total latency is greater than ${local.alarm_latency_threshold_ms / 1000}s" 639 | window_size = "PT5M" 640 | frequency = "PT5M" 641 | severity = 2 # Warning 642 | 643 | criteria { 644 | metric_namespace = "Microsoft.Cdn/profiles" 645 | metric_name = "TotalLatency" 646 | aggregation = "Minimum" 647 | operator = "GreaterThan" 648 | # 1,000ms = 1s 649 | threshold = local.alarm_latency_threshold_ms 650 | } 651 | 652 | action { 653 | action_group_id = azurerm_monitor_action_group.main[0].id 654 | } 655 | 656 | tags = local.tags 657 | } 658 | 659 | resource "azurerm_monitor_scheduled_query_rules_alert_v2" "log-analytics-ingestion" { 660 | count = local.enable_monitoring && local.alarm_log_ingestion_gb_per_day != 0 ? 1 : 0 661 | 662 | name = "Log Ingestion Rate - ${azurerm_log_analytics_workspace.container_app.name}" 663 | description = "${azurerm_log_analytics_workspace.container_app.name} log ingestion reaches more than ${local.alarm_log_ingestion_gb_per_day}GB/day" 664 | resource_group_name = local.resource_group.name 665 | location = local.resource_group.location 666 | 667 | criteria { 668 | operator = "GreaterThan" 669 | query = "Usage | where IsBillable | summarize DataGB = sum(Quantity / 1000)" 670 | threshold = local.alarm_log_ingestion_gb_per_day 671 | time_aggregation_method = "Total" 672 | metric_measure_column = "DataGB" 673 | } 674 | 675 | evaluation_frequency = "P1D" 676 | scopes = [azurerm_log_analytics_workspace.container_app.id] 677 | severity = 2 # Warning 678 | window_duration = "P1D" 679 | 680 | action { 681 | action_groups = [azurerm_monitor_action_group.main[0].id] 682 | } 683 | 684 | tags = local.tags 685 | } 686 | 687 | resource "azurerm_monitor_activity_log_alert" "delete_container_app" { 688 | for_each = local.enable_monitoring && local.alarm_for_delete_events ? merge(azurerm_container_app.container_apps, azurerm_container_app.custom_container_apps) : {} 689 | 690 | name = "Resource Deletion - Container App - ${each.value.name}" 691 | resource_group_name = local.resource_group.name 692 | location = local.resource_group.location 693 | scopes = [local.resource_group.id] 694 | description = "Delete Resource event started for Container App ${each.value.name}" 695 | 696 | criteria { 697 | resource_id = each.value.id 698 | operation_name = "microsoft.app/containerapps/delete" 699 | category = "Administrative" 700 | statuses = ["Started"] 701 | } 702 | 703 | action { 704 | action_group_id = azurerm_monitor_action_group.main[0].id 705 | } 706 | 707 | tags = local.tags 708 | } 709 | 710 | resource "azurerm_monitor_activity_log_alert" "delete_sql_database" { 711 | count = local.enable_monitoring && local.alarm_for_delete_events && local.enable_mssql_database ? 1 : 0 712 | 713 | name = "Resource Deletion - SQL Database - ${azurerm_mssql_database.default[0].name}" 714 | resource_group_name = local.resource_group.name 715 | location = local.resource_group.location 716 | scopes = [local.resource_group.id] 717 | description = "Delete Resource event started for SQL Database ${azurerm_mssql_database.default[0].name}" 718 | 719 | criteria { 720 | resource_id = azurerm_mssql_database.default[0].id 721 | operation_name = "microsoft.sql/servers/databases/delete" 722 | category = "Administrative" 723 | statuses = ["Started"] 724 | } 725 | 726 | action { 727 | action_group_id = azurerm_monitor_action_group.main[0].id 728 | } 729 | 730 | tags = local.tags 731 | } 732 | 733 | resource "azurerm_monitor_activity_log_alert" "delete_dns_zone" { 734 | count = local.enable_monitoring && local.alarm_for_delete_events && local.enable_dns_zone ? 1 : 0 735 | 736 | name = "Resource Deletion - DNS Zone - ${azurerm_dns_zone.default[0].name}" 737 | resource_group_name = local.resource_group.name 738 | location = local.resource_group.location 739 | scopes = [local.resource_group.id] 740 | description = "Delete Resource event started for DNS Zone ${azurerm_dns_zone.default[0].name}" 741 | 742 | criteria { 743 | resource_id = azurerm_dns_zone.default[0].id 744 | operation_name = "microsoft.network/dnszones/delete" 745 | category = "Administrative" 746 | statuses = ["Started"] 747 | } 748 | 749 | action { 750 | action_group_id = azurerm_monitor_action_group.main[0].id 751 | } 752 | 753 | tags = local.tags 754 | } 755 | 756 | resource "azurerm_monitor_activity_log_alert" "delete_redis_cache" { 757 | count = local.enable_monitoring && local.alarm_for_delete_events && local.enable_redis_cache ? 1 : 0 758 | 759 | name = "Resource Deletion - Redis Cache - ${azurerm_redis_cache.default[0].name}" 760 | resource_group_name = local.resource_group.name 761 | location = local.resource_group.location 762 | scopes = [local.resource_group.id] 763 | description = "Delete Resource event started for Redis Cache ${azurerm_redis_cache.default[0].name}" 764 | 765 | criteria { 766 | resource_id = azurerm_redis_cache.default[0].id 767 | operation_name = "microsoft.cache/redis/delete" 768 | category = "Administrative" 769 | statuses = ["Started"] 770 | } 771 | 772 | action { 773 | action_group_id = azurerm_monitor_action_group.main[0].id 774 | } 775 | 776 | tags = local.tags 777 | } 778 | 779 | resource "azurerm_monitor_activity_log_alert" "delete_postgresql_database" { 780 | count = local.enable_monitoring && local.alarm_for_delete_events && local.enable_postgresql_database ? 1 : 0 781 | 782 | name = "Resource Deletion - PostgreSQL Database - ${azurerm_postgresql_flexible_server_database.default[0].name}" 783 | resource_group_name = local.resource_group.name 784 | location = local.resource_group.location 785 | scopes = [local.resource_group.id] 786 | description = "Delete Resource event started for PostgreSQL Database ${azurerm_postgresql_flexible_server_database.default[0].name}" 787 | 788 | criteria { 789 | resource_id = azurerm_postgresql_flexible_server_database.default[0].id 790 | operation_name = "microsoft.dbforpostgresql/servers/databases/delete" 791 | category = "Administrative" 792 | statuses = ["Started"] 793 | } 794 | 795 | action { 796 | action_group_id = azurerm_monitor_action_group.main[0].id 797 | } 798 | 799 | tags = local.tags 800 | } 801 | 802 | resource "azurerm_monitor_activity_log_alert" "delete_frontdoor_cdn" { 803 | count = local.enable_monitoring && local.alarm_for_delete_events && local.enable_cdn_frontdoor ? 1 : 0 804 | 805 | name = "Resource Deletion - Front Door - ${azurerm_cdn_frontdoor_profile.cdn[0].name}" 806 | resource_group_name = local.resource_group.name 807 | location = local.resource_group.location 808 | scopes = [local.resource_group.id] 809 | description = "Delete Resource event started for Front Door ${azurerm_cdn_frontdoor_profile.cdn[0].name}" 810 | 811 | criteria { 812 | resource_id = azurerm_cdn_frontdoor_profile.cdn[0].id 813 | operation_name = "microsoft.cdn/profiles/delete" 814 | category = "Administrative" 815 | statuses = ["Started"] 816 | } 817 | 818 | action { 819 | action_group_id = azurerm_monitor_action_group.main[0].id 820 | } 821 | 822 | tags = local.tags 823 | } 824 | 825 | resource "azurerm_monitor_activity_log_alert" "delete_vnet" { 826 | count = local.enable_monitoring && local.alarm_for_delete_events && local.launch_in_vnet && local.existing_virtual_network == "" ? 1 : 0 827 | 828 | name = "Resource Deletion - Virtual Network - ${local.virtual_network.name}" 829 | resource_group_name = local.resource_group.name 830 | location = local.resource_group.location 831 | scopes = [local.resource_group.id] 832 | description = "Delete Resource event started for Virtual Network ${local.virtual_network.name}" 833 | 834 | criteria { 835 | resource_id = local.virtual_network.id 836 | operation_name = "microsoft.network/virtualnetworks/delete" 837 | category = "Administrative" 838 | statuses = ["Started"] 839 | } 840 | 841 | action { 842 | action_group_id = azurerm_monitor_action_group.main[0].id 843 | } 844 | 845 | tags = local.tags 846 | } 847 | -------------------------------------------------------------------------------- /locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | # Global options 3 | environment = var.environment 4 | project_name = var.project_name 5 | resource_prefix = "${local.environment}${local.project_name}" 6 | resource_prefix_sha = sha1(local.resource_prefix) 7 | resource_prefix_sha_short = substr(local.resource_prefix_sha, 0, 6) 8 | azure_location = var.azure_location 9 | tags = var.tags 10 | 11 | # Resource Group 12 | existing_resource_group = var.existing_resource_group 13 | resource_group = local.existing_resource_group == "" ? azurerm_resource_group.default[0] : data.azurerm_resource_group.existing_resource_group[0] 14 | enable_resource_group_lock = var.enable_resource_group_lock 15 | 16 | # Key Vault 17 | escrow_container_app_secrets_in_key_vault = var.escrow_container_app_secrets_in_key_vault 18 | existing_key_vault = var.existing_key_vault 19 | key_vault = !local.escrow_container_app_secrets_in_key_vault && local.existing_key_vault == "" ? null : local.existing_key_vault == "" ? azurerm_key_vault.default[0] : data.azurerm_key_vault.existing_key_vault[0] 20 | key_vault_managed_identity_assign_role = var.key_vault_managed_identity_assign_role 21 | key_vault_access_ipv4 = var.key_vault_access_ipv4 22 | 23 | # Networking 24 | launch_in_vnet = var.launch_in_vnet 25 | existing_virtual_network = var.existing_virtual_network 26 | virtual_network = local.existing_virtual_network == "" ? azurerm_virtual_network.default[0] : data.azurerm_virtual_network.existing_virtual_network[0] 27 | virtual_network_deny_all_egress = var.virtual_network_deny_all_egress 28 | virtual_network_address_space = var.virtual_network_address_space 29 | virtual_network_address_space_mask = element(split("/", local.virtual_network_address_space), 1) 30 | # Networking / Subnetting 31 | container_apps_infra_subnet_cidr = var.container_apps_infra_subnet_cidr == "" ? cidrsubnet(local.virtual_network_address_space, 23 - local.virtual_network_address_space_mask, 0) : var.container_apps_infra_subnet_cidr 32 | container_apps_infra_address_space_mask = element(split("/", local.container_apps_infra_subnet_cidr), 1) 33 | 34 | remaining_subnet_cidr = cidrsubnet(local.virtual_network_address_space, 1, 1) 35 | mssql_private_endpoint_subnet_cidr = var.mssql_private_endpoint_subnet_cidr == "" ? cidrsubnet(local.remaining_subnet_cidr, 27 - local.container_apps_infra_address_space_mask, 0) : var.mssql_private_endpoint_subnet_cidr 36 | registry_subnet_cidr = var.registry_subnet_cidr == "" ? cidrsubnet(local.remaining_subnet_cidr, 27 - local.container_apps_infra_address_space_mask, 1) : var.registry_subnet_cidr 37 | redis_cache_subnet_cidr = var.redis_cache_subnet_cidr == "" ? cidrsubnet(local.remaining_subnet_cidr, 27 - local.container_apps_infra_address_space_mask, 2) : var.redis_cache_subnet_cidr 38 | storage_subnet_cidr = var.storage_subnet_cidr == "" ? cidrsubnet(local.remaining_subnet_cidr, 27 - local.container_apps_infra_address_space_mask, 3) : var.storage_subnet_cidr 39 | app_configuration_subnet_cidr = var.app_configuration_subnet_cidr == "" ? cidrsubnet(local.remaining_subnet_cidr, 27 - local.container_apps_infra_address_space_mask, 4) : var.app_configuration_subnet_cidr 40 | postgresql_subnet_cidr = var.postgresql_subnet_cidr == "" ? cidrsubnet(local.remaining_subnet_cidr, 27 - local.container_apps_infra_address_space_mask, 5) : var.postgresql_subnet_cidr 41 | function_app_subnet_cidr = var.function_app_subnet_cidr == "" ? cidrsubnet(local.remaining_subnet_cidr, 27 - local.container_apps_infra_address_space_mask, 6) : var.function_app_subnet_cidr 42 | 43 | container_app_environment_internal_load_balancer_enabled = var.container_app_environment_internal_load_balancer_enabled 44 | container_apps_infra_subnet_service_endpoints = distinct(concat( 45 | local.launch_in_vnet && local.enable_storage_account ? ["Microsoft.Storage"] : [], 46 | var.container_apps_infra_subnet_service_endpoints, 47 | local.escrow_container_app_secrets_in_key_vault ? ["Microsoft.KeyVault"] : [] 48 | )) 49 | # Networking / Private Endpoints 50 | enable_private_endpoint_redis = local.enable_redis_cache ? ( 51 | local.launch_in_vnet ? true : false 52 | ) : false 53 | private_endpoint_redis = local.enable_private_endpoint_redis ? { 54 | "rediscache" : { 55 | resource_group : local.resource_group, 56 | subnet_id : azurerm_subnet.redis_cache_subnet[0].id, 57 | resource_id : azurerm_redis_cache.default[0].id, 58 | subresource_names : ["redisCache"], 59 | private_zone_id : azurerm_private_dns_zone.redis_cache_private_link[0].id, 60 | } 61 | } : {} 62 | enable_private_endpoint_mssql = local.enable_mssql_database ? ( 63 | local.launch_in_vnet ? true : false 64 | ) : false 65 | private_endpoint_mssql = local.enable_private_endpoint_mssql ? { 66 | "mssql" : { 67 | resource_group : local.resource_group, 68 | subnet_id : azurerm_subnet.mssql_private_endpoint_subnet[0].id, 69 | resource_id : azurerm_mssql_server.default[0].id, 70 | subresource_names : ["sqlServer"], 71 | private_zone_id : azurerm_private_dns_zone.mssql_private_link[0].id, 72 | } 73 | } : {} 74 | enable_private_endpoint_postgres = local.enable_postgresql_database && local.launch_in_vnet && local.postgresql_network_connectivity_method == "private" ? true : false 75 | private_endpoint_postgres = local.enable_private_endpoint_postgres ? { 76 | "postgres" : { 77 | resource_group : local.resource_group, 78 | subnet_id : azurerm_subnet.postgresql_subnet[0].id, 79 | resource_id : azurerm_postgresql_flexible_server.default[0].id, 80 | subresource_names : ["postgresqlServer"], 81 | private_zone_id : azurerm_private_dns_zone.postgresql_private_link[0].id, 82 | } 83 | } : {} 84 | enable_private_endpoint_registry = local.registry_sku == "Premium" ? true : false 85 | private_endpoint_registry = local.enable_private_endpoint_registry ? { 86 | "registry" : { 87 | resource_group : local.resource_group, 88 | subnet_id : azurerm_subnet.registry_private_endpoint_subnet[0].id, 89 | resource_id : azurerm_container_registry.acr[0].id, 90 | private_zone_id : azurerm_private_dns_zone.registry_private_link[0].id, 91 | } 92 | } : {} 93 | enable_private_endpoint_storage = local.enable_storage_account ? true : false 94 | private_endpoint_storage_blob = local.enable_container_app_blob_storage ? { 95 | "blob" : { 96 | resource_group : local.resource_group, 97 | subnet_id : azurerm_subnet.storage_private_endpoint_subnet[0].id, 98 | resource_id : azurerm_storage_account.container_app[0].id, 99 | subresource_names : ["blob"], 100 | private_zone_id : azurerm_private_dns_zone.storage_private_link_blob[0].id, 101 | } 102 | } : {} 103 | private_endpoint_storage_file = local.enable_container_app_file_share ? { 104 | "file" : { 105 | resource_group : local.resource_group, 106 | subnet_id : azurerm_subnet.storage_private_endpoint_subnet[0].id, 107 | resource_id : azurerm_storage_account.container_app[0].id, 108 | subresource_names : ["file"], 109 | private_zone_id : azurerm_private_dns_zone.storage_private_link_file[0].id, 110 | } 111 | } : {} 112 | enable_private_endpoint_app_configuration = local.enable_app_configuration && local.app_configuration_sku != "free" ? true : false 113 | private_endpoint_app_configuration = local.enable_private_endpoint_app_configuration ? { 114 | "appconfig" : { 115 | resource_group : local.resource_group, 116 | subnet_id : azurerm_subnet.app_configuration_private_endpoint_subnet[0].id, 117 | resource_id : azurerm_app_configuration.default[0].id, 118 | private_zone_id : azurerm_private_dns_zone.app_configuration_private_link[0].id, 119 | } 120 | } : {} 121 | private_endpoints = merge( 122 | local.private_endpoint_redis, 123 | local.private_endpoint_mssql, 124 | local.private_endpoint_postgres, 125 | local.private_endpoint_registry, 126 | local.private_endpoint_storage_blob, 127 | local.private_endpoint_storage_file, 128 | local.private_endpoint_app_configuration, 129 | ) 130 | 131 | # Azure Container Registry 132 | enable_container_registry = var.enable_container_registry 133 | registry_retention_days = var.registry_retention_days 134 | enable_registry_retention_policy = var.enable_registry_retention_policy 135 | registry_server = var.registry_server != "" ? var.registry_server : local.enable_container_registry ? azurerm_container_registry.acr[0].login_server : null 136 | registry_username = var.registry_username != "" ? var.registry_username : local.enable_container_registry ? azurerm_container_registry.acr[0].admin_username : null 137 | registry_password = var.registry_password != "" ? var.registry_password : local.enable_container_registry ? azurerm_container_registry.acr[0].admin_password : null 138 | registry_sku = var.registry_sku 139 | registry_admin_enabled = var.registry_admin_enabled 140 | registry_public_access_enabled = var.registry_public_access_enabled 141 | registry_ipv4_allow_list = var.registry_ipv4_allow_list 142 | registry_use_managed_identity = var.registry_use_managed_identity 143 | registry_managed_identity_assign_role = var.registry_managed_identity_assign_role 144 | 145 | # SQL Server 146 | enable_mssql_database = var.enable_mssql_database 147 | mssql_server_admin_password = var.mssql_server_admin_password 148 | mssql_sku_name = var.mssql_sku_name 149 | mssql_max_size_gb = var.mssql_max_size_gb 150 | mssql_database_name = var.mssql_database_name 151 | mssql_firewall_ipv4_allow_list = var.mssql_firewall_ipv4_allow_list 152 | mssql_azuread_admin_username = var.mssql_azuread_admin_username 153 | mssql_azuread_admin_object_id = var.mssql_azuread_admin_object_id 154 | mssql_azuread_auth_only = var.mssql_azuread_auth_only 155 | mssql_version = var.mssql_version 156 | mssql_server_public_access_enabled = var.mssql_server_public_access_enabled 157 | enable_mssql_vulnerability_assessment = var.enable_mssql_vulnerability_assessment 158 | mssql_security_storage_firewall_ipv4_allow_list = var.mssql_security_storage_firewall_ipv4_allow_list 159 | mssql_managed_identity_assign_role = var.mssql_managed_identity_assign_role 160 | mssql_maintenance_configuration_name = var.mssql_maintenance_configuration_name 161 | 162 | # Postgres Server 163 | enable_postgresql_database = var.enable_postgresql_database 164 | postgresql_server_version = var.postgresql_server_version 165 | postgresql_administrator_login = var.postgresql_administrator_login 166 | postgresql_administrator_password = var.postgresql_administrator_password 167 | postgresql_availability_zone = var.postgresql_availability_zone 168 | postgresql_max_storage_mb = var.postgresql_max_storage_mb 169 | postgresql_sku_name = var.postgresql_sku_name 170 | postgresql_enabled_extensions = var.postgresql_enabled_extensions 171 | postgresql_collation = var.postgresql_collation 172 | postgresql_charset = var.postgresql_charset 173 | postgresql_network_connectivity_method = var.postgresql_network_connectivity_method 174 | postgresql_firewall_ipv4_allow = merge( 175 | { 176 | "container-app" = { 177 | start_ip_address = azurerm_container_app.container_apps["main"].outbound_ip_addresses[0] 178 | end_ip_address = azurerm_container_app.container_apps["main"].outbound_ip_addresses[0] 179 | } 180 | }, 181 | var.postgresql_firewall_ipv4_allow 182 | ) 183 | 184 | # Azure Cache for Redis 185 | enable_redis_cache = var.enable_redis_cache 186 | redis_cache_version = var.redis_cache_version 187 | redis_cache_family = var.redis_cache_family 188 | redis_cache_sku = var.redis_cache_sku 189 | redis_cache_capacity = var.redis_cache_capacity 190 | redis_cache_patch_schedule_day = var.redis_cache_patch_schedule_day 191 | redis_cache_patch_schedule_hour = var.redis_cache_patch_schedule_hour 192 | redis_cache_firewall_ipv4_allow_list = var.redis_cache_firewall_ipv4_allow_list 193 | # Azure Cache for Redis/Configuration 194 | redis_config_defaults = { 195 | maxmemory_reserved = local.redis_cache_sku == "Basic" ? 2 : local.redis_cache_sku == "Standard" ? 50 : local.redis_cache_sku == "Premium" ? 200 : null 196 | maxmemory_delta = local.redis_cache_sku == "Basic" ? 2 : local.redis_cache_sku == "Standard" ? 50 : local.redis_cache_sku == "Premium" ? 200 : null 197 | maxfragmentationmemory_reserved = local.redis_cache_sku == "Basic" ? 2 : local.redis_cache_sku == "Standard" ? 50 : local.redis_cache_sku == "Premium" ? 200 : null 198 | maxmemory_policy = "volatile-lru" 199 | } 200 | redis_config = merge(local.redis_config_defaults, var.redis_config) 201 | 202 | # SignalR 203 | enable_signalr = var.enable_signalr 204 | signalr_sku = var.signalr_sku 205 | signalr_service_mode = var.signalr_service_mode 206 | 207 | # Container App 208 | container_app_environment_workload_profile_type = var.container_app_environment_workload_profile_type 209 | container_app_environment_min_host_count = var.container_app_environment_min_host_count 210 | container_app_environment_max_host_count = var.container_app_environment_max_host_count 211 | 212 | container_cpu = var.container_cpu 213 | container_memory = var.container_memory 214 | container_min_replicas = var.container_min_replicas 215 | container_max_replicas = var.container_max_replicas 216 | container_port = var.container_port 217 | container_command = var.container_command 218 | container_environment_variables = var.container_environment_variables 219 | container_secret_environment_variables = var.container_secret_environment_variables 220 | container_fqdn = azurerm_container_app.container_apps["main"].ingress[0].fqdn 221 | container_app_name_override = var.container_app_name_override 222 | container_app_name = local.container_app_name_override == "" ? "${local.resource_prefix}-${local.image_name}" : local.container_app_name_override 223 | container_app_secrets = { for i, v in concat( 224 | [ 225 | { 226 | "name" : "acr-password", 227 | "value" : local.registry_use_managed_identity && !local.registry_admin_enabled ? "not-in-use" : local.registry_password 228 | } 229 | ], 230 | local.enable_app_insights_integration ? [ 231 | { 232 | name = "applicationinsights--connectionstring", 233 | value = azurerm_application_insights.main[0].connection_string 234 | }, 235 | { 236 | name = "applicationinsights--instrumentationkey", 237 | value = azurerm_application_insights.main[0].instrumentation_key 238 | } 239 | ] : [], 240 | local.enable_redis_cache ? [ 241 | { 242 | name = "connectionstrings--redis", 243 | value = azurerm_redis_cache.default[0].primary_connection_string 244 | } 245 | ] : [], 246 | local.enable_app_configuration ? [ 247 | { 248 | name = "connectionstrings--appconfig", 249 | value = azurerm_app_configuration.default[0].endpoint 250 | } 251 | ] : [], 252 | local.enable_signalr ? [ 253 | { 254 | name = "connectionstrings--azuresignalr", 255 | value = azurerm_signalr_service.default[0].primary_connection_string 256 | } 257 | ] : [], 258 | local.container_app_blob_storage_sas_secret, 259 | [for env_name, env_value in nonsensitive(local.container_secret_environment_variables) : { 260 | name = lower(replace(env_name, "_", "-")) 261 | value = sensitive(env_value) 262 | } 263 | ] 264 | ) : v.name => v } 265 | container_app_secrets_in_key_vault = local.escrow_container_app_secrets_in_key_vault ? { for name, secret in local.container_app_secrets : name => { 266 | key_vault_secret_id = azurerm_key_vault_secret.secret_app_setting[name].versionless_id 267 | name = secret["name"] 268 | } } : {} 269 | container_app_env_vars = { for i, v in concat( 270 | local.enable_app_insights_integration ? [ 271 | { 272 | "name" : "ApplicationInsights__ConnectionString", 273 | "secretRef" : "applicationinsights--connectionstring" 274 | }, 275 | { 276 | "name" : "ApplicationInsights__InstrumentationKey", 277 | "secretRef" : "applicationinsights--instrumentationkey" 278 | } 279 | ] : [], 280 | (length(local.container_app_blob_storage_sas_secret) > 0) ? 281 | [ 282 | { 283 | "name" : "ConnectionStrings__BlobStorage", 284 | "secretRef" : "connectionstrings--blobstorage" 285 | } 286 | ] : [], 287 | local.enable_redis_cache ? 288 | [ 289 | { 290 | "name" : "ConnectionStrings__Redis", 291 | "secretRef" : "connectionstrings--redis" 292 | } 293 | ] : [], 294 | local.enable_app_configuration ? [ 295 | { 296 | "name" : "ConnectionStrings__AppConfig", 297 | "secretRef" : "connectionstrings--appconfig" 298 | } 299 | ] : [], 300 | local.enable_signalr ? [ 301 | { 302 | "name" : "ConnectionStrings__AzureSignalR", 303 | "secretRef" : "connectionstrings--azuresignalr" 304 | } 305 | ] : [], 306 | local.enable_container_app_uami ? [ 307 | { 308 | "name" : "AZURE_CLIENT_ID" 309 | "value" : local.container_app_uami.client_id 310 | } 311 | ] : [], 312 | [ 313 | for env_name, env_value in local.container_environment_variables : { 314 | name = env_name 315 | value = env_value 316 | } 317 | ], 318 | [ 319 | for env_name, env_value in nonsensitive(local.container_secret_environment_variables) : { 320 | name = env_name 321 | secretRef = lower(replace(env_name, "_", "-")) 322 | } 323 | ]) : v.name => v } 324 | # Container App / Init Containers 325 | enable_init_container = var.enable_init_container 326 | init_container_image = var.init_container_image 327 | init_container_command = var.init_container_command 328 | 329 | # Container App Environment 330 | existing_container_app_environment = var.existing_container_app_environment 331 | container_app_environment = local.existing_container_app_environment.name == "" ? azurerm_container_app_environment.container_app_env[0] : data.azurerm_container_app_environment.existing_container_app_environment[0] 332 | 333 | # Container App / Identity 334 | enable_container_app_uami = anytrue([ 335 | var.container_app_use_managed_identity, 336 | local.registry_use_managed_identity, 337 | local.enable_app_configuration, 338 | local.key_vault != null, 339 | local.enable_storage_account, 340 | ]) 341 | container_app_uami = local.enable_container_app_uami ? azurerm_user_assigned_identity.containerapp[0] : null 342 | container_app_identity_ids = concat( 343 | var.container_app_identities, local.container_app_uami != null ? [local.container_app_uami.id] : [] 344 | ) 345 | 346 | # Container App / Container image 347 | image_name = var.image_name 348 | image_tag = var.image_tag 349 | # Container App / Liveness Probe 350 | enable_container_health_probe = var.enable_container_health_probe 351 | container_health_probe_interval = var.container_health_probe_interval 352 | container_health_probe_path = var.container_health_probe_path 353 | container_health_probe_protocol = var.container_health_probe_protocol 354 | container_health_tcp_probe = { 355 | interval_seconds = local.container_health_probe_interval 356 | transport = "TCP" 357 | port = local.container_port 358 | } 359 | container_health_http_probe = { 360 | interval_seconds = local.container_health_probe_interval 361 | transport = "HTTP" 362 | port = local.container_port 363 | path = local.container_health_probe_path 364 | } 365 | container_health_https_probe = { 366 | interval_seconds = local.container_health_probe_interval 367 | transport = "HTTPS" 368 | port = local.container_port 369 | path = local.container_health_probe_path 370 | } 371 | container_health_probes = { 372 | "tcp" : local.container_health_tcp_probe 373 | "http" : local.container_health_http_probe 374 | "https" : local.container_health_https_probe 375 | } 376 | container_health_probe = lookup(local.container_health_probes, local.container_health_probe_protocol, null) 377 | # Container App / Scale Rules 378 | container_scale_out_at_defined_time = var.container_scale_out_at_defined_time 379 | container_scale_out_rule_start = var.container_scale_out_rule_start 380 | container_scale_out_rule_end = var.container_scale_out_rule_end 381 | container_scale_http_concurrency = var.container_scale_http_concurrency 382 | # Container App / Sidecar 383 | enable_worker_container = var.enable_worker_container 384 | worker_container_command = var.worker_container_command 385 | worker_container_min_replicas = var.worker_container_min_replicas 386 | worker_container_max_replicas = var.worker_container_max_replicas 387 | # Container app / Custom 388 | custom_container_apps = var.custom_container_apps 389 | custom_container_apps_cdn_frontdoor_custom_domain_dns_names = local.enable_cdn_frontdoor ? { 390 | for name, container in local.custom_container_apps : name => replace(container.ingress.cdn_frontdoor_custom_domain, local.dns_zone_domain_name, "") 391 | if container.ingress.external_enabled && container.ingress.cdn_frontdoor_custom_domain != "" && endswith(container.ingress.cdn_frontdoor_custom_domain, local.dns_zone_domain_name) 392 | } : {} 393 | 394 | # Storage Account 395 | enable_storage_account = local.enable_container_app_blob_storage || local.enable_container_app_file_share 396 | storage_account_ipv4_allow_list = concat( 397 | var.storage_account_ipv4_allow_list, 398 | [azurerm_container_app.container_apps["main"].outbound_ip_addresses[0]] 399 | ) 400 | storage_account_public_access_enabled = var.storage_account_public_access_enabled 401 | storage_account_file_share_quota_gb = var.storage_account_file_share_quota_gb 402 | storage_account_access_key_rotation_reminder_days = var.storage_account_access_key_rotation_reminder_days 403 | # Storage Account / Container 404 | container_app_storage_account_shared_access_key_enabled = var.container_app_storage_account_shared_access_key_enabled 405 | enable_container_app_blob_storage = var.enable_container_app_blob_storage 406 | create_container_app_blob_storage_sas = var.create_container_app_blob_storage_sas 407 | container_app_blob_storage_sas_secret = (local.enable_container_app_blob_storage && local.create_container_app_blob_storage_sas) ? [ 408 | { 409 | name = "connectionstrings--blobstorage", 410 | value = "${azurerm_storage_account.container_app[0].primary_blob_endpoint}${azurerm_storage_container.container_app[0].name}${data.azurerm_storage_account_blob_container_sas.container_app[0].sas}" 411 | } 412 | ] : [] 413 | container_app_blob_storage_public_access_enabled = local.enable_container_app_blob_storage == false ? false : var.container_app_blob_storage_public_access_enabled 414 | container_app_storage_cross_tenant_replication_enabled = var.container_app_storage_cross_tenant_replication_enabled 415 | storage_account_sas_expiration_period = var.storage_account_sas_expiration_period 416 | # Storage Account / File Share 417 | enable_container_app_file_share = var.enable_container_app_file_share 418 | container_app_file_share_mount_path = var.container_app_file_share_mount_path 419 | container_app_file_share_security_profile = var.container_app_file_share_security_profile 420 | # Storage Account / MSSQL Security 421 | mssql_security_storage_shared_access_key_enabled = var.mssql_storage_account_shared_access_key_enabled 422 | mssql_security_storage_access_key_rotation_reminder_days = var.mssql_security_storage_access_key_rotation_reminder_days != 0 ? var.mssql_security_storage_access_key_rotation_reminder_days : local.storage_account_access_key_rotation_reminder_days 423 | mssql_security_storage_cross_tenant_replication_enabled = var.mssql_security_storage_cross_tenant_replication_enabled 424 | 425 | # Azure Functions 426 | linux_function_apps = var.linux_function_apps 427 | linux_function_health_insights_api = (local.enable_app_insights_integration && local.enable_monitoring && var.enable_health_insights_api) ? { 428 | "health-api" = { 429 | runtime = "python" 430 | runtime_version = "3.11" 431 | app_settings = { 432 | "TARGET_LOG_ANALYTICS_RESOURCE_ID" = azurerm_application_insights.main[0].id 433 | } 434 | allowed_origins = var.health_insights_api_cors_origins 435 | ftp_publish_basic_authentication_enabled = false 436 | webdeploy_publish_basic_authentication_enabled = true 437 | ipv4_access = var.health_insights_api_ipv4_allow_list 438 | } 439 | } : {} 440 | enable_linux_function_apps = (length(local.linux_function_apps) > 0 || length(keys(local.linux_function_health_insights_api)) > 0) ? true : false 441 | 442 | # Azure DNS Zone 443 | enable_dns_zone = var.enable_dns_zone 444 | dns_zone_domain_name = var.dns_zone_domain_name 445 | dns_zone_soa_record = var.dns_zone_soa_record 446 | dns_a_records = var.dns_a_records 447 | dns_alias_records = var.dns_alias_records 448 | dns_aaaa_records = var.dns_aaaa_records 449 | dns_caa_records = var.dns_caa_records 450 | dns_cname_records = var.dns_cname_records 451 | dns_mx_records = var.dns_mx_records 452 | dns_ns_records = var.dns_ns_records 453 | dns_ptr_records = var.dns_ptr_records 454 | dns_srv_records = var.dns_srv_records 455 | dns_txt_records = var.dns_txt_records 456 | 457 | # Azure Front Door 458 | enable_cdn_frontdoor = var.enable_cdn_frontdoor 459 | cdn_frontdoor_sku = var.cdn_frontdoor_sku 460 | cdn_frontdoor_response_timeout = var.cdn_frontdoor_response_timeout 461 | cdn_frontdoor_custom_domains = var.cdn_frontdoor_custom_domains 462 | cdn_frontdoor_enable_waf_logs = var.cdn_frontdoor_enable_waf_logs 463 | cdn_frontdoor_enable_access_logs = var.cdn_frontdoor_enable_access_logs 464 | cdn_frontdoor_enable_health_probe_logs = var.cdn_frontdoor_enable_health_probe_logs 465 | cdn_frontdoor_custom_domain_dns_names = local.enable_cdn_frontdoor && local.enable_dns_zone ? toset([ 466 | for domain in local.cdn_frontdoor_custom_domains : replace(domain, local.dns_zone_domain_name, "") if endswith(domain, local.dns_zone_domain_name) 467 | ]) : [] 468 | cdn_frontdoor_custom_domains_create_dns_records = var.cdn_frontdoor_custom_domains_create_dns_records 469 | cdn_frontdoor_origin_fqdn_override = var.cdn_frontdoor_origin_fqdn_override != "" ? var.cdn_frontdoor_origin_fqdn_override : local.container_fqdn 470 | cdn_frontdoor_origin_host_header_override = var.cdn_frontdoor_origin_host_header_override != "" ? var.cdn_frontdoor_origin_host_header_override : null 471 | cdn_frontdoor_origin_http_port = var.cdn_frontdoor_origin_http_port 472 | cdn_frontdoor_origin_https_port = var.cdn_frontdoor_origin_https_port 473 | cdn_frontdoor_forwarding_protocol = var.cdn_frontdoor_forwarding_protocol 474 | enable_cdn_frontdoor_health_probe = var.enable_cdn_frontdoor_health_probe 475 | cdn_frontdoor_health_probe_protocol = var.cdn_frontdoor_health_probe_protocol 476 | cdn_frontdoor_health_probe_interval = var.cdn_frontdoor_health_probe_interval 477 | cdn_frontdoor_health_probe_path = var.cdn_frontdoor_health_probe_path 478 | cdn_frontdoor_health_probe_request_type = var.cdn_frontdoor_health_probe_request_type 479 | restrict_container_apps_to_cdn_inbound_only = var.restrict_container_apps_to_cdn_inbound_only 480 | restrict_container_apps_to_agw_inbound_only = var.restrict_container_apps_to_agw_inbound_only 481 | container_apps_allow_agw_resource = var.container_apps_allow_agw_resource 482 | container_apps_allow_agw_pip_resource_id = length(data.azurerm_application_gateway.existing_agw) > 0 ? split("/", data.azurerm_application_gateway.existing_agw[0].frontend_ip_configuration[0].public_ip_address_id) : null 483 | container_apps_allow_agw_ip = length(data.azurerm_application_gateway.existing_agw) > 0 ? data.azurerm_public_ip.existing_agw_ip[0].ip_address : "" 484 | container_apps_allow_ips_inbound = var.container_apps_allow_ips_inbound 485 | cdn_frontdoor_host_redirects = var.cdn_frontdoor_host_redirects 486 | cdn_frontdoor_host_add_response_headers = var.cdn_frontdoor_host_add_response_headers 487 | cdn_frontdoor_remove_response_headers = var.cdn_frontdoor_remove_response_headers 488 | ruleset_redirects_id = length(local.cdn_frontdoor_host_redirects) > 0 ? [azurerm_cdn_frontdoor_rule_set.redirects[0].id] : [] 489 | ruleset_add_response_headers_id = length(local.cdn_frontdoor_host_add_response_headers) > 0 ? [azurerm_cdn_frontdoor_rule_set.add_response_headers[0].id] : [] 490 | ruleset_remove_response_headers_id = length(local.cdn_frontdoor_remove_response_headers) > 0 ? [azurerm_cdn_frontdoor_rule_set.remove_response_headers[0].id] : [] 491 | ruleset_vdp_id = local.enable_cdn_frontdoor_vdp_redirects ? [azurerm_cdn_frontdoor_rule_set.vdp[0].id] : [] 492 | ruleset_ids = concat( 493 | local.ruleset_redirects_id, 494 | local.ruleset_add_response_headers_id, 495 | local.ruleset_remove_response_headers_id, 496 | local.ruleset_vdp_id 497 | ) 498 | cdn_frontdoor_enable_rate_limiting = var.cdn_frontdoor_enable_rate_limiting 499 | cdn_frontdoor_rate_limiting_duration_in_minutes = var.cdn_frontdoor_rate_limiting_duration_in_minutes 500 | cdn_frontdoor_rate_limiting_threshold = var.cdn_frontdoor_rate_limiting_threshold 501 | cdn_frontdoor_enable_waf = local.enable_cdn_frontdoor && local.cdn_frontdoor_enable_rate_limiting 502 | cdn_frontdoor_waf_mode = var.cdn_frontdoor_waf_mode 503 | cdn_frontdoor_waf_custom_rules = var.cdn_frontdoor_waf_custom_rules 504 | cdn_frontdoor_waf_managed_rulesets = var.cdn_frontdoor_waf_managed_rulesets 505 | cdn_frontdoor_rate_limiting_bypass_ip_list = var.cdn_frontdoor_rate_limiting_bypass_ip_list 506 | enable_cdn_frontdoor_vdp_redirects = var.enable_cdn_frontdoor_vdp_redirects 507 | cdn_frontdoor_vdp_destination_hostname = var.cdn_frontdoor_vdp_destination_hostname 508 | 509 | # Event Hub 510 | enable_event_hub = var.enable_event_hub 511 | enable_logstash_consumer = var.enable_logstash_consumer 512 | eventhub_export_log_analytics_table_names = var.eventhub_export_log_analytics_table_names 513 | 514 | # Application Insights 515 | enable_app_insights_integration = var.enable_app_insights_integration 516 | app_insights_retention_days = var.app_insights_retention_days 517 | app_insights_smart_detection_enabled = var.app_insights_smart_detection_enabled 518 | 519 | # Azure Monitor 520 | enable_monitoring = var.enable_monitoring 521 | # Azure Monitor / Logic App Workflow 522 | existing_logic_app_workflow = var.existing_logic_app_workflow 523 | logic_app_workflow_name = local.existing_logic_app_workflow.name == "" ? "" : data.azurerm_logic_app_workflow.existing_logic_app_workflow[0].name 524 | logic_app_workflow_id = local.existing_logic_app_workflow.name == "" ? "" : data.azurerm_logic_app_workflow.existing_logic_app_workflow[0].id 525 | logic_app_workflow_callback_url = local.existing_logic_app_workflow.name == "" ? "" : data.azapi_resource_action.existing_logic_app_workflow_callback_url[0].output.value 526 | monitor_email_receivers = var.monitor_email_receivers 527 | monitor_endpoint_healthcheck = var.monitor_endpoint_healthcheck 528 | monitor_http_availability_fqdn = var.monitor_http_availability_fqdn == "" ? local.enable_cdn_frontdoor ? ( 529 | length(local.cdn_frontdoor_custom_domains) >= 1 ? local.cdn_frontdoor_custom_domains[0] : azurerm_cdn_frontdoor_endpoint.endpoint[0].host_name 530 | ) : local.container_fqdn : var.monitor_http_availability_fqdn 531 | monitor_http_availability_url = "https://${local.monitor_http_availability_fqdn}${local.monitor_endpoint_healthcheck}" 532 | monitor_http_availability_verb = var.monitor_http_availability_verb 533 | monitor_default_container = { "default" = azurerm_container_app.container_apps["main"] } 534 | monitor_worker_container = local.enable_worker_container ? { "worker" = azurerm_container_app.container_apps["worker"] } : {} 535 | monitor_containers = merge( 536 | local.monitor_default_container, 537 | local.monitor_worker_container, 538 | { 539 | for name, container in local.custom_container_apps : name => azurerm_container_app.custom_container_apps[name] 540 | } 541 | ) 542 | monitor_logic_app_receiver = { 543 | name = local.logic_app_workflow_name 544 | resource_id = local.logic_app_workflow_id 545 | callback_url = local.logic_app_workflow_callback_url 546 | } 547 | enable_monitoring_traces = var.enable_monitoring_traces 548 | enable_monitoring_traces_include_warnings = var.enable_monitoring_traces_include_warnings 549 | 550 | # Azure Monitor / Alarm thresholds 551 | alarm_cpu_threshold_percentage = var.alarm_cpu_threshold_percentage 552 | alarm_memory_threshold_percentage = var.alarm_memory_threshold_percentage 553 | alarm_latency_threshold_ms = var.alarm_latency_threshold_ms 554 | alarm_log_ingestion_gb_per_day = var.alarm_log_ingestion_gb_per_day 555 | alarm_for_delete_events = var.alarm_for_delete_events 556 | 557 | # Network Watcher 558 | enable_network_watcher = var.enable_network_watcher 559 | existing_network_watcher_name = var.existing_network_watcher_name 560 | existing_network_watcher_resource_group_name = var.existing_network_watcher_resource_group_name 561 | network_watcher_name = local.enable_network_watcher ? azurerm_network_watcher.default[0].name : local.existing_network_watcher_name 562 | network_watcher_resource_group_name = local.network_watcher_name != "" ? local.existing_network_watcher_resource_group_name : local.resource_group.name 563 | network_watcher_flow_log_retention = var.network_watcher_flow_log_retention 564 | enable_network_watcher_traffic_analytics = var.enable_network_watcher_traffic_analytics 565 | network_watcher_traffic_analytics_interval = var.network_watcher_traffic_analytics_interval 566 | network_watcher_nsg_storage_access_key_rotation_reminder_days = var.network_watcher_nsg_storage_access_key_rotation_reminder_days != 0 ? var.network_watcher_nsg_storage_access_key_rotation_reminder_days : local.storage_account_access_key_rotation_reminder_days 567 | 568 | # App Configuration 569 | enable_app_configuration = var.enable_app_configuration 570 | app_configuration_sku = var.app_configuration_sku 571 | app_configuration_assign_role = var.app_configuration_assign_role 572 | } 573 | --------------------------------------------------------------------------------