Search code examples
terraformazure-functionsazure-web-app-serviceazure-rmazure-private-link

azurerm_linux_function_app Creation of storage file share failed - check if the storage account is accessible - azurerm_private_endpoint


Trying to get azurerm_linux_function_app to work with azurerm_storage_account_network_rules and azurerm_private_endpoint.

Terraform v1.3.7
on darwin_arm64
+ provider registry.terraform.io/hashicorp/azurerm v3.38.0

This is what I'm trying.

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "func_rg" {
  name     = "func"
  location = "eastus"
}

# Create the network VNET
resource "azurerm_virtual_network" "func" {
  name                = "func_vnet"
  address_space       = ["10.0.0.0/16"]
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
}

# Create a subnet for endpoint
resource "azurerm_subnet" "endpoint_subnet" {
  name                                      = "endpoint_subnet"
  address_prefixes                          = ["10.0.0.0/24"]
  virtual_network_name                      = azurerm_virtual_network.func.name
  resource_group_name                       = azurerm_resource_group.func_rg.name
  private_endpoint_network_policies_enabled = true
}

# Create subnet for functions
resource "azurerm_subnet" "func_subnet" {
  name                 = "func_subnet"
  resource_group_name  = azurerm_resource_group.func_rg.name
  virtual_network_name = azurerm_virtual_network.func.name
  address_prefixes     = ["10.0.1.0/24"]
  service_endpoints    = ["Microsoft.Storage"]

  delegation {
    name = "delegation"

    service_delegation {
      name = "Microsoft.Web/serverFarms"
    }
  }
}

# Create nsg for subnets
resource "azurerm_network_security_group" "func" {
  name                = "func_nsg"
  location            = azurerm_resource_group.func_rg.location
  resource_group_name = azurerm_resource_group.func_rg.name
}

# Associate nsg with func subnet
resource "azurerm_subnet_network_security_group_association" "func" {
  subnet_id                 = azurerm_subnet.func_subnet.id
  network_security_group_id = azurerm_network_security_group.func.id
}

# Associate nsg with endpoint subnet
resource "azurerm_subnet_network_security_group_association" "endpoint" {
  subnet_id                 = azurerm_subnet.endpoint_subnet.id
  network_security_group_id = azurerm_network_security_group.func.id
}

# Create blob Private DNS Zone
resource "azurerm_private_dns_zone" "blob_dns_zone" {
  name                = "privatelink.blob.core.windows.net"
  resource_group_name = azurerm_resource_group.func_rg.name
}

# Create file Private DNS Zone
resource "azurerm_private_dns_zone" "file_dns_zone" {
  name                = "privatelink.file.core.windows.net"
  resource_group_name = azurerm_resource_group.func_rg.name
}

# Create table Private DNS Zone
resource "azurerm_private_dns_zone" "table_dns_zone" {
  name                = "privatelink.table.core.windows.net"
  resource_group_name = azurerm_resource_group.func_rg.name
}

# Create queue Private DNS Zone
resource "azurerm_private_dns_zone" "queue_dns_zone" {
  name                = "privatelink.queue.core.windows.net"
  resource_group_name = azurerm_resource_group.func_rg.name
}

# Create blob Private DNS Zone Network Link
resource "azurerm_private_dns_zone_virtual_network_link" "blob_vnl" {
  name                  = "blob_vnl"
  resource_group_name   = azurerm_resource_group.func_rg.name
  private_dns_zone_name = azurerm_private_dns_zone.blob_dns_zone.name
  virtual_network_id    = azurerm_virtual_network.func.id
}

# Create file Private DNS Zone Network Link
resource "azurerm_private_dns_zone_virtual_network_link" "file_vnl" {
  name                  = "file_vnl"
  resource_group_name   = azurerm_resource_group.func_rg.name
  private_dns_zone_name = azurerm_private_dns_zone.file_dns_zone.name
  virtual_network_id    = azurerm_virtual_network.func.id
}

# Create table Private DNS Zone Network Link
resource "azurerm_private_dns_zone_virtual_network_link" "table_vnl" {
  name                  = "table_vnl"
  resource_group_name   = azurerm_resource_group.func_rg.name
  private_dns_zone_name = azurerm_private_dns_zone.table_dns_zone.name
  virtual_network_id    = azurerm_virtual_network.func.id
}

# Create queue Private DNS Zone Network Link
resource "azurerm_private_dns_zone_virtual_network_link" "queue_vnl" {
  name                  = "queue_vnl"
  resource_group_name   = azurerm_resource_group.func_rg.name
  private_dns_zone_name = azurerm_private_dns_zone.queue_dns_zone.name
  virtual_network_id    = azurerm_virtual_network.func.id
}

resource "azurerm_storage_account" "func" {
  name                     = "rdtestfuncsa"
  resource_group_name      = azurerm_resource_group.func_rg.name
  location                 = azurerm_resource_group.func_rg.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_account_network_rules" "func" {
  storage_account_id = azurerm_storage_account.func.id

  default_action             = "Deny"
  ip_rules                   = ["***.***.***.***"]
  virtual_network_subnet_ids = [azurerm_subnet.func_subnet.id]
  bypass                     = ["Metrics", "Logging", "AzureServices"]
}

resource "azurerm_storage_container" "func" {
  name                 = "func-sc"
  storage_account_name = azurerm_storage_account.func.name
  container_access_type = "private"
}

# Create Private Endpints
resource "azurerm_private_endpoint" "file_endpoint" {
  name                = "rdtest_file_pe"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  subnet_id           = azurerm_subnet.endpoint_subnet.id
  private_service_connection {
    name                           = "rdtest_file_psc"
    private_connection_resource_id = azurerm_storage_account.func.id
    is_manual_connection           = false
    subresource_names              = ["file"]
  }
}

resource "azurerm_private_endpoint" "blob_endpoint" {
  name                = "rdtest_blob_pe"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  subnet_id           = azurerm_subnet.endpoint_subnet.id
  private_service_connection {
    name                           = "rdtest_blob_psc"
    private_connection_resource_id = azurerm_storage_account.func.id
    is_manual_connection           = false
    subresource_names              = ["blob"]
  }
}

resource "azurerm_private_endpoint" "table_endpoint" {
  name                = "rdtest_table_pe"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  subnet_id           = azurerm_subnet.endpoint_subnet.id
  private_service_connection {
    name                           = "rdtest_table_psc"
    private_connection_resource_id = azurerm_storage_account.func.id
    is_manual_connection           = false
    subresource_names              = ["table"]
  }
}

resource "azurerm_private_endpoint" "queue_endpoint" {
  name                = "rdtest_queue_pe"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  subnet_id           = azurerm_subnet.endpoint_subnet.id
  private_service_connection {
    name                           = "rdtest_queue_psc"
    private_connection_resource_id = azurerm_storage_account.func.id
    is_manual_connection           = false
    subresource_names              = ["queue"]
  }
}

# Create DNS A Records
resource "azurerm_private_dns_a_record" "blob_dns_a" {
  name                = "rdfunctestblobdns"
  zone_name           = azurerm_private_dns_zone.blob_dns_zone.name
  resource_group_name = azurerm_resource_group.func_rg.name
  ttl                 = 300
  records             = [azurerm_private_endpoint.blob_endpoint.private_service_connection.0.private_ip_address]
}

resource "azurerm_private_dns_a_record" "file_dns_a" {
  name                = "filedns"
  zone_name           = azurerm_private_dns_zone.file_dns_zone.name
  resource_group_name = azurerm_resource_group.func_rg.name
  ttl                 = 300
  records             = [azurerm_private_endpoint.file_endpoint.private_service_connection.0.private_ip_address]
}

resource "azurerm_private_dns_a_record" "table_dns_a" {
  name                = "tabledns"
  zone_name           = azurerm_private_dns_zone.table_dns_zone.name
  resource_group_name = azurerm_resource_group.func_rg.name
  ttl                 = 300
  records             = [azurerm_private_endpoint.table_endpoint.private_service_connection.0.private_ip_address]
}

resource "azurerm_private_dns_a_record" "queue_dns_a" {
  name                = "queuedns"
  zone_name           = azurerm_private_dns_zone.queue_dns_zone.name
  resource_group_name = azurerm_resource_group.func_rg.name
  ttl                 = 300
  records             = [azurerm_private_endpoint.queue_endpoint.private_service_connection.0.private_ip_address]
}

resource "azurerm_service_plan" "func" {
  name                = "func_sp"
  resource_group_name = azurerm_resource_group.func_rg.name
  location            = azurerm_resource_group.func_rg.location
  os_type             = "Linux"
  sku_name            = "EP1"
}

resource "azurerm_linux_function_app" "func" {
  name                       = "rd-test-func"
  resource_group_name        = azurerm_resource_group.func_rg.name
  location                   = azurerm_resource_group.func_rg.location
  virtual_network_subnet_id  = azurerm_subnet.func_subnet.id
  storage_account_name       = azurerm_storage_account.func.name
  storage_account_access_key = azurerm_storage_account.func.primary_access_key
  service_plan_id            = azurerm_service_plan.func.id
  site_config {
    application_stack {
      node_version = 16
    }
  }
}

I get this error from that config

│ Error: creating Linux Function App: (Site Name "rd-test-func" / Resource Group "func"): web.AppsClient#CreateOrUpdate: Failure sending request: StatusCode=400 -- Original Error: 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." 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":{"Code":"BadRequest","ExtendedCode":"99022","Message":"Creation of storage file share failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible.","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."]}}]
│ 
│   with azurerm_linux_function_app.func,
│   on main.tf line 236, in resource "azurerm_linux_function_app" "func":
│  236: resource "azurerm_linux_function_app" "func" {
│ 
│ creating Linux Function App: (Site Name "rd-test-func" / Resource Group "func"): web.AppsClient#CreateOrUpdate: Failure sending request: StatusCode=400 -- Original Error: 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." 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":{"Code":"BadRequest","ExtendedCode":"99022","Message":"Creation of storage file share
│ failed with: 'The remote server returned an error: (403) Forbidden.'. Please check if the storage account is accessible.","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."]}}]

If I take out the azurerm_storage_account_network_rules, which I need for compliance, the config will deploy, but publishing the function fails on syncing triggers.

func azure functionapp publish rd-test-func
Getting site publishing info...
Uploading package...
Uploading 1.35 KB [###############################################################################]
Upload completed successfully.
Deployment completed successfully.
Syncing triggers...
Syncing triggers...
Syncing triggers...
Syncing triggers...
Syncing triggers...
Syncing triggers...
Error calling sync triggers (BadRequest).

If I remove virtual_network_subnet_id = azurerm_subnet.func_subnet.id from azurerm_linux_function_app as well the function will publish, but I need the function app connecting over the private endpoint or with network rules.

Anyone know how to get this to work?


Solution

  • Turns out azurerm_service_plan needs sku_name = "B1" or better for the vnet integration with azurerm_storage_account_network_rules to work. The following worked with B1, S1 and P1v2.

    provider "azurerm" {
      features {}
    }
    
    resource "azurerm_resource_group" "func_rg" {
      name     = "func"
      location = "eastus"
    }
    
    resource "azurerm_virtual_network" "func" {
      name                = "func_vnet"
      address_space       = ["10.0.0.0/16"]
      resource_group_name = azurerm_resource_group.func_rg.name
      location            = azurerm_resource_group.func_rg.location
    }
    
    resource "azurerm_subnet" "func_subnet" {
      name                 = "func_subnet"
      resource_group_name  = azurerm_resource_group.func_rg.name
      virtual_network_name = azurerm_virtual_network.func.name
      address_prefixes     = ["10.0.1.0/24"]
      service_endpoints    = ["Microsoft.Storage"]
    
      delegation {
        name = "delegation"
    
        service_delegation {
          name = "Microsoft.Web/serverFarms"
        }
      }
    }
    
    resource "azurerm_storage_account" "func" {
      name                     = "rdtestfuncsa"
      resource_group_name      = azurerm_resource_group.func_rg.name
      location                 = azurerm_resource_group.func_rg.location
      account_tier             = "Standard"
      account_replication_type = "LRS"
    }
    
    resource "azurerm_storage_account_network_rules" "func" {
      storage_account_id = azurerm_storage_account.func.id
    
      default_action             = "Deny"
      ip_rules                   = ["***.***.***.***"]
      virtual_network_subnet_ids = [azurerm_subnet.func_subnet.id]
      bypass                     = ["Metrics", "Logging", "AzureServices"]
    }
    
    resource "azurerm_service_plan" "func" {
      name                = "func_sp"
      resource_group_name = azurerm_resource_group.func_rg.name
      location            = azurerm_resource_group.func_rg.location
      os_type             = "Linux"
      sku_name            = "B1"
    }
    
    resource "azurerm_linux_function_app" "func" {
      name                       = "rd-test-func"
      resource_group_name        = azurerm_resource_group.func_rg.name
      location                   = azurerm_resource_group.func_rg.location
      virtual_network_subnet_id  = azurerm_subnet.func_subnet.id
      storage_account_name       = azurerm_storage_account.func.name
      storage_account_access_key = azurerm_storage_account.func.primary_access_key
      service_plan_id            = azurerm_service_plan.func.id
    
      site_config {
        application_stack {
          node_version = 16
        }
      }
    }