Palo Alto Automation with Terraform

Oh boy, as Network Engineers we have been learning so many new things lately, it's like we're in a never-ending loop. First, we had to learn Python then, we had to master Ansible. And now, on top of that, we're being told we need to learn Terraform too.

Overview

Terraform is an open-source infrastructure as code (IaC) tool that enables you to create, manage and version your infrastructure declaratively. It allows you to define your infrastructure as code using a high-level configuration language and then automates deploying and updating that infrastructure across various cloud providers, such as AWS, Azure, and Google Cloud Platform.

In addition to cloud resources, Terraform can also be used to automate network devices such as firewalls, routers, and switches. Terraform can interact with network devices' APIs and automate their configuration and deployment in a repeatable and consistent manner. This makes managing network infrastructure at scale easier, reducing manual errors and streamlining operations.

In this blog post, we will explore how Terraform can be used to automate Palo Alto firewalls, providing a scalable and repeatable way to manage security policies and configurations.

Prerequisites

Before we dive into automating Palo Alto firewalls with Terraform, it's important to note that this blog post assumes that you have prior knowledge of Palo Alto firewalls. Additionally, make sure that you have installed Terraform on your machine, and that you are familiar with its basic usage. With that out of the way, let's dive in.

Get the API Key

Terraform interact with the Firewalls/Panorama using the API key. If you don't have the key yet, you can generate one using the following command. Replace the firewall IP, username and password.

suresh@mac:~|⇒  curl -k -X GET 'https://<FIREWALL_IP>/api/?type=keygen&user=<USERNAME>&password=<PASSWORD>'

<response status = 'success'><result><key>LUFRPT1ueFpaNXFCUGM2ckxXNGFIeVRscVkrfgty5ldDN1pQMUxMN0FDRCtIa0tUczBSbHhlTDZtZWFUb1F4d3FhSnpRTVVwS3VsYXlxblODJFS2xWczUwL3FlRQ==</key></result></response>

Take note of the API Key you receive, it usually looks like the following.

LUFRPT1ueFpaNXFCUGM2ckxXNGFIeVRscVkrfgty5ldDN1pQMUxMN0FDRCtIa0tUczBSbHhlTDZtZWFUb1F4d3FhSnpRTVVwS3VsYXbg67DJFS2xWczUwL3FlRQ==

Working Directory and Files

To set up the environment, we will start by creating a directory called panos_terraform (can be any name).

Inside this directory, initially, we will create two files, provider.tf and panos-creds.json. The provider.tf file is used to specify the provider and its configuration details.

We will also define the credentials required to authenticate with the firewall in the panos-creds.json file. This file will contain the firewall's IP address and the API key, which Terraform will use to authenticate and interact with the firewall.

{
    "hostname": "10.10.10.10",
    "api_key": "LUFRPT1ad567uVphbVRESDlNUURkREcyVDV4anRSVFk9Wi9nWlNoTVJEK0hicWdUQVNJa2N2tYUjZZMHZ1U2lRNTFMQTAwckZ8zgt6wdU5HYcw==",
    "timeout": 10,
    "verify_certificate": false
}

panos-creds.json

terraform {
  required_providers {
    panos = {
      source = "PaloAltoNetworks/panos"
      version = "1.11.0"
    }
  }
}

provider "panos" {
  hostname = "10.10.10.10"
  json_config_file = "panos-creds.json"
}

provider.tf

What are we configuring?

For this example, I'm going to configure two interfaces, two zones, some address objects and two security policies. Let's put some data into a variable file called variables.tf

Variable File

The variables.tf file is used in Terraform to define input variables that can be used across multiple files within the same Terraform configuration. By defining variables in a separate file, you can easily modify and update them without changing your main configuration code.

variable interfaces {
    default = {
    "ethernet1/1" = { ip_address = "10.10.1.1/24", zone = "USERS", comment = "user traffic" }
    "ethernet1/10" = { ip_address = "185.10.10.1/30", zone = "OUTSIDE", comment = "internet traffic" }
    }
}

variable address_objects{
    default = {
        user_subnet = "192.168.10.0/24",
        dns_server = "8.8.8.8/32"
    }
}

variables.tf

This variables.tf file defines two input variables - interfaces and address_objects.

The interfaces variable is a map type variable that contains the configuration details for the firewall interfaces and zones. It has a default value which is a map containing two key-value pairs. Each key represents the name of the interface and the value is another map that defines the configuration details for that interface. In this case, the ethernet1/1 interface is configured with an IP address of 10.10.1.1/24, assigned to the USERS zone and labelled with a comment of "user traffic". The ethernet1/10 interface is configured with an IP address of 185.10.10.1/30, assigned to the OUTSIDE zone and labelled with a comment of "internet traffic".

The address_objects variable is also a map type variable that contains the configuration details for the firewall address objects. It has a default value that is a map containing two key-value pairs. Each key represents the name of the address object, and the value is the IP address or subnet associated with that address object. In this case, there are two address objects defined - user_subnet with an IP address of 192.168.10.0/24 and dns_server with an IP address of 8.8.8.8/32. These address objects will be used to simplify the configuration of security policies in the next step.

Configuration File

In Terraform, main.tf is the main configuration file where you define the resources and their configurations that you want to create or manage. It is the entry point for your Terraform configuration, and it contains the core infrastructure as code logic. In this file, you specify the desired state of your infrastructure and the resources you want to provision, as well as any dependencies or variables that are required.

In short, main.tf is where you define your desired infrastructure and how you want Terraform to manage it.

💡
Please note that I'm configuring Panorama. If you want to configure a firewall directly, just remove the template and device_group parameters from the below file.
resource "panos_device_group" "dg" {
    name = "test_lab_dg"
    description = "Test Lab"
}

resource "panos_panorama_template" "tp" {
    name = "test_lab_tp"
    description = "Test Lab"
}

resource "panos_panorama_ethernet_interface" "ports" {
    template = panos_panorama_template.tp.name
    for_each = var.interfaces
    name = each.key
    mode = "layer3"
    static_ips = [each.value.ip_address]
    comment = each.value.comment
}

resource "panos_zone" "zones" {
  template   = panos_panorama_template.tp.name
  for_each   = var.interfaces
  name       = each.value.zone
  mode       = "layer3"
  interfaces = [
    for k, v in var.interfaces :
    k if v.zone == each.value.zone
  ]
  depends_on = [panos_panorama_ethernet_interface.ports]
}

resource "panos_address_object" "objects" {
    device_group   = panos_device_group.dg.name
    for_each = var.address_objects
    name = each.key
    value = each.value
    lifecycle {
        create_before_destroy = true
    }
}

resource "panos_security_policy" "rule1" {
    device_group   = panos_device_group.dg.name
    rule {
        name = "users-to-internet"
        source_zones = ["USERS"]
        source_addresses = ["user_subnet"]
        source_users = ["any"]
        destination_zones = ["OUTSIDE"]
        destination_addresses = ["any"]
        applications = ["ssl"]
        services = ["application-default"]
        categories = ["any"]
        action = "allow"
    }
    depends_on = [panos_address_object.objects]

    lifecycle {
        create_before_destroy = true
    }
}

resource "panos_security_policy" "rule2" {
    device_group   = panos_device_group.dg.name
    rule {
        name = "Allow-DNS"
        source_zones = ["USERS"]
        source_addresses = ["user_subnet"]
        source_users = ["any"]
        destination_zones = ["OUTSIDE"]
        destination_addresses = ["dns_server"]
        applications = ["dns"]
        services = ["application-default"]
        categories = ["any"]
        action = "allow"
    }
    depends_on = [panos_address_object.objects]

    lifecycle {
        create_before_destroy = true
    }
}

main.tf

Let's break down each resource block.

  • panos_device_group - Creates a device group on the Palo Alto firewall with the name test_lab_dg and description Test Lab.
  • panos_panorama_template - Creates a Panorama template with the name test_lab_tp and description Test Lab.
  • panos_panorama_ethernet_interface - Creates a layer 3 ethernet interface for each key-value pair in the interfaces variable defined in variables.tf. The template parameter specifies the Panorama template to use, and for_each parameter is used to loop over each interface in the interfaces variable. The static_ips parameter sets the IP address for each interface, and the comment parameter sets the interface description.
  • panos_zone - Creates a zone for each unique zone value in the interfaces variable. The template parameter specifies the Panorama template to use, and the for_each parameter is used to loop over each zone in the interfaces variable. The interfaces parameter associates the interface names with each zone, based on the zone value in the interfaces variable. This ensures that each interface is assigned to the correct zone.
  • panos_address_object - Creates address objects for each key-value pair in the address_objects variable defined in variables.tf. These address objects can be used to simplify the configuration of security policies.
  • panos_security_policy - creates two security policies to allow traffic from the user_subnet to the internet and allow DNS traffic to the dns_server. The rule parameter specifies the security policy rule and its parameters. The depends_on parameter ensures that the panos_address_object resources are created before the security policies, as they are required to set the source and destination addresses in the policy rules.
  • The lifecycle parameter is used to set the create_before_destroy parameter to true, ensuring that new security policies are created before the old ones are destroyed, and minimizing downtime during updates.

Resource Creation

There are four main Terraform commands that you need to know about that are terraform init, terraform plan, terraform apply and terraform destroy. These commands are the core building blocks of the Terraform workflow and are used to initialize, plan, apply and destroy infrastructure changes respectively.

terraform init is used to set up the Terraform environment, including downloading provider plugins and setting up the backend configuration.

terraform plan is used to preview the changes that will be made to the infrastructure based on the current Terraform configuration, while terraform apply is used to apply those changes and create, modify or delete resources.

Finally, terraform destroy is used to remove all the resources that were created by Terraform, ensuring that the infrastructure is returned to its original state. Together, these commands provide a powerful way to manage infrastructure as code, allowing you to automate your infrastructure deployments and make them more scalable, reliable, and maintainable.

For brevity, I will just show you the output of terraform apply and terraform destroy.

Terraform apply

Terraform apply says it is going to create 10 resources as shown below.

suresh@mac:~/Documents/panos_terraform|blog⚡ ⇒  terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # panos_address_object.objects["dns_server"] will be created
  + resource "panos_address_object" "objects" {
      + device_group = "test_lab_dg"
      + id           = (known after apply)
      + name         = "dns_server"
      + type         = "ip-netmask"
      + value        = "8.8.8.8/32"
      + vsys         = "vsys1"
    }

  # panos_address_object.objects["user_subnet"] will be created
  + resource "panos_address_object" "objects" {
      + device_group = "test_lab_dg"
      + id           = (known after apply)
      + name         = "user_subnet"
      + type         = "ip-netmask"
      + value        = "192.168.10.0/24"
      + vsys         = "vsys1"
    }

  # panos_device_group.dg will be created
  + resource "panos_device_group" "dg" {
      + description = "Test Lab"
      + id          = (known after apply)
      + name        = "test_lab_dg"
    }

  # panos_panorama_ethernet_interface.ports["ethernet1/1"] will be created
  + resource "panos_panorama_ethernet_interface" "ports" {
      + comment    = "user traffic"
      + id         = (known after apply)
      + mode       = "layer3"
      + name       = "ethernet1/1"
      + static_ips = [
          + "10.10.1.1/24",
        ]
      + template   = "test_lab_tp"
      + vsys       = "vsys1"
    }

  # panos_panorama_ethernet_interface.ports["ethernet1/10"] will be created
  + resource "panos_panorama_ethernet_interface" "ports" {
      + comment    = "internet traffic"
      + id         = (known after apply)
      + mode       = "layer3"
      + name       = "ethernet1/10"
      + static_ips = [
          + "185.10.10.1/30",
        ]
      + template   = "test_lab_tp"
      + vsys       = "vsys1"
    }

  # panos_panorama_template.tp will be created
  + resource "panos_panorama_template" "tp" {
      + default_vsys = (known after apply)
      + description  = "Test Lab"
      + id           = (known after apply)
      + name         = "test_lab_tp"
    }

  # panos_security_policy.rule1 will be created
  + resource "panos_security_policy" "rule1" {
      + device_group = "test_lab_dg"
      + id           = (known after apply)
      + rulebase     = "pre-rulebase"
      + vsys         = "vsys1"

      + rule {
          + action                = "allow"
          + applications          = [
              + "ssl",
            ]
          + categories            = [
              + "any",
            ]
          + destination_addresses = [
              + "any",
            ]
          + destination_zones     = [
              + "OUTSIDE",
            ]
          + log_end               = true
          + name                  = "users-to-internet"
          + services              = [
              + "application-default",
            ]
          + source_addresses      = [
              + "user_subnet",
            ]
          + source_users          = [
              + "any",
            ]
          + source_zones          = [
              + "USERS",
            ]
          + type                  = "universal"
          + uuid                  = (known after apply)
        }
    }

  # panos_security_policy.rule2 will be created
  + resource "panos_security_policy" "rule2" {
      + device_group = "test_lab_dg"
      + id           = (known after apply)
      + rulebase     = "pre-rulebase"
      + vsys         = "vsys1"

      + rule {
          + action                = "allow"
          + applications          = [
              + "dns",
            ]
          + categories            = [
              + "any",
            ]
          + destination_addresses = [
              + "dns_server",
            ]
          + destination_zones     = [
              + "OUTSIDE",
            ]
          + log_end               = true
          + name                  = "Allow-DNS"
          + services              = [
              + "application-default",
            ]
          + source_addresses      = [
              + "user_subnet",
            ]
          + source_users          = [
              + "any",
            ]
          + source_zones          = [
              + "USERS",
            ]
          + type                  = "universal"
          + uuid                  = (known after apply)
        }
    }

  # panos_zone.zones["ethernet1/1"] will be created
  + resource "panos_zone" "zones" {
      + id         = (known after apply)
      + interfaces = [
          + "ethernet1/1",
        ]
      + mode       = "layer3"
      + name       = "USERS"
      + template   = "test_lab_tp"
      + vsys       = "vsys1"
    }

  # panos_zone.zones["ethernet1/10"] will be created
  + resource "panos_zone" "zones" {
      + id         = (known after apply)
      + interfaces = [
          + "ethernet1/10",
        ]
      + mode       = "layer3"
      + name       = "OUTSIDE"
      + template   = "test_lab_tp"
      + vsys       = "vsys1"
    }

Plan: 10 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

panos_panorama_template.tp: Creating...
panos_device_group.dg: Creating...
panos_device_group.dg: Creation complete after 1s [id=test_lab_dg]
panos_address_object.objects["user_subnet"]: Creating...
panos_address_object.objects["dns_server"]: Creating...
panos_panorama_template.tp: Creation complete after 1s [id=test_lab_tp]
panos_panorama_ethernet_interface.ports["ethernet1/10"]: Creating...
panos_panorama_ethernet_interface.ports["ethernet1/1"]: Creating...
panos_address_object.objects["user_subnet"]: Creation complete after 1s [id=test_lab_dg:user_subnet]
panos_address_object.objects["dns_server"]: Creation complete after 1s [id=test_lab_dg:dns_server]
panos_security_policy.rule1: Creating...
panos_security_policy.rule2: Creating...
panos_panorama_ethernet_interface.ports["ethernet1/1"]: Creation complete after 2s [id=test_lab_tp::vsys1:ethernet1/1]
panos_panorama_ethernet_interface.ports["ethernet1/10"]: Creation complete after 2s [id=test_lab_tp::vsys1:ethernet1/10]
panos_zone.zones["ethernet1/1"]: Creating...
panos_zone.zones["ethernet1/10"]: Creating...
panos_security_policy.rule1: Creation complete after 2s [id=test_lab_dg:pre-rulebase:vsys1]
panos_zone.zones["ethernet1/10"]: Creation complete after 1s [id=test_lab_tp::vsys1:OUTSIDE]
panos_zone.zones["ethernet1/1"]: Creation complete after 1s [id=test_lab_tp::vsys1:USERS]
panos_security_policy.rule2: Creation complete after 2s [id=test_lab_dg:pre-rulebase:vsys1]

Terraform destroy

In the context of the blog post, terraform destroy would remove all the security policies, address objects, zones, interfaces, device groups, and templates that were created in the previous step. This command can be executed after the terraform apply command to clean up the resources. This allows you to easily manage your infrastructure lifecycle and keep your environments consistent and clean.

suresh@mac:~/Documents/panos_terraform|blog⚡ ⇒  terraform destroy
panos_device_group.dg: Refreshing state... [id=test_lab_dg]
panos_panorama_template.tp: Refreshing state... [id=test_lab_tp]
panos_panorama_ethernet_interface.ports["ethernet1/10"]: Refreshing state... [id=test_lab_tp::vsys1:ethernet1/10]
panos_panorama_ethernet_interface.ports["ethernet1/1"]: Refreshing state... [id=test_lab_tp::vsys1:ethernet1/1]
panos_address_object.objects["dns_server"]: Refreshing state... [id=test_lab_dg:dns_server]
panos_address_object.objects["user_subnet"]: Refreshing state... [id=test_lab_dg:user_subnet]
panos_security_policy.rule2: Refreshing state... [id=test_lab_dg:pre-rulebase:vsys1]
panos_security_policy.rule1: Refreshing state... [id=test_lab_dg:pre-rulebase:vsys1]
panos_zone.zones["ethernet1/1"]: Refreshing state... [id=test_lab_tp::vsys1:USERS]
panos_zone.zones["ethernet1/10"]: Refreshing state... [id=test_lab_tp::vsys1:OUTSIDE]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # panos_address_object.objects["dns_server"] will be destroyed
  - resource "panos_address_object" "objects" {
      - device_group = "test_lab_dg" -> null
      - id           = "test_lab_dg:dns_server" -> null
      - name         = "dns_server" -> null
      - tags         = [] -> null
      - type         = "ip-netmask" -> null
      - value        = "8.8.8.8/32" -> null
      - vsys         = "vsys1" -> null
    }

  # panos_address_object.objects["user_subnet"] will be destroyed
  - resource "panos_address_object" "objects" {
      - device_group = "test_lab_dg" -> null
      - id           = "test_lab_dg:user_subnet" -> null
      - name         = "user_subnet" -> null
      - tags         = [] -> null
      - type         = "ip-netmask" -> null
      - value        = "192.168.10.0/24" -> null
      - vsys         = "vsys1" -> null
    }

  # panos_device_group.dg will be destroyed
  - resource "panos_device_group" "dg" {
      - description = "Test Lab" -> null
      - id          = "test_lab_dg" -> null
      - name        = "test_lab_dg" -> null
    }

  # panos_panorama_ethernet_interface.ports["ethernet1/1"] will be destroyed
  - resource "panos_panorama_ethernet_interface" "ports" {
      - adjust_tcp_mss                  = false -> null
      - comment                         = "user traffic" -> null
      - create_dhcp_default_route       = false -> null
      - decrypt_forward                 = false -> null
      - dhcp_default_route_metric       = 0 -> null
      - dhcp_send_hostname_enable       = false -> null
      - enable_dhcp                     = false -> null
      - id                              = "test_lab_tp::vsys1:ethernet1/1" -> null
      - ipv4_mss_adjust                 = 0 -> null
      - ipv6_enabled                    = false -> null
      - ipv6_mss_adjust                 = 0 -> null
      - lacp_ha_passive_pre_negotiation = false -> null
      - lacp_port_priority              = 0 -> null
      - lldp_enabled                    = false -> null
      - lldp_ha_passive_pre_negotiation = false -> null
      - mode                            = "layer3" -> null
      - mtu                             = 0 -> null
      - name                            = "ethernet1/1" -> null
      - rx_policing_rate                = 0 -> null
      - static_ips                      = [
          - "10.10.1.1/24",
        ] -> null
      - template                        = "test_lab_tp" -> null
      - tx_policing_rate                = 0 -> null
      - vsys                            = "vsys1" -> null
    }

  # panos_panorama_ethernet_interface.ports["ethernet1/10"] will be destroyed
  - resource "panos_panorama_ethernet_interface" "ports" {
      - adjust_tcp_mss                  = false -> null
      - comment                         = "internet traffic" -> null
      - create_dhcp_default_route       = false -> null
      - decrypt_forward                 = false -> null
      - dhcp_default_route_metric       = 0 -> null
      - dhcp_send_hostname_enable       = false -> null
      - enable_dhcp                     = false -> null
      - id                              = "test_lab_tp::vsys1:ethernet1/10" -> null
      - ipv4_mss_adjust                 = 0 -> null
      - ipv6_enabled                    = false -> null
      - ipv6_mss_adjust                 = 0 -> null
      - lacp_ha_passive_pre_negotiation = false -> null
      - lacp_port_priority              = 0 -> null
      - lldp_enabled                    = false -> null
      - lldp_ha_passive_pre_negotiation = false -> null
      - mode                            = "layer3" -> null
      - mtu                             = 0 -> null
      - name                            = "ethernet1/10" -> null
      - rx_policing_rate                = 0 -> null
      - static_ips                      = [
          - "185.10.10.1/30",
        ] -> null
      - template                        = "test_lab_tp" -> null
      - tx_policing_rate                = 0 -> null
      - vsys                            = "vsys1" -> null
    }

  # panos_panorama_template.tp will be destroyed
  - resource "panos_panorama_template" "tp" {
      - default_vsys = "vsys1" -> null
      - description  = "Test Lab" -> null
      - id           = "test_lab_tp" -> null
      - name         = "test_lab_tp" -> null
    }

  # panos_security_policy.rule1 will be destroyed
  - resource "panos_security_policy" "rule1" {
      - device_group = "test_lab_dg" -> null
      - id           = "test_lab_dg:pre-rulebase:vsys1" -> null
      - rulebase     = "pre-rulebase" -> null
      - vsys         = "vsys1" -> null

      - rule {
          - action                             = "allow" -> null
          - applications                       = [
              - "ssl",
            ] -> null
          - categories                         = [
              - "any",
            ] -> null
          - destination_addresses              = [
              - "any",
            ] -> null
          - destination_devices                = [] -> null
          - destination_zones                  = [
              - "OUTSIDE",
            ] -> null
          - disable_server_response_inspection = false -> null
          - disabled                           = false -> null
          - hip_profiles                       = [] -> null
          - icmp_unreachable                   = false -> null
          - log_end                            = true -> null
          - log_start                          = false -> null
          - name                               = "users-to-internet" -> null
          - negate_destination                 = false -> null
          - negate_source                      = false -> null
          - negate_target                      = false -> null
          - services                           = [
              - "application-default",
            ] -> null
          - source_addresses                   = [
              - "user_subnet",
            ] -> null
          - source_devices                     = [] -> null
          - source_users                       = [
              - "any",
            ] -> null
          - source_zones                       = [
              - "USERS",
            ] -> null
          - tags                               = [] -> null
          - type                               = "universal" -> null
          - uuid                               = "21b4309f-59b8-4c48-a4d8-de71b4d18193" -> null
        }
    }

  # panos_security_policy.rule2 will be destroyed
  - resource "panos_security_policy" "rule2" {
      - device_group = "test_lab_dg" -> null
      - id           = "test_lab_dg:pre-rulebase:vsys1" -> null
      - rulebase     = "pre-rulebase" -> null
      - vsys         = "vsys1" -> null

      - rule {
          - action                             = "allow" -> null
          - applications                       = [
              - "ssl",
            ] -> null
          - categories                         = [
              - "any",
            ] -> null
          - destination_addresses              = [
              - "any",
            ] -> null
          - destination_devices                = [] -> null
          - destination_zones                  = [
              - "OUTSIDE",
            ] -> null
          - disable_server_response_inspection = false -> null
          - disabled                           = false -> null
          - hip_profiles                       = [] -> null
          - icmp_unreachable                   = false -> null
          - log_end                            = true -> null
          - log_start                          = false -> null
          - name                               = "users-to-internet" -> null
          - negate_destination                 = false -> null
          - negate_source                      = false -> null
          - negate_target                      = false -> null
          - services                           = [
              - "application-default",
            ] -> null
          - source_addresses                   = [
              - "user_subnet",
            ] -> null
          - source_devices                     = [] -> null
          - source_users                       = [
              - "any",
            ] -> null
          - source_zones                       = [
              - "USERS",
            ] -> null
          - tags                               = [] -> null
          - type                               = "universal" -> null
          - uuid                               = "21b4309f-59b8-4c48-a4d8-de71b4d18193" -> null
        }
    }

  # panos_zone.zones["ethernet1/1"] will be destroyed
  - resource "panos_zone" "zones" {
      - enable_user_id = false -> null
      - exclude_acls   = [] -> null
      - id             = "test_lab_tp::vsys1:USERS" -> null
      - include_acls   = [] -> null
      - interfaces     = [
          - "ethernet1/1",
        ] -> null
      - mode           = "layer3" -> null
      - name           = "USERS" -> null
      - template       = "test_lab_tp" -> null
      - vsys           = "vsys1" -> null
    }

  # panos_zone.zones["ethernet1/10"] will be destroyed
  - resource "panos_zone" "zones" {
      - enable_user_id = false -> null
      - exclude_acls   = [] -> null
      - id             = "test_lab_tp::vsys1:OUTSIDE" -> null
      - include_acls   = [] -> null
      - interfaces     = [
          - "ethernet1/10",
        ] -> null
      - mode           = "layer3" -> null
      - name           = "OUTSIDE" -> null
      - template       = "test_lab_tp" -> null
      - vsys           = "vsys1" -> null
    }

Plan: 0 to add, 0 to change, 10 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

panos_zone.zones["ethernet1/10"]: Destroying... [id=test_lab_tp::vsys1:OUTSIDE]
panos_zone.zones["ethernet1/1"]: Destroying... [id=test_lab_tp::vsys1:USERS]
panos_security_policy.rule2: Destroying... [id=test_lab_dg:pre-rulebase:vsys1]
panos_security_policy.rule1: Destroying... [id=test_lab_dg:pre-rulebase:vsys1]
panos_zone.zones["ethernet1/10"]: Destruction complete after 1s
panos_zone.zones["ethernet1/1"]: Destruction complete after 1s
panos_panorama_ethernet_interface.ports["ethernet1/10"]: Destroying... [id=test_lab_tp::vsys1:ethernet1/10]
panos_panorama_ethernet_interface.ports["ethernet1/1"]: Destroying... [id=test_lab_tp::vsys1:ethernet1/1]
panos_security_policy.rule1: Destruction complete after 1s
panos_security_policy.rule2: Destruction complete after 1s
panos_address_object.objects["user_subnet"]: Destroying... [id=test_lab_dg:user_subnet]
panos_address_object.objects["dns_server"]: Destroying... [id=test_lab_dg:dns_server]
panos_address_object.objects["dns_server"]: Destruction complete after 1s
panos_panorama_ethernet_interface.ports["ethernet1/1"]: Destruction complete after 1s
panos_panorama_ethernet_interface.ports["ethernet1/10"]: Destruction complete after 1s
panos_address_object.objects["user_subnet"]: Destruction complete after 1s
panos_panorama_template.tp: Destroying... [id=test_lab_tp]
panos_device_group.dg: Destroying... [id=test_lab_dg]
panos_panorama_template.tp: Destruction complete after 1s
panos_device_group.dg: Destruction complete after 1s

Destroy complete! Resources: 10 destroyed.

Closing up

In this blog post, we explored how Terraform can be used to configure Palo Alto firewalls, create security policies, address objects, zones, and interfaces. By defining the infrastructure as code, we can easily manage and update the network configuration in a repeatable and scalable way. With the terraform init, terraform plan, terraform apply, and terraform destroy commands, we can automate the entire infrastructure lifecycle, from initialization to clean-up.