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://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.
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