Problem
A user may run into the following error when provisioning resources where the storage account has public network access disabled.
╷
│ Error: creating App Service (Subscription: "6056f999-3ec1-488a-88ae-9946e00aa0b9"
│ Resource Group Name: "logicapp-rg"
│ Site Name: "logicapptest"): performing CreateOrUpdate: unexpected status 400 (400 Bad Request) with response: {"Code":"BadRequest","Message":"Creation of storage file share failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible.","Target":null,"Details":[{"Message":"Creation of storage file share failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible."},{"Code":"BadRequest"},{"ErrorEntity":{"ExtendedCode":"99022","MessageTemplate":"Creation of storage file share failed with: '{0}'. Please check if the storage account is accessible.","Parameters":["The remote server returned an error: (403) Forbidden."],"Code":"BadRequest","Message":"Creation of storage file share failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible."}}],"Innererror":null}
│
│ with azurerm_logic_app_standard.logicapp,
│ on main.tf line 127, in resource "azurerm_logic_app_standard" "logicapp":
│ 127: resource "azurerm_logic_app_standard" "logicapp" {
│
╵
Cause
Due to limitations within the Azure API, the AzureRM Provider has to make use of the Data Plane API when provisioning certain resources (e.g., storage containers, keys). When such resource has public network access disabled, these calls against the Data Plane will fail because they are going to the public endpoint which is not authorised to reach the resources which are only available through private access.
Solution
To deploy a Logic App Standard that would remain completely private, with no public exposure to the internet. To achieve this, so that all communication would flow entirely through a Virtual Network (VNet).
1. We create a Virtual Network.
This VNet would be handling the private traffic between the logic app and storage account . This would require two subnets:
-
A subnet for private endpoints(no delegation ), which wouldn’t be delegated to any particular service. This would be attached to the storage account’s private endpoints to this subnet.
-
A subnet delegated to
Microsoft.Web/serverFarms
, so the Logic App Standard (which runs on Azure App Service technology) could integrate seamlessly with the network. Delegation means telling Azure that “this subnet is reserved for running App Service or Logic App workloads.”
- Delegating the subnet to
Microsoft.Web/serverFarms
is crucial: otherwise, Azure wouldn’t allow our Logic App Standard to sit inside it.
resource "azurerm_subnet" "logicapp_subnet" {
name = "logicapp-subnet"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.0.2.0/24"]
delegation {
name = "logicapp-delegation"
service_delegation {
name = "Microsoft.Web/serverFarms"
actions = [
"Microsoft.Network/virtualNetworks/subnets/action",
]
}
}
service_endpoints = ["Microsoft.Storage"]
}
2. Setting Up a Private Storage Account
Next came our storage account. We disabled public network access (public_network_access_enabled = false
) and made sure hierarchical namespace (HNS) was turned off. Logic App Standard needs to create Azure Files shares for storing its content, and HNS can conflict with that.
Why?
-
Public network access off means no direct internet traffic can reach the account.
-
Turning off HNS ensures we can mount Azure Files shares properly for the Logic App.
3. Adding Private Endpoints for “blob” and “file”
Because the storage account was entirely private, we needed a way for the Logic App to talk to it internally. We created two private endpoints—one for blob and one for file.
Why two endpoints?
-
Even though our Logic App primarily needs “file” for its content share, behind the scenes it also uses “blob” for logs and extension bundles. If we only had “file,” the app could still fail with a 403 error when it tried to talk to a blob endpoint.
Private DNS zones came next. We needed privatelink.blob.core.windows.net
and privatelink.file.core.windows.net
, each with an A-record that mapped <storageaccount>.blob.core.windows.net
or <storageaccount>.file.core.windows.net
to the private endpoint’s IP.
Why DNS zones?
-
When public access is off, Azure routes traffic via private IP addresses. The Logic App needs to resolve those storage account hostnames to a private IP, and private DNS zones achieve that.
4. Introducing the Service Plan
After the storage details were sorted, we needed a Service Plan to run our Logic App Standard. Since it’s built on Azure App Service, we picked the Workflow Standard (WS1, WS2, etc.) SKU.
Why?
-
Logic App Standard runs on the same infrastructure as App Service. The plan determines performance and cost.
-
We connected it to our delegated subnet so it would remain within the private network.
5 .Now create the Logic App Standard resource itself with these crucial settings:
-
public_network_access = "Disabled"
ensures no inbound public traffic. -
virtual_network_subnet_id
points to the delegated subnet in our VNet. -
site_config
andapp_settings
included-
WEBSITE_VNET_ROUTE_ALL = "1"
-
WEBSITE_CONTENTOVERVNET = "1"
so that all storage traffic is forced through the VNet.
-
-
A
depends_on
reference made sure the storage account was fully provisioned (and private endpoints were ready) before the Logic App started trying to mount its file share.
Why the app settings?
-
Without
WEBSITE_VNET_ROUTE_ALL
andWEBSITE_CONTENTOVERVNET
, the Logic App might still attempt to use a public endpoint for some calls—leading to 403 errors.
resource "azurerm_logic_app_standard" "logicapp" {
name = "logicapptest"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
app_service_plan_id = azurerm_service_plan.logicapp_plan.id
storage_account_name = azurerm_storage_account.sa.name
storage_account_access_key = azurerm_storage_account.sa.primary_access_key
public_network_access = "Disabled"
virtual_network_subnet_id = azurerm_subnet.logicapp_subnet.id
enabled = true
https_only = true
version = "~3"
site_config {}
app_settings = {
WEBSITE_VNET_ROUTE_ALL = "1"
WEBSITE_CONTENTOVERVNET = "1"
}
# optionally add depends_on if needed:
depends_on = [
azurerm_storage_account.sa
]
}
Outcome
After using the solution above, Terraform should be able to successfully provision the resource.
Additional Information
If the above solution does not resolve your issue, please open a ticket with HashiCorp Support for additional assistance.
References
https://techcommunity.microsoft.com/blog/integrationsonazureblog/deploying-standard-logic-app-to-storage-account-behind-firewall-using-service-or/2626286
https://learn.microsoft.com/en-us/azure/logic-apps/deploy-single-tenant-logic-apps-private-storage-account#deploy-arm-template