From bd34ceb3627dab9b0635e797abf9135f8e6eaf1c Mon Sep 17 00:00:00 2001 From: Michal Pipal Date: Mon, 10 Feb 2025 00:31:58 +0100 Subject: [PATCH 1/5] feat: Added Standard Logic App with Managed Identity and IP restriction (for HTTP trigger) --- .../main.tf | 297 ++++++++++++++++++ .../outputs.tf | 11 + .../variables.tf | 159 ++++++++++ 3 files changed, 467 insertions(+) create mode 100644 modules/azure/logic_app_standard_http_managed_identity/main.tf create mode 100644 modules/azure/logic_app_standard_http_managed_identity/outputs.tf create mode 100644 modules/azure/logic_app_standard_http_managed_identity/variables.tf diff --git a/modules/azure/logic_app_standard_http_managed_identity/main.tf b/modules/azure/logic_app_standard_http_managed_identity/main.tf new file mode 100644 index 00000000..e56e9f42 --- /dev/null +++ b/modules/azure/logic_app_standard_http_managed_identity/main.tf @@ -0,0 +1,297 @@ +terraform { + required_version = "~> 1.3" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.48" + } + archive = { + source = "hashicorp/archive" + version = "~> 2.3" + } + azapi = { + source = "Azure/azapi" + version = "~> 1.4" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 2.36" + } + } + + backend "azurerm" {} +} + +provider "azurerm" { + features {} +} + +provider "archive" { +} + +locals { + identity_type = var.use_managed_identity && length(var.identity_ids) > 0 ? "SystemAssigned, UserAssigned" : var.use_managed_identity ? "SystemAssigned" : length(var.identity_ids) > 0 ? "UserAssigned" : null + is_linux = length(regexall("/home/", lower(abspath(path.root)))) > 0 + identifiers = concat(["api://${var.managed_identity_provider.create.application_name}"], var.managed_identity_provider.identifier_uris != null ? var.managed_identity_provider.identifier_uris : []) + allowed_audiences = concat(local.identifiers, var.managed_identity_provider.allowed_audiences != null ? var.managed_identity_provider.allowed_audiences : []) +} + +resource "azurerm_logic_app_standard" "app" { + name = var.logic_app_name + location = var.location + resource_group_name = var.resource_group_name + enabled = var.enabled + https_only = var.https_only + version = var.logic_app_version + + dynamic "identity" { + for_each = local.identity_type != null ? [1] : [] + content { + type = local.identity_type + identity_ids = var.identity_ids + } + } + + site_config { + ftps_state = "Disabled" + elastic_instance_minimum = var.elastic_instance_minimum + pre_warmed_instance_count = var.pre_warmed_instance_count + + dynamic "ip_restriction" { + for_each = var.ip_restrictions + + content { + ip_address = ip_restriction.value.ip_address + service_tag = ip_restriction.value.service_tag + virtual_network_subnet_id = ip_restriction.value.virtual_network_subnet_id + name = ip_restriction.value.name + priority = ip_restriction.value.priority + action = ip_restriction.value.action + + dynamic "headers" { + for_each = ip_restriction.value.headers + + content { + x_azure_fdid = headers.value.x_azure_fdid + x_fd_health_probe = headers.value.x_fd_health_probe + x_forwarded_for = headers.value.x_forwarded_for + x_forwarded_host = headers.value.x_forwarded_host + } + } + } + } + } + + app_settings = merge({ + WEBSITE_NODE_DEFAULT_VERSION = "~18", + FUNCTIONS_WORKER_RUNTIME = "node", + MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = "${var.managed_identity_provider != null ? azuread_application_password.password[0].value : ""}" + }, var.app_settings) + + app_service_plan_id = var.service_plan_id + storage_account_access_key = var.storage_account_access_key + storage_account_name = var.storage_account_name + virtual_network_subnet_id = var.integration_subnet_id +} + +# Safest way is to always zip the file, even if there are no changes, it ensures that portal changes do not affect deployment results +resource "null_resource" "zip_logic_app" { + triggers = { + always_run = timestamp() + } + # if check.zip file changes, create deploy.zip file + provisioner "local-exec" { + interpreter = local.is_linux ? ["bash", "-c"] : ["PowerShell", "-Command"] + command = local.is_linux ? "cd ${path.module} && mkdir -p files && cd ${var.workflows_source_path} && zip -rq $OLDPWD/files/deploy.zip ." : "New-Item -Path \"${path.module}\" -Name \"files\" -ItemType \"directory\" -Force; Compress-Archive -Path \"${var.workflows_source_path}\\*\" -DestinationPath \"${path.module}\\files\\deploy.zip\" -Force" + } +} + +# After the logic app is created, start a deployment using the Azure CLI +# It is not possible to use a ZIP-deployment from blob storage, as it can not be updated from the portal + +# When you add parameters to your logic app using the parameters.json file, and you reference an app setting +# the file will not be accepted if the app setting does not exist. However, there is a small delay between +# updating the logic app and the app settings being available. Therefore, we need to add a timeout to the +# deployment to make sure the app settings are available before the deployment is started. +resource "time_sleep" "wait_for_app_settings" { + depends_on = [ + azurerm_logic_app_standard.app, + null_resource.zip_logic_app + ] + create_duration = "${var.deployment_wait_timeout}s" +} + +# The first step is to ensure that the logic apps extension is installed +resource "null_resource" "install-extension" { + depends_on = [time_sleep.wait_for_app_settings] + + provisioner "local-exec" { + command = "az extension add --name logic" + } +} + +# Fetch the subscription name +data "azurerm_subscription" "current" {} + +# Then use the Azure CLI to start the deployment +resource "null_resource" "deploy" { + depends_on = [ + null_resource.install-extension, + null_resource.zip_logic_app + ] + + triggers = { + always_run = timestamp() # null_resource.zip_logic_app might not always actually change, trigger ensures the execution anyway + } + + provisioner "local-exec" { + command = "az logicapp deployment source config-zip --name ${var.logic_app_name} --resource-group ${var.resource_group_name} --subscription ${data.azurerm_subscription.current.display_name} --src ${path.module}/files/deploy.zip" + } +} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories" { + count = var.log_analytics_workspace_id == null ? 0 : 1 + resource_id = azurerm_logic_app_standard.app.id +} + +// Write logs and metrics to log analytics if specified +// Needs to be done once the deployment is finished, because updating Diagnostic Settings leads to a restart of the Logic App +// which causes the deployment to fail if it is not finished yet +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting" { + depends_on = [ + null_resource.deploy + ] + + count = var.log_analytics_workspace_id == null ? 0 : 1 + name = "diag-${var.logic_app_name}" + target_resource_id = azurerm_logic_app_standard.app.id + log_analytics_workspace_id = var.log_analytics_workspace_id + + dynamic "enabled_log" { + for_each = length(var.log_analytics_diagnostic_categories) > 0 ? var.log_analytics_diagnostic_categories : data.azurerm_monitor_diagnostic_categories.diagnostic_categories[0].log_category_types + + content { + category = enabled_log.value + + retention_policy { + enabled = false + } + } + } + + dynamic "metric" { + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories[0].metrics + + content { + category = metric.value + enabled = true + + retention_policy { + enabled = false + } + } + } +} + +# Managed Identity Provider +data "azuread_client_config" "current" {} + +resource "random_uuid" "oath2_uuid" {} + +resource "azuread_application" "application" { + count = var.managed_identity_provider != null ? 1 : 0 + display_name = var.managed_identity_provider.create.display_name + owners = var.managed_identity_provider.create.owners != null ? concat([data.azuread_client_config.current.object_id], var.managed_identity_provider.create.owners) : [data.azuread_client_config.current.object_id] + sign_in_audience = "AzureADMyOrg" + identifier_uris = local.identifiers + + api { + requested_access_token_version = 2 + + oauth2_permission_scope { + admin_consent_description = var.managed_identity_provider.create.oauth2_settings.admin_consent_description + admin_consent_display_name = var.managed_identity_provider.create.oauth2_settings.admin_consent_display_name + enabled = var.managed_identity_provider.create.oauth2_settings.enabled + id = random_uuid.oath2_uuid.result + type = var.managed_identity_provider.create.oauth2_settings.type + user_consent_description = var.managed_identity_provider.create.oauth2_settings.user_consent_description + user_consent_display_name = var.managed_identity_provider.create.oauth2_settings.user_consent_display_name + value = var.managed_identity_provider.create.oauth2_settings.role_value + } + } + + web { + redirect_uris = ["https://${var.logic_app_name}.azurewebsites.net/.auth/login/aad/callback"] + + implicit_grant { + access_token_issuance_enabled = false + id_token_issuance_enabled = true + } + } + + required_resource_access { + resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph + + resource_access { + id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" # User.Read + type = "Scope" + } + } +} + +resource "null_resource" "always_run" { + triggers = { + timestamp = "${timestamp()}" + } +} + +resource "azapi_update_resource" "setup_auth_settings" { + count = var.managed_identity_provider != null ? 1 : 0 + type = "Microsoft.Web/sites/config@2020-12-01" + resource_id = "${azurerm_logic_app_standard.app.id}/config/web" + + depends_on = [ + azurerm_logic_app_standard.app, + null_resource.always_run + ] + + body = jsonencode({ + properties = { + siteAuthSettingsV2 = { + globalValidation = { + excludedPaths = [] + require_authentication = true, + // Even though is looks weird, it is needed. Otherwise, the app and also the designer in Azure Portal are not working + // https://techcommunity.microsoft.com/blog/integrationsonazureblog/trigger-workflows-in-standard-logic-apps-with-easy-auth/3207378 + unauthenticatedClientAction = "AllowAnonymous" + }, + IdentityProviders = { + azureActiveDirectory = { + enabled = true, + registration = { + clientId = azuread_application.application[0].application_id + clientSecretSettingName = "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" + }, + validation = { + allowedAudiences = local.allowed_audiences + } + } + } + } + } + }) + lifecycle { + /* This action should always be replaces since is works under the hood as an api call + * So it does not really track issues with the function app properly + */ + replace_triggered_by = [ + null_resource.always_run + ] + } +} + +resource "azuread_application_password" "password" { + count = var.managed_identity_provider != null ? 1 : 0 + application_object_id = azuread_application.application[0].object_id +} diff --git a/modules/azure/logic_app_standard_http_managed_identity/outputs.tf b/modules/azure/logic_app_standard_http_managed_identity/outputs.tf new file mode 100644 index 00000000..b1a36663 --- /dev/null +++ b/modules/azure/logic_app_standard_http_managed_identity/outputs.tf @@ -0,0 +1,11 @@ +output "principal_id" { + value = length(azurerm_logic_app_standard.app.identity) > 0 ? azurerm_logic_app_standard.app.identity[0].principal_id : null +} + +output "name" { + value = azurerm_logic_app_standard.app.name +} + +output "default_hostname" { + value = azurerm_logic_app_standard.app.default_hostname +} \ No newline at end of file diff --git a/modules/azure/logic_app_standard_http_managed_identity/variables.tf b/modules/azure/logic_app_standard_http_managed_identity/variables.tf new file mode 100644 index 00000000..c096e401 --- /dev/null +++ b/modules/azure/logic_app_standard_http_managed_identity/variables.tf @@ -0,0 +1,159 @@ +variable "location" { + type = string + description = "A datacenter location in Azure." +} + +variable "resource_group_name" { + type = string + description = "Name of the resource group." +} + +variable "logic_app_name" { + type = string + description = "Specifies the name of the logic app." +} + +variable "service_plan_id" { + type = string + description = "The ID of the Service Plan to use for this Logic App." +} + +variable "storage_account_name" { + type = string + description = "The name of the storage account to connect to the logic app" +} + +variable "storage_account_access_key" { + type = string + description = "The access key of the storage account to connect to the logic app." +} + +variable "enabled" { + type = bool + description = "If this workflow should be enabled by default or not, defaults to true" + default = true +} + +variable "use_managed_identity" { + type = bool + description = "Use System Assigned Managed Identity for this logic app" + default = false +} + +variable "identity_ids" { + type = list(string) + description = "User Assigned Managed Identity ids for this logic app" + default = [] +} + +variable "app_settings" { + type = map(string) + description = "A map of key/value pairs to be used as application settings for the logic app." + default = {} +} + +variable "workflows_source_path" { + type = string + description = "The path to the source code of all workflows." +} + +variable "integration_subnet_id" { + type = string + description = "The ID of the integration subnet to enable virtual network integration." + default = null +} + +variable "elastic_instance_minimum" { + type = number + description = "Minimum amount of elastic instances." + default = 1 +} + +variable "pre_warmed_instance_count" { + type = number + description = "Amount of pre-warmed instances. Requires at least 1 for VNet-integration." + default = 0 +} + +variable "deployment_wait_timeout" { + type = number + description = "The amount of time to wait for the deployment to start after the logic app was deployed." + default = 30 +} + +variable "https_only" { + type = bool + description = "Allow only HTTPS access." + default = false +} + +variable "logic_app_version" { + type = string + description = "The runtime version associated with the Logic App." + default = "~4" +} + +variable "log_analytics_workspace_id" { + type = string + description = "Specifies the ID of a Log Analytics Workspace where diagnostics data should be sent." + default = null +} + +variable "log_analytics_diagnostic_categories" { + type = list(string) + description = "Optional list of diagnostic categories to override the default categories." + default = [] +} + +variable "managed_identity_provider" { + type = object({ + existing = optional(object({ + client_id = string + client_secret = string + })) + create = optional(object({ + application_name = string + display_name = string + oauth2_settings = object({ + admin_consent_description = string + admin_consent_display_name = string + enabled = bool + type = string + user_consent_description = string + user_consent_display_name = string + role_value = string + }) + owners = optional(list(string)) # Deployment user will be added as owner by default + redirect_uris = optional(list(string)) # Only for additional URIs, function uri will be added by default + group_id = optional(string) # Group ID where service principal of the existing application will belong to + })) + identifier_uris = optional(list(string)) # api:// will be added by default if application is create + allowed_audiences = optional(list(string)) # api:// will be added by default + }) + validation { + condition = var.managed_identity_provider.existing != null || var.managed_identity_provider.create != null + error_message = "Variable managed_identity_provider has to provide either an existing managed identity provider or given information to create one" + } + description = "The managed identity provider to use for connections on this function app" + default = null +} + +variable "ip_restrictions" { + type = list(object({ + ip_address = optional(string), + service_tag = optional(string), + virtual_network_subnet_id = optional(string), + name = optional(string), + priority = optional(number), + action = optional(string), + + headers = optional(list(object({ + x_azure_fdid = optional(list(string)), + x_fd_health_probe = optional(list(string)), + x_forwarded_for = optional(list(string)), + x_forwarded_host = optional(list(string)) + }))) + })) + description = "A List of objects representing IP restrictions." + default = [] +} From 531c54834ec21fb78ec5ca75ad1df85635ede96b Mon Sep 17 00:00:00 2001 From: Michal Pipal Date: Mon, 10 Feb 2025 12:35:04 +0100 Subject: [PATCH 2/5] Merged Standard Logic App modules into a single one --- modules/azure/logic_app_standard/main.tf | 150 ++++++++- modules/azure/logic_app_standard/variables.tf | 53 ++++ .../main.tf | 297 ------------------ .../outputs.tf | 11 - .../variables.tf | 159 ---------- 5 files changed, 193 insertions(+), 477 deletions(-) delete mode 100644 modules/azure/logic_app_standard_http_managed_identity/main.tf delete mode 100644 modules/azure/logic_app_standard_http_managed_identity/outputs.tf delete mode 100644 modules/azure/logic_app_standard_http_managed_identity/variables.tf diff --git a/modules/azure/logic_app_standard/main.tf b/modules/azure/logic_app_standard/main.tf index 0b8c7e18..985a0711 100644 --- a/modules/azure/logic_app_standard/main.tf +++ b/modules/azure/logic_app_standard/main.tf @@ -6,9 +6,13 @@ terraform { source = "hashicorp/azurerm" version = "~> 3.48" } - archive = { - source = "hashicorp/archive" - version = "~> 2.3" + azapi = { + source = "Azure/azapi" + version = "~> 1.4" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 2.36" } } @@ -19,12 +23,11 @@ provider "azurerm" { features {} } -provider "archive" { -} - locals { - identity_type = var.use_managed_identity && length(var.identity_ids) > 0 ? "SystemAssigned, UserAssigned" : var.use_managed_identity ? "SystemAssigned" : length(var.identity_ids) > 0 ? "UserAssigned" : null - is_linux = length(regexall("/home/", lower(abspath(path.root)))) > 0 + identity_type = var.use_managed_identity && length(var.identity_ids) > 0 ? "SystemAssigned, UserAssigned" : var.use_managed_identity ? "SystemAssigned" : length(var.identity_ids) > 0 ? "UserAssigned" : null + is_linux = length(regexall("/home/", lower(abspath(path.root)))) > 0 + identifiers = var.managed_identity_provider != null ? concat(["api://${var.managed_identity_provider.create.application_name}"], var.managed_identity_provider.identifier_uris != null ? var.managed_identity_provider.identifier_uris : []) : [] + allowed_audiences = var.managed_identity_provider != null ? concat(local.identifiers, var.managed_identity_provider.allowed_audiences != null ? var.managed_identity_provider.allowed_audiences : []) : [] } resource "azurerm_logic_app_standard" "app" { @@ -47,11 +50,36 @@ resource "azurerm_logic_app_standard" "app" { ftps_state = "Disabled" elastic_instance_minimum = var.elastic_instance_minimum pre_warmed_instance_count = var.pre_warmed_instance_count + + dynamic "ip_restriction" { + for_each = var.ip_restrictions + + content { + ip_address = ip_restriction.value.ip_address + service_tag = ip_restriction.value.service_tag + virtual_network_subnet_id = ip_restriction.value.virtual_network_subnet_id + name = ip_restriction.value.name + priority = ip_restriction.value.priority + action = ip_restriction.value.action + + dynamic "headers" { + for_each = ip_restriction.value.headers + + content { + x_azure_fdid = headers.value.x_azure_fdid + x_fd_health_probe = headers.value.x_fd_health_probe + x_forwarded_for = headers.value.x_forwarded_for + x_forwarded_host = headers.value.x_forwarded_host + } + } + } + } } app_settings = merge({ - WEBSITE_NODE_DEFAULT_VERSION = "~18", - FUNCTIONS_WORKER_RUNTIME = "node", + WEBSITE_NODE_DEFAULT_VERSION = "~18", + FUNCTIONS_WORKER_RUNTIME = "node", + MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = "${var.managed_identity_provider != null ? azuread_application_password.password[0].value : ""}" }, var.app_settings) app_service_plan_id = var.service_plan_id @@ -158,3 +186,105 @@ resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting" { } } } + +# Managed Identity Provider +data "azuread_client_config" "current" {} + +resource "random_uuid" "oath2_uuid" {} + +resource "azuread_application" "application" { + count = var.managed_identity_provider != null ? 1 : 0 + display_name = var.managed_identity_provider.create.display_name + owners = var.managed_identity_provider.create.owners != null ? concat([data.azuread_client_config.current.object_id], var.managed_identity_provider.create.owners) : [data.azuread_client_config.current.object_id] + sign_in_audience = "AzureADMyOrg" + identifier_uris = local.identifiers + + api { + requested_access_token_version = 2 + + oauth2_permission_scope { + admin_consent_description = var.managed_identity_provider.create.oauth2_settings.admin_consent_description + admin_consent_display_name = var.managed_identity_provider.create.oauth2_settings.admin_consent_display_name + enabled = var.managed_identity_provider.create.oauth2_settings.enabled + id = random_uuid.oath2_uuid.result + type = var.managed_identity_provider.create.oauth2_settings.type + user_consent_description = var.managed_identity_provider.create.oauth2_settings.user_consent_description + user_consent_display_name = var.managed_identity_provider.create.oauth2_settings.user_consent_display_name + value = var.managed_identity_provider.create.oauth2_settings.role_value + } + } + + web { + redirect_uris = ["https://${var.logic_app_name}.azurewebsites.net/.auth/login/aad/callback"] + + implicit_grant { + access_token_issuance_enabled = false + id_token_issuance_enabled = true + } + } + + required_resource_access { + resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph + + resource_access { + id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" # User.Read + type = "Scope" + } + } +} + +resource "null_resource" "always_run" { + triggers = { + timestamp = "${timestamp()}" + } +} + +resource "azapi_update_resource" "setup_auth_settings" { + count = var.managed_identity_provider != null ? 1 : 0 + type = "Microsoft.Web/sites/config@2020-12-01" + resource_id = "${azurerm_logic_app_standard.app.id}/config/web" + + depends_on = [ + azurerm_logic_app_standard.app, + null_resource.always_run + ] + + body = jsonencode({ + properties = { + siteAuthSettingsV2 = { + globalValidation = { + excludedPaths = [] + require_authentication = true, + // Even though is looks weird, it is needed. Otherwise, the app and also the designer in Azure Portal are not working + // https://techcommunity.microsoft.com/blog/integrationsonazureblog/trigger-workflows-in-standard-logic-apps-with-easy-auth/3207378 + unauthenticatedClientAction = "AllowAnonymous" + }, + IdentityProviders = { + azureActiveDirectory = { + enabled = true, + registration = { + clientId = azuread_application.application[0].application_id + clientSecretSettingName = "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" + }, + validation = { + allowedAudiences = local.allowed_audiences + } + } + } + } + } + }) + lifecycle { + /* This action should always be replaces since is works under the hood as an api call + * So it does not really track issues with the function app properly + */ + replace_triggered_by = [ + null_resource.always_run + ] + } +} + +resource "azuread_application_password" "password" { + count = var.managed_identity_provider != null ? 1 : 0 + application_object_id = azuread_application.application[0].object_id +} diff --git a/modules/azure/logic_app_standard/variables.tf b/modules/azure/logic_app_standard/variables.tf index 7853ec4f..c096e401 100644 --- a/modules/azure/logic_app_standard/variables.tf +++ b/modules/azure/logic_app_standard/variables.tf @@ -104,3 +104,56 @@ variable "log_analytics_diagnostic_categories" { description = "Optional list of diagnostic categories to override the default categories." default = [] } + +variable "managed_identity_provider" { + type = object({ + existing = optional(object({ + client_id = string + client_secret = string + })) + create = optional(object({ + application_name = string + display_name = string + oauth2_settings = object({ + admin_consent_description = string + admin_consent_display_name = string + enabled = bool + type = string + user_consent_description = string + user_consent_display_name = string + role_value = string + }) + owners = optional(list(string)) # Deployment user will be added as owner by default + redirect_uris = optional(list(string)) # Only for additional URIs, function uri will be added by default + group_id = optional(string) # Group ID where service principal of the existing application will belong to + })) + identifier_uris = optional(list(string)) # api:// will be added by default if application is create + allowed_audiences = optional(list(string)) # api:// will be added by default + }) + validation { + condition = var.managed_identity_provider.existing != null || var.managed_identity_provider.create != null + error_message = "Variable managed_identity_provider has to provide either an existing managed identity provider or given information to create one" + } + description = "The managed identity provider to use for connections on this function app" + default = null +} + +variable "ip_restrictions" { + type = list(object({ + ip_address = optional(string), + service_tag = optional(string), + virtual_network_subnet_id = optional(string), + name = optional(string), + priority = optional(number), + action = optional(string), + + headers = optional(list(object({ + x_azure_fdid = optional(list(string)), + x_fd_health_probe = optional(list(string)), + x_forwarded_for = optional(list(string)), + x_forwarded_host = optional(list(string)) + }))) + })) + description = "A List of objects representing IP restrictions." + default = [] +} diff --git a/modules/azure/logic_app_standard_http_managed_identity/main.tf b/modules/azure/logic_app_standard_http_managed_identity/main.tf deleted file mode 100644 index e56e9f42..00000000 --- a/modules/azure/logic_app_standard_http_managed_identity/main.tf +++ /dev/null @@ -1,297 +0,0 @@ -terraform { - required_version = "~> 1.3" - - required_providers { - azurerm = { - source = "hashicorp/azurerm" - version = "~> 3.48" - } - archive = { - source = "hashicorp/archive" - version = "~> 2.3" - } - azapi = { - source = "Azure/azapi" - version = "~> 1.4" - } - azuread = { - source = "hashicorp/azuread" - version = "~> 2.36" - } - } - - backend "azurerm" {} -} - -provider "azurerm" { - features {} -} - -provider "archive" { -} - -locals { - identity_type = var.use_managed_identity && length(var.identity_ids) > 0 ? "SystemAssigned, UserAssigned" : var.use_managed_identity ? "SystemAssigned" : length(var.identity_ids) > 0 ? "UserAssigned" : null - is_linux = length(regexall("/home/", lower(abspath(path.root)))) > 0 - identifiers = concat(["api://${var.managed_identity_provider.create.application_name}"], var.managed_identity_provider.identifier_uris != null ? var.managed_identity_provider.identifier_uris : []) - allowed_audiences = concat(local.identifiers, var.managed_identity_provider.allowed_audiences != null ? var.managed_identity_provider.allowed_audiences : []) -} - -resource "azurerm_logic_app_standard" "app" { - name = var.logic_app_name - location = var.location - resource_group_name = var.resource_group_name - enabled = var.enabled - https_only = var.https_only - version = var.logic_app_version - - dynamic "identity" { - for_each = local.identity_type != null ? [1] : [] - content { - type = local.identity_type - identity_ids = var.identity_ids - } - } - - site_config { - ftps_state = "Disabled" - elastic_instance_minimum = var.elastic_instance_minimum - pre_warmed_instance_count = var.pre_warmed_instance_count - - dynamic "ip_restriction" { - for_each = var.ip_restrictions - - content { - ip_address = ip_restriction.value.ip_address - service_tag = ip_restriction.value.service_tag - virtual_network_subnet_id = ip_restriction.value.virtual_network_subnet_id - name = ip_restriction.value.name - priority = ip_restriction.value.priority - action = ip_restriction.value.action - - dynamic "headers" { - for_each = ip_restriction.value.headers - - content { - x_azure_fdid = headers.value.x_azure_fdid - x_fd_health_probe = headers.value.x_fd_health_probe - x_forwarded_for = headers.value.x_forwarded_for - x_forwarded_host = headers.value.x_forwarded_host - } - } - } - } - } - - app_settings = merge({ - WEBSITE_NODE_DEFAULT_VERSION = "~18", - FUNCTIONS_WORKER_RUNTIME = "node", - MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = "${var.managed_identity_provider != null ? azuread_application_password.password[0].value : ""}" - }, var.app_settings) - - app_service_plan_id = var.service_plan_id - storage_account_access_key = var.storage_account_access_key - storage_account_name = var.storage_account_name - virtual_network_subnet_id = var.integration_subnet_id -} - -# Safest way is to always zip the file, even if there are no changes, it ensures that portal changes do not affect deployment results -resource "null_resource" "zip_logic_app" { - triggers = { - always_run = timestamp() - } - # if check.zip file changes, create deploy.zip file - provisioner "local-exec" { - interpreter = local.is_linux ? ["bash", "-c"] : ["PowerShell", "-Command"] - command = local.is_linux ? "cd ${path.module} && mkdir -p files && cd ${var.workflows_source_path} && zip -rq $OLDPWD/files/deploy.zip ." : "New-Item -Path \"${path.module}\" -Name \"files\" -ItemType \"directory\" -Force; Compress-Archive -Path \"${var.workflows_source_path}\\*\" -DestinationPath \"${path.module}\\files\\deploy.zip\" -Force" - } -} - -# After the logic app is created, start a deployment using the Azure CLI -# It is not possible to use a ZIP-deployment from blob storage, as it can not be updated from the portal - -# When you add parameters to your logic app using the parameters.json file, and you reference an app setting -# the file will not be accepted if the app setting does not exist. However, there is a small delay between -# updating the logic app and the app settings being available. Therefore, we need to add a timeout to the -# deployment to make sure the app settings are available before the deployment is started. -resource "time_sleep" "wait_for_app_settings" { - depends_on = [ - azurerm_logic_app_standard.app, - null_resource.zip_logic_app - ] - create_duration = "${var.deployment_wait_timeout}s" -} - -# The first step is to ensure that the logic apps extension is installed -resource "null_resource" "install-extension" { - depends_on = [time_sleep.wait_for_app_settings] - - provisioner "local-exec" { - command = "az extension add --name logic" - } -} - -# Fetch the subscription name -data "azurerm_subscription" "current" {} - -# Then use the Azure CLI to start the deployment -resource "null_resource" "deploy" { - depends_on = [ - null_resource.install-extension, - null_resource.zip_logic_app - ] - - triggers = { - always_run = timestamp() # null_resource.zip_logic_app might not always actually change, trigger ensures the execution anyway - } - - provisioner "local-exec" { - command = "az logicapp deployment source config-zip --name ${var.logic_app_name} --resource-group ${var.resource_group_name} --subscription ${data.azurerm_subscription.current.display_name} --src ${path.module}/files/deploy.zip" - } -} - -data "azurerm_monitor_diagnostic_categories" "diagnostic_categories" { - count = var.log_analytics_workspace_id == null ? 0 : 1 - resource_id = azurerm_logic_app_standard.app.id -} - -// Write logs and metrics to log analytics if specified -// Needs to be done once the deployment is finished, because updating Diagnostic Settings leads to a restart of the Logic App -// which causes the deployment to fail if it is not finished yet -resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting" { - depends_on = [ - null_resource.deploy - ] - - count = var.log_analytics_workspace_id == null ? 0 : 1 - name = "diag-${var.logic_app_name}" - target_resource_id = azurerm_logic_app_standard.app.id - log_analytics_workspace_id = var.log_analytics_workspace_id - - dynamic "enabled_log" { - for_each = length(var.log_analytics_diagnostic_categories) > 0 ? var.log_analytics_diagnostic_categories : data.azurerm_monitor_diagnostic_categories.diagnostic_categories[0].log_category_types - - content { - category = enabled_log.value - - retention_policy { - enabled = false - } - } - } - - dynamic "metric" { - for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories[0].metrics - - content { - category = metric.value - enabled = true - - retention_policy { - enabled = false - } - } - } -} - -# Managed Identity Provider -data "azuread_client_config" "current" {} - -resource "random_uuid" "oath2_uuid" {} - -resource "azuread_application" "application" { - count = var.managed_identity_provider != null ? 1 : 0 - display_name = var.managed_identity_provider.create.display_name - owners = var.managed_identity_provider.create.owners != null ? concat([data.azuread_client_config.current.object_id], var.managed_identity_provider.create.owners) : [data.azuread_client_config.current.object_id] - sign_in_audience = "AzureADMyOrg" - identifier_uris = local.identifiers - - api { - requested_access_token_version = 2 - - oauth2_permission_scope { - admin_consent_description = var.managed_identity_provider.create.oauth2_settings.admin_consent_description - admin_consent_display_name = var.managed_identity_provider.create.oauth2_settings.admin_consent_display_name - enabled = var.managed_identity_provider.create.oauth2_settings.enabled - id = random_uuid.oath2_uuid.result - type = var.managed_identity_provider.create.oauth2_settings.type - user_consent_description = var.managed_identity_provider.create.oauth2_settings.user_consent_description - user_consent_display_name = var.managed_identity_provider.create.oauth2_settings.user_consent_display_name - value = var.managed_identity_provider.create.oauth2_settings.role_value - } - } - - web { - redirect_uris = ["https://${var.logic_app_name}.azurewebsites.net/.auth/login/aad/callback"] - - implicit_grant { - access_token_issuance_enabled = false - id_token_issuance_enabled = true - } - } - - required_resource_access { - resource_app_id = "00000003-0000-0000-c000-000000000000" # Microsoft Graph - - resource_access { - id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" # User.Read - type = "Scope" - } - } -} - -resource "null_resource" "always_run" { - triggers = { - timestamp = "${timestamp()}" - } -} - -resource "azapi_update_resource" "setup_auth_settings" { - count = var.managed_identity_provider != null ? 1 : 0 - type = "Microsoft.Web/sites/config@2020-12-01" - resource_id = "${azurerm_logic_app_standard.app.id}/config/web" - - depends_on = [ - azurerm_logic_app_standard.app, - null_resource.always_run - ] - - body = jsonencode({ - properties = { - siteAuthSettingsV2 = { - globalValidation = { - excludedPaths = [] - require_authentication = true, - // Even though is looks weird, it is needed. Otherwise, the app and also the designer in Azure Portal are not working - // https://techcommunity.microsoft.com/blog/integrationsonazureblog/trigger-workflows-in-standard-logic-apps-with-easy-auth/3207378 - unauthenticatedClientAction = "AllowAnonymous" - }, - IdentityProviders = { - azureActiveDirectory = { - enabled = true, - registration = { - clientId = azuread_application.application[0].application_id - clientSecretSettingName = "MICROSOFT_PROVIDER_AUTHENTICATION_SECRET" - }, - validation = { - allowedAudiences = local.allowed_audiences - } - } - } - } - } - }) - lifecycle { - /* This action should always be replaces since is works under the hood as an api call - * So it does not really track issues with the function app properly - */ - replace_triggered_by = [ - null_resource.always_run - ] - } -} - -resource "azuread_application_password" "password" { - count = var.managed_identity_provider != null ? 1 : 0 - application_object_id = azuread_application.application[0].object_id -} diff --git a/modules/azure/logic_app_standard_http_managed_identity/outputs.tf b/modules/azure/logic_app_standard_http_managed_identity/outputs.tf deleted file mode 100644 index b1a36663..00000000 --- a/modules/azure/logic_app_standard_http_managed_identity/outputs.tf +++ /dev/null @@ -1,11 +0,0 @@ -output "principal_id" { - value = length(azurerm_logic_app_standard.app.identity) > 0 ? azurerm_logic_app_standard.app.identity[0].principal_id : null -} - -output "name" { - value = azurerm_logic_app_standard.app.name -} - -output "default_hostname" { - value = azurerm_logic_app_standard.app.default_hostname -} \ No newline at end of file diff --git a/modules/azure/logic_app_standard_http_managed_identity/variables.tf b/modules/azure/logic_app_standard_http_managed_identity/variables.tf deleted file mode 100644 index c096e401..00000000 --- a/modules/azure/logic_app_standard_http_managed_identity/variables.tf +++ /dev/null @@ -1,159 +0,0 @@ -variable "location" { - type = string - description = "A datacenter location in Azure." -} - -variable "resource_group_name" { - type = string - description = "Name of the resource group." -} - -variable "logic_app_name" { - type = string - description = "Specifies the name of the logic app." -} - -variable "service_plan_id" { - type = string - description = "The ID of the Service Plan to use for this Logic App." -} - -variable "storage_account_name" { - type = string - description = "The name of the storage account to connect to the logic app" -} - -variable "storage_account_access_key" { - type = string - description = "The access key of the storage account to connect to the logic app." -} - -variable "enabled" { - type = bool - description = "If this workflow should be enabled by default or not, defaults to true" - default = true -} - -variable "use_managed_identity" { - type = bool - description = "Use System Assigned Managed Identity for this logic app" - default = false -} - -variable "identity_ids" { - type = list(string) - description = "User Assigned Managed Identity ids for this logic app" - default = [] -} - -variable "app_settings" { - type = map(string) - description = "A map of key/value pairs to be used as application settings for the logic app." - default = {} -} - -variable "workflows_source_path" { - type = string - description = "The path to the source code of all workflows." -} - -variable "integration_subnet_id" { - type = string - description = "The ID of the integration subnet to enable virtual network integration." - default = null -} - -variable "elastic_instance_minimum" { - type = number - description = "Minimum amount of elastic instances." - default = 1 -} - -variable "pre_warmed_instance_count" { - type = number - description = "Amount of pre-warmed instances. Requires at least 1 for VNet-integration." - default = 0 -} - -variable "deployment_wait_timeout" { - type = number - description = "The amount of time to wait for the deployment to start after the logic app was deployed." - default = 30 -} - -variable "https_only" { - type = bool - description = "Allow only HTTPS access." - default = false -} - -variable "logic_app_version" { - type = string - description = "The runtime version associated with the Logic App." - default = "~4" -} - -variable "log_analytics_workspace_id" { - type = string - description = "Specifies the ID of a Log Analytics Workspace where diagnostics data should be sent." - default = null -} - -variable "log_analytics_diagnostic_categories" { - type = list(string) - description = "Optional list of diagnostic categories to override the default categories." - default = [] -} - -variable "managed_identity_provider" { - type = object({ - existing = optional(object({ - client_id = string - client_secret = string - })) - create = optional(object({ - application_name = string - display_name = string - oauth2_settings = object({ - admin_consent_description = string - admin_consent_display_name = string - enabled = bool - type = string - user_consent_description = string - user_consent_display_name = string - role_value = string - }) - owners = optional(list(string)) # Deployment user will be added as owner by default - redirect_uris = optional(list(string)) # Only for additional URIs, function uri will be added by default - group_id = optional(string) # Group ID where service principal of the existing application will belong to - })) - identifier_uris = optional(list(string)) # api:// will be added by default if application is create - allowed_audiences = optional(list(string)) # api:// will be added by default - }) - validation { - condition = var.managed_identity_provider.existing != null || var.managed_identity_provider.create != null - error_message = "Variable managed_identity_provider has to provide either an existing managed identity provider or given information to create one" - } - description = "The managed identity provider to use for connections on this function app" - default = null -} - -variable "ip_restrictions" { - type = list(object({ - ip_address = optional(string), - service_tag = optional(string), - virtual_network_subnet_id = optional(string), - name = optional(string), - priority = optional(number), - action = optional(string), - - headers = optional(list(object({ - x_azure_fdid = optional(list(string)), - x_fd_health_probe = optional(list(string)), - x_forwarded_for = optional(list(string)), - x_forwarded_host = optional(list(string)) - }))) - })) - description = "A List of objects representing IP restrictions." - default = [] -} From a11dcd968d05ff18682faca75a24dd1f804ecd01 Mon Sep 17 00:00:00 2001 From: Michal Pipal Date: Mon, 10 Feb 2025 22:04:12 +0100 Subject: [PATCH 3/5] Omit MICROSOFT_PROVIDER_AUTHENTICATION_SECRET when not applicable --- modules/azure/logic_app_standard/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/azure/logic_app_standard/main.tf b/modules/azure/logic_app_standard/main.tf index 985a0711..ef63916c 100644 --- a/modules/azure/logic_app_standard/main.tf +++ b/modules/azure/logic_app_standard/main.tf @@ -79,7 +79,7 @@ resource "azurerm_logic_app_standard" "app" { app_settings = merge({ WEBSITE_NODE_DEFAULT_VERSION = "~18", FUNCTIONS_WORKER_RUNTIME = "node", - MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = "${var.managed_identity_provider != null ? azuread_application_password.password[0].value : ""}" + MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = var.managed_identity_provider == null ? azuread_application_password.password[0].value : null }, var.app_settings) app_service_plan_id = var.service_plan_id From 8b93e56d4fd81554b4bc279ed107fb80fbeb1a9c Mon Sep 17 00:00:00 2001 From: Michal Pipal Date: Mon, 10 Feb 2025 22:08:20 +0100 Subject: [PATCH 4/5] Reverse condition --- modules/azure/logic_app_standard/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/azure/logic_app_standard/main.tf b/modules/azure/logic_app_standard/main.tf index ef63916c..5812d8f3 100644 --- a/modules/azure/logic_app_standard/main.tf +++ b/modules/azure/logic_app_standard/main.tf @@ -79,7 +79,7 @@ resource "azurerm_logic_app_standard" "app" { app_settings = merge({ WEBSITE_NODE_DEFAULT_VERSION = "~18", FUNCTIONS_WORKER_RUNTIME = "node", - MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = var.managed_identity_provider == null ? azuread_application_password.password[0].value : null + MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = var.managed_identity_provider != null ? azuread_application_password.password[0].value : null }, var.app_settings) app_service_plan_id = var.service_plan_id From 90ec9740533090b8493397579e1613c8cf529e92 Mon Sep 17 00:00:00 2001 From: Michal Pipal Date: Tue, 11 Feb 2025 15:14:52 +0100 Subject: [PATCH 5/5] Updated appsettings configuration --- modules/azure/logic_app_standard/main.tf | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/modules/azure/logic_app_standard/main.tf b/modules/azure/logic_app_standard/main.tf index 5812d8f3..75559e6c 100644 --- a/modules/azure/logic_app_standard/main.tf +++ b/modules/azure/logic_app_standard/main.tf @@ -77,10 +77,9 @@ resource "azurerm_logic_app_standard" "app" { } app_settings = merge({ - WEBSITE_NODE_DEFAULT_VERSION = "~18", - FUNCTIONS_WORKER_RUNTIME = "node", - MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = var.managed_identity_provider != null ? azuread_application_password.password[0].value : null - }, var.app_settings) + WEBSITE_NODE_DEFAULT_VERSION = "~18", + FUNCTIONS_WORKER_RUNTIME = "node" + }, var.managed_identity_provider != null ? { MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = azuread_application_password.password[0].value } : {}, var.app_settings) app_service_plan_id = var.service_plan_id storage_account_access_key = var.storage_account_access_key