Hey folks in this blog post I’m going to cover how you can easily deploy the Palo Alto Cloud Next-Generation Firewall (NGFW) with Strata Cloud Manager integration using Terraform and AzAPI.
This blog will cover the IaC side of things and to keep this short and sweet I’m assuming you already have working knowledge of how to deploy your IaC via CICD workflows/pipelines.
Be sure to also checkout the Palo Alto documentation on Cloud NGFW to fill in any gaps for the overall deployment prerequisities and configuration. One thing that caught me out early on was onboarding the Azure Tenant to the Strata Cloud Manager account and ensuring the required Azure resource providers were registered on the target subscription – which I why I recommend familiarising yourself with the Palo Alto guidance above!
Provider Config
These are the providers I pinned to for my Palo Alto deployment. Note that I am using AzAPI provider because at the time of writing AzureRM did not have a resource covering my use case to deploy a Palo Alto NGFW with Strata Cloud Manager (SCM) integration.
terraform {
required_version = ">= 1.3"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.39.0"
}
azapi = {
source = "Azure/azapi"
version = ">= 2.4"
}
}
}
AzureRM Resources
Here I’m creating the Palo Alto virtual network appliance which I’m associating to my virtual WAN hub ID.
There’s also 2 separate baseline public IPs resources to cover the Egress NAT traffic and standard traffic. Additional public IPs can be easily created by incrementing the related count variable value.
Finally there’s a user assigned managed identity which is required by the Palo Alto NGFW as it didn’t support a system managed identity.
data "azurerm_client_config" "current" {} resource "azurerm_palo_alto_virtual_network_appliance" "this" { name = var.appliance_name virtual_hub_id = var.virtual_hub_id } resource "azurerm_public_ip" "egress_nat" { for_each = { for i in range(var.egress_nat_ip_address_count) : i => i } name = "${var.firewall_name}-egress-nat-pip-${each.key}" location = var.location resource_group_name = var.resource_group_name sku = "Standard" allocation_method = "Static" tags = var.tags } resource "azurerm_public_ip" "this" { for_each = { for i in range(var.public_ip_address_count) : i => i } name = "${var.firewall_name}-pip-${each.key}" location = var.location resource_group_name = var.resource_group_name sku = "Standard" allocation_method = "Static" tags = var.tags } resource "azurerm_user_assigned_identity" "this" { name = "${var.firewall_name}-uami" location = var.location resource_group_name = var.resource_group_name tags = var.tags }
AzAPI Resources
The AzAPI resource here is fairly simple:
- set the parent_id to the RG
- pass through the user assiged identity ID
- configure the FW to use Strata Cloud Manager or Panorama based on a variable value (var.cloud_managed_type)
- pass through the Strata Cloud Manager config or Panorama config based on a variable value (var.cloud_managed_type)
- for each on the public IPs based on the count
- link the FW to the network virtual appliance and VWAN Hub
resource "azapi_resource" "this" {
type = "PaloAltoNetworks.Cloudngfw/firewalls@2025-05-23"
name = var.firewall_name
parent_id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/resourceGroups/${var.resource_group_name}"
location = var.location
tags = var.tags
identity {
type = "UserAssigned" # Only user assigned identity is supported at this time
identity_ids = [azurerm_user_assigned_identity.this.id]
}
body = {
properties = {
dnsSettings = {
dnsServers = [
for dns_server in var.dns_settings.dns_servers : {
address = dns_server.address
resourceId = dns_server.resourceId
}
]
enabledDnsType = var.dns_settings.enabled_dns_type
enableDnsProxy = var.dns_settings.enable_dns_proxy
}
isStrataCloudManaged = var.cloud_managed_type == "Strata" ? true : false
isPanoramaManaged = var.cloud_managed_type == "Panorama" ? true : false
networkProfile = {
egressNatIp = [
for i in range(var.egress_nat_ip_address_count) : {
address = azurerm_public_ip.egress_nat[i].ip_address
resourceId = azurerm_public_ip.egress_nat[i].id
}
]
enableEgressNat = var.enable_egress_nat
networkType = var.network_type
publicIps = [
for i in range(var.public_ip_address_count) : {
address = azurerm_public_ip.this[i].ip_address
resourceId = azurerm_public_ip.this[i].id
}
]
trustedRanges = var.trusted_ranges
vwanConfiguration = {
networkVirtualApplianceId = azurerm_palo_alto_virtual_network_appliance.this.id
vHub = {
resourceId = var.virtual_hub_id
}
}
}
panoramaConfig = {
configString = var.cloud_managed_type == "Panorama" ? var.panorama_config_string : ""
}
strataCloudManagerConfig = {
cloudManagerName = var.cloud_managed_type == "Strata" ? var.strata_cloud_manager_name : ""
}
marketplaceDetails = {
offerId = var.marketplace_details.offer_id
publisherId = var.marketplace_details.publisher_id
}
planData = {
billingCycle = var.plan_data.billing_cycle
planId = var.plan_data.plan_id
}
}
}
depends_on = [azurerm_user_assigned_identity.this, azurerm_palo_alto_virtual_network_appliance.this, azurerm_public_ip.egress_nat, azurerm_public_ip.this]
}
Variables
These variables below complete the full IaC codebase for the Palo Alto Cloud NGFW deployment and I used them to ensure the module for the deployment was repeatable and as generic as possible.
variable "resource_group_name" {
type = string
description = "The name of the Resource Group where the Palo Alto Cloud NGFW Firewall will be deployed."
}
variable "appliance_name" {
type = string
description = "The name which should be used for this Palo Alto Local Network Virtual Appliance. Changing this forces a new Palo Alto Local Network Virtual Appliance to be created."
}
variable "virtual_hub_id" {
type = string
description = "The ID of the Virtual Hub to deploy this appliance onto. Changing this forces a new Palo Alto Local Network Virtual Appliance to be created."
}
variable "firewall_name" {
type = string
description = "The name of the Palo Alto Cloud NGFW Firewall."
}
variable "location" {
type = string
description = "The Azure region where the Palo Alto Cloud NGFW Firewall will be deployed."
}
variable "tags" {
type = map(string)
description = "A map of tags to assign to the Palo Alto Cloud NGFW Firewall."
default = {}
}
variable "network_type" {
type = string
description = "The type of network to deploy the Palo Alto Cloud NGFW Firewall onto. Allowed values: 'VWAN' or 'VNET'. Defaults to VWAN."
default = "VWAN"
validation {
condition = contains(["VWAN", "VNET"], var.network_type)
error_message = "network_type must be either 'VWAN' or 'VNET'."
}
}
variable "enable_egress_nat" {
type = string
description = "Enable Egress NAT for the Palo Alto Cloud NGFW Firewall. Allowed values: 'ENABLED' or 'DISABLED'. Defaults to 'ENABLED'."
default = "ENABLED"
validation {
condition = contains(["ENABLED", "DISABLED"], var.enable_egress_nat)
error_message = "enable_egress_nat must be either 'ENABLED' or 'DISABLED'."
}
}
variable "egress_nat_ip_address_count" {
type = number
description = "The number of Egress NAT IP addresses to allocate for the firewall."
default = 1
}
variable "public_ip_address_count" {
type = number
description = "The number of Public IP addresses to allocate for the firewall."
default = 1
}
variable "dns_settings" {
type = object({
dns_servers = list(object({
address = string
resourceId = string
}))
enabled_dns_type = string
enable_dns_proxy = string
})
description = "DNS settings for the Palo Alto Cloud NGFW Firewall."
}
variable "trusted_ranges" {
type = list(string)
description = "List of NON-RFC 1918 trusted ranges for the Palo Alto Cloud NGFW Firewall."
}
variable "cloud_managed_type" {
type = string
description = "Is this appliance managed by Panorama or Strata Cloud Manager? Allowed values: 'Panorama' or 'Strata'."
validation {
condition = contains(["Panorama", "Strata"], var.cloud_managed_type)
error_message = "cloud_managed_type must be either 'Panorama' or 'Strata'."
}
}
variable "panorama_config_string" {
type = string
description = "Base64 encoded string representing Panorama parameters to be used by Firewall to connect to Panorama. This string is generated via azure plugin in Panorama"
default = ""
}
variable "strata_cloud_manager_name" {
type = string
description = "The name of the Strata Cloud Manager which is intended to manage the policy for this firewall."
default = ""
}
variable "marketplace_details" {
type = object({
offer_id = string # e.g. "pan_swfw_cloud_ngfw"
publisher_id = string # e.g. "paloaltonetworks"
})
description = "Marketplace details for the Palo Alto Cloud NGFW Firewall."
}
variable "plan_data" {
type = object({
billing_cycle = string # e.g. "MONTHLY"
plan_id = string # e.g. "panw-cloud-ngfw-payg"
})
description = "Plan data for the Palo Alto Cloud NGFW Firewall."
}
I hope you enjoyed reading & best of luck with your Terraforming!
Jesse