Azure Standard Logic App with private endpoint and private Storage Account

In your organization, you may have an Azure environment where all deployed services need to be private for security-related requirements. In this context Azure has a set of services such as service endpoint, private endpoint, Azure bastion, IP restrictions, etc…
In this article I will show how to deploy a standard azure logic app privatized with a private endpoint which uses a storage account also privatized with a private endpoint using terraform

I will need to deploy the following services :

  • An azure Virtual Network
  • An InboundSubnet used to setup private endpoints
  • OutboundSubnet used to setup virtual network integration
  • Private dns zone and link it to virtual network
  • Private dns zone and link it to virtual network
  • Storage account
  • Private endpoint for the storage account
  • An azure logic app standard
  • Private endpoint for the storage account

Setup terraform

The first step should be to setup your terraform environment. For more information on terraform, you can find by following these links all the information to start using terraform and also the azure provider which allows you to deploy in azure.

https://www.terraform.io/

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs

The following configuration requires the Azurerm provider to enable deployments to Azure using a 3.0.0 version or later

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">=3.0.0"
    }
  }
}
provider "azurerm" {
  features {}
}

Resource group and virtual network

The following code creates a ressource group and a virtual network

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group

https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network

Here you can find all documentation related to azure rm provider : https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs

resource "azurerm_resource_group" "resource_group" {
  location = var.resource_group_location
  name     = var.resource_group_name
}

resource "azurerm_virtual_network" "virtual_network" {
  address_space       = [var.virtual_network_address_space]
  location            = var.resource_group_location
  name                = var.virtual_network_name
  resource_group_name = var.resource_group_name
  depends_on = [
    azurerm_resource_group.resource_group,
  ]
}

Inbound subnet

I need a inbound subnet to setup private endpoints for azure logic app and the storage account

resource "azurerm_subnet" "inbound_subnet" {
  address_prefixes     = [var.inbound_subnet_address_space]
  name                 = var.inbound_subnet_name
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.virtual_network.name
  depends_on = [
    azurerm_virtual_network.virtual_network
  ]
}

Outbound subnet

I need a outbound subnet to configure virtual network integration for azure azure logic app ,

For more information about virtual network integration, please refer to the following link:

https://learn.microsoft.com/en-us/azure/app-service/configure-vnet-integration-enable.

The subnet should be delegated to Microsoft.Web/serverFarms with a service endpoint configured for Microsoft.Storage.

Delegation to Microsoft.Web/serverFarms is required to configure virtual network integration for azure azure logic app.

The “Microsoft.Storage” service endpoint will be utilized later to enable a private Logic App to access the storage account.

resource "azurerm_subnet" "outbound_subnet" {
  address_prefixes     = [var.outbound_subnet_address_space]
  name                 = var.outbound_subnet_name
  resource_group_name  = var.resource_group_name
  service_endpoints    = ["Microsoft.Storage"]
  virtual_network_name = azurerm_virtual_network.virtual_network.name
  delegation {
    name = "delegation"
    service_delegation {
      actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
      name    = "Microsoft.Web/serverFarms"
    }
  }
  depends_on = [
    azurerm_virtual_network.virtual_network
  ]
}

Private dns zones

Private dns zones are required to configure private endpoints for azure logic app (privatelink.azurewebsites.net) and for storage account (privatelink.web.core.windows.net). A dns record will associate the dns name with the private ip adress.

For more information about private endpoint , please refer to the following links:

https://learn.microsoft.com/en-us/azure/dns/private-dns-privatednszone

https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview.

https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-dns?ranMID=43674&ranEAID=wizKxmN8no4&ranSiteID=wizKxmN8no4-oRMSAJsDrSilpYZ_dhi2Og&epi=wizKxmN8no4-oRMSAJsDrSilpYZ_dhi2Og&irgwc=1&OCID=AIDcmm549zy227_aff_7795_1243925&tduid=(ir__1bpp0y3rvskfbz3i9w901do1vu2x9rtoyllxoivl00)(7795)(1243925)(wizKxmN8no4-oRMSAJsDrSilpYZ_dhi2Og)()&irclickid=_1bpp0y3rvskfbz3i9w901do1vu2x9rtoyllxoivl00

resource "azurerm_private_dns_zone" "azurewebsites" {
  name                = "privatelink.azurewebsites.net"
  resource_group_name = var.resource_group_name
  depends_on = [
    azurerm_resource_group.resource_group
  ]
}

resource "azurerm_private_dns_zone" "web_core_windows" {
  name                = "privatelink.web.core.windows.net"
  resource_group_name = var.resource_group_name

  depends_on = [
    azurerm_resource_group.resource_group
  ]
}

Link private dns zones to virtual network

The following code will link the virtual network to the private dsn zones created earlier so that Virtual Machines hosted in that virtual network can resolve the dns configured in the private DNS zones.

For more information about private endpoint , please refer to the following links: https://learn.microsoft.com/en-us/azure/dns/private-dns-getstarted-portal

resource "azurerm_private_dns_zone_virtual_network_link" "virtual_network_link_azurewebsites" {
  name                  = "${var.virtual_network_name}-azurewebsites-link"
  private_dns_zone_name = azurerm_private_dns_zone.azurewebsites.name
  resource_group_name   = var.resource_group_name
  virtual_network_id    = azurerm_virtual_network.virtual_network.id
  depends_on = [
    azurerm_private_dns_zone.azurewebsites,
    azurerm_virtual_network.virtual_network
  ]
}

resource "azurerm_private_dns_zone_virtual_network_link" "virtual_network_link_windows_web_core" {
  name                  = "${var.virtual_network_name}-windows_web_core-link"
  private_dns_zone_name = azurerm_private_dns_zone.web_core_windows.name
  resource_group_name   = var.resource_group_name
  virtual_network_id    = azurerm_virtual_network.virtual_network.id
  depends_on = [
    azurerm_private_dns_zone.web_core_windows,
    azurerm_virtual_network.virtual_network
  ]
}

Storage Account

The following code will create a private azure storage account

resource "azurerm_storage_account" "storage_account" {
  account_replication_type        = var.storage_account_replication_type
  account_tier                    = var.storage_account_tier
  location                        = var.resource_group_location
  name                            = "${var.storage_account_name}${var.environment}"
  min_tls_version                 = "TLS1_2"
  allow_nested_items_to_be_public = false
  public_network_access_enabled   = false
  resource_group_name             = var.resource_group_name

  depends_on = [
    azurerm_resource_group.resource_group
  ]
}

Setup private endpoint for storage account

The following code will configure the private endpoint for the storage account

locals {
  storage_subresources = ["blob", "file", "queue", "table"]
}

resource "azurerm_private_endpoint" "private_endpoint_storage" {
  for_each            = toset(local.storage_subresources)
  location            = var.resource_group_location
  name                = "pe-${var.storage_account_name}-${each.key}"
  resource_group_name = var.resource_group_name
  subnet_id           = azurerm_subnet.inbound_subnet.id
  private_service_connection {
    is_manual_connection           = false
    name                           = "pe-con-${var.storage_account_name}-${each.key}"
    private_connection_resource_id = azurerm_storage_account.storage_account.id
    subresource_names              = [each.key]
  }

  private_dns_zone_group {
    name                 = "default"
    private_dns_zone_ids = [azurerm_private_dns_zone.web_core_windows.id]
  }
  depends_on = [azurerm_private_dns_zone.web_core_windows, azurerm_storage_account.storage_account, azurerm_private_dns_zone_virtual_network_link.virtual_network_link_azurewebsites]
}

Azure Logic App Standard

The following code will create a private azure logic app standard

resource "azurerm_service_plan" "service_plan" {
  location            = var.resource_group_location
  name                = "${var.service_plan_name}-${var.environment}"
  os_type             = "Windows"
  resource_group_name = var.resource_group_name
  sku_name            = "WS1"

  maximum_elastic_worker_count = 20

  zone_balancing_enabled = var.service_plan_zone_balancing_enabled
  depends_on = [
    azurerm_resource_group.resource_group
  ]
}
resource "azurerm_logic_app_standard" "logic_app_standard" {
  app_service_plan_id        = azurerm_service_plan.service_plan.id
  https_only                 = true
  location                   = var.resource_group_location
  name                       = "${var.windows_logic_app_name}-${var.environment}"
  resource_group_name        = var.resource_group_name
  storage_account_access_key = azurerm_storage_account.storage_account.primary_access_key
  storage_account_name       = azurerm_storage_account.storage_account.name

  version                   = "~4"
  virtual_network_subnet_id = azurerm_subnet.outbound_subnet.id
  identity {
    type = "SystemAssigned"
  }
  app_settings = {
    "FUNCTIONS_WORKER_RUNTIME" : "node"
    "WEBSITE_NODE_DEFAULT_VERSION" : "~18"
  }

  site_config {
    use_32_bit_worker_process        = false
    ftps_state                       = "Disabled"
    websockets_enabled               = false
    min_tls_version                  = "1.2"
    runtime_scale_monitoring_enabled = false
    always_on                        = true
    public_network_access_enabled    = false
    elastic_instance_minimum         = 3
  }
  depends_on = [
    azurerm_subnet.outbound_subnet, azurerm_storage_share.storage_share,
    azurerm_service_plan.service_plan
  ]
}

Logic app private endpoint

The following code will configure the private endpoint for the azure logic app.

resource "azurerm_private_endpoint" "private_endpoint" {
  location            = var.resource_group_location
  name                = "pe-${var.windows_logic_app_name}"
  resource_group_name = var.resource_group_name
  subnet_id           = azurerm_subnet.inbound_subnet.id
  private_service_connection {
    is_manual_connection           = false
    name                           = "pe-con-${var.windows_logic_app_name}"
    private_connection_resource_id = azurerm_logic_app_standard.logic_app_standard.id
    subresource_names              = ["sites"]
  }

  private_dns_zone_group {
    name                 = "default"
    private_dns_zone_ids = [azurerm_private_dns_zone.azurewebsites.id]
  }
  depends_on = [azurerm_private_dns_zone.azurewebsites, azurerm_logic_app_standard.logic_app_standard, azurerm_private_dns_zone_virtual_network_link.virtual_network_link_azurewebsites]
}

The terraform variables

Variables

variable "environment" {
}

variable "resource_group_name" {
}

variable "resource_group_location" {
}

variable "virtual_network_name" {
}

variable "virtual_network_address_space" {
}

variable "inbound_subnet_name" {
}

variable "inbound_subnet_address_space" {
}
variable "outbound_subnet_name" {
}

variable "outbound_subnet_address_space" {
}

variable "service_plan_name" {
}
variable "service_plan_zone_balancing_enabled" {
}
variable "windows_logic_app_name" {
}

variable "storage_account_name" {
}

variable "storage_account_replication_type" {
}

variable "storage_account_tier" {
}

Values

The terraform values used to set the actual values of the variables in order to deploy the dev environment

environment                         = "dev"
resource_group_name                 = "rg-spoke-demo-dev"
resource_group_location             = "francecentral"
virtual_network_name                = "vnet-logicapp-demo"
virtual_network_address_space       = "10.0.0.0/16"
inbound_subnet_name                 = "inboundSubnet"
inbound_subnet_address_space        = "10.0.0.0/24"
outbound_subnet_name                = "outboundSubnet"
outbound_subnet_address_space       = "10.0.1.0/24"
service_plan_name                   = "logicappdatasync-asp"
service_plan_zone_balancing_enabled = false
windows_logic_app_name              = "logicappdatasync"
storage_account_name                = "storagedatasync"
storage_account_replication_type    = "LRS"
storage_account_tier                = "Standard"

Deploy

terraform workspace new dev: This command creates a new workspace named “dev”. Workspaces in Terraform allow you to manage multiple environments (such as development, staging, production) with separate state files.

terraform workspace select dev: This command selects the workspace named “dev”. After creating a workspace, you need to select it before performing any Terraform operations within that workspace.

terraform init -var-file=”dev.tfvars”: This command initializes a Terraform configuration in the selected workspace. .

terraform plan -var-file=”dev.tfvars”: This command generates an execution plan. It compares the current state of your infrastructure (defined in your Terraform configuration) with the desired state and produces an execution plan describing what Terraform will do to achieve the desired state.

terraform apply -var-file=”dev.tfvars” –auto-approve: This command applies the changes necessary to reach the desired state of the configuration.

The –auto-approve flag is used to automatically approve and apply the changes without requiring manual confirmation from the user.

terraform workspace new dev 
terraform workspace select dev
terraform init -var-file="dev.tfvars" 
terraform plan -var-file="dev.tfvars" 
terraform apply -var-file="dev.tfvars"  --auto-approve

Testing

To test the configuration , I can go to portal.azure.com and navigate to the azure logic app, I can see that the azure logic app cannot access to the private storage account.

Firewall rule

To resolve the issue , I can allow the storage account to access to the outbound subnet of the azure logic app. Note that azure logic app virtual network integration is also configured the outbound subnet

resource "azurerm_storage_account_network_rules" "storage_account_network_rules" {
  storage_account_id         = azurerm_storage_account.storage_account.id
  default_action             = "Deny"
  bypass                     = ["AzureServices"]
  ip_rules                   = [var.storage_account_allowed_ip]
  virtual_network_subnet_ids = [azurerm_subnet.outbound_subnet.id]
  depends_on                 = [azurerm_storage_account.storage_account]
}

Storage account file share

The azure logic cannot create the fileshare , so I will create it with name of the logic app followed by content ( mylogicappname-content ).

It should be possible to auto provisionne the fileshare using another way

resource "azurerm_storage_share" "storage_share" {
  quota                = 5120
  name                 = "${var.windows_logic_app_name}-${var.environment}-content"
  storage_account_name = azurerm_storage_account.storage_account.name

  depends_on = [azurerm_storage_account.storage_account]
}

Update logic app configuration

I will add to parameters in the appsetting of the azure loogic app

  •  “WEBSITE_VNET_ROUTE_ALL”      = 1
  • “WEBSITE_CONTENTOVERVNET” = 1
  app_settings = {
     "FUNCTIONS_WORKER_RUNTIME" : "node"
    "WEBSITE_NODE_DEFAULT_VERSION" : "~18"
    "WEBSITE_VNET_ROUTE_ALL"          = 1
    "WEBSITE_CONTENTOVERVNET" : 1
  }

Testing

When I test again , it should work

Source code

source code is available here : https://github.com/azurecorner/azure-logic-app-standard-with-private-endpoint-and-private-storage-account

Leave a Comment