diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9edb978
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+.terraform.lock.hcl
+.terraform
+.terraform.tfstate.lock.info
+terraform.tfstate
+terraform.tfstate.backup
+terraform.tfvars
+bootstrap.xml
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0375682
--- /dev/null
+++ b/README.md
@@ -0,0 +1,529 @@
+# Secure IPv4 & IPv6 with VM-Series on Google Cloud
+
+This tutorial shows how to deploy and configure Palo Alto Networks VM-Series to secure IPv4 and IPv6 traffic on Google Cloud.
+
+This guide is intended for network administrators, solution architects, and security professionals who are very familiar with [Compute Engine](https://cloud.google.com/compute) and [Virtual Private Cloud (VPC) networking](https://cloud.google.com/vpc).
+
+>[!WARNING]
+>IPv6 support for VM-Series on Google Cloud is currently not supported. This open-source tutorial represents a best-effort to demonstrate how VM-Series secures IPv6 traffic on Google Cloud.
+
+## Requirements
+
+The following are required for this tutorial:
+
+1. A Google Cloud project.
+2. Access to Google Cloud Shell to deploy the resources.
+3. If using BYOL, an VM-Series authkey to license the firewall.
+
+## Architecture
+
+The diagram shows the resources created with Terraform.
+
+
+
+The VM-Series has 3 network interfaces, each belonging to a dual-stack subnet in separate VPC networks. The VM-Series is deployed to an unmanaged instance group which is a backend service of an external pass-through load balancer. The load balancer is configured with IPv4 and IPv6 frontend addresses to distribute internet inbound traffic to the VM-Series for inspection.
+
+Test workloads are deployed to test north/south traffic. The `external-vm` will be used to test internet inbound traffic through the VM-Series to the `internal-vm` in the trust network.
+
+>[!CAUTION]
+>At the time of this writing, IPv6 traffic cannot be routed to an internal load balancer as the next hop.
+
+
+## Prepare for Deployment
+
+On your local machine or in Google Cloud Shell, perform the following.
+
+1. Enable the required APIs, generate an SSH key, and clone the repository.
+
+ ```
+ gcloud services enable compute.googleapis.com
+ git clone https://github.com/PaloAltoNetworks/google-cloud-vmseries-ipv6-tutorial
+ cd google-cloud-vmseries-ipv6-tutorial
+ ```
+
+2. Create an SSH key to assign to the GCE instances created.
+
+ ```
+ ssh-keygen -f ~/.ssh/vmseries-tutorial -t rsa
+ ```
+
+3. Create a `terraform.tfvars`.
+
+ ```
+ cp terraform.tfvars.example terraform.tfvars
+ ```
+
+4. Edit the `terraform.tfvars` file and set values for the following variables:
+
+
+ | Key | Value | Default |
+ | ----------------------- | ------------------------------------------------------------------------------------ | ------------------------------ |
+ | `project_id` | The Project ID within Google Cloud. | `null` |
+ | `public_key_path` | The local path of the public key you previously created | `~/.ssh/vmseries-tutorial.pub` |
+ | `mgmt_allow_ips` | A list of IPv4 addresses that can have access to the VM-Series management interface. | `["0.0.0.0/0"]` |
+ | `create_test_vms` | Set to `false` if you do not want to create the test VMs. | `true` |
+ | `vmseries_image_name` | Set to the VM-Series image you want to deploy. | `vmseries-flex-bundle1-1102` |
+
+1. Save your `terraform.tfvars` file.
+
+
+## Deployment
+When no further changes are necessary, deploy the resources:
+
+1. Initialize and apply the Terraform plan.
+
+ ```
+ terraform init
+ terraform apply
+ ```
+
+2. Enter `yes` to start the deployment.
+
+3. After the resources are created, Terraform displays the following message:
+
+ ```
+ Apply complete!
+
+ Outputs:
+
+ EXTLB_IPv4 = "1.2.3.4/32"
+ EXTLB_IPv6 = "2600:1900:4000:eba6:8000::/32"
+ SSH_INTERNAL_VM = "gcloud compute ssh paloalto@internal-vm --zone=us-central1-a"
+ SSH_EXTERNAL_VM = "gcloud compute ssh paloalto@external-vm --zone=us-central1-a"
+ VMSERIES_CLI = "ssh admin@1.1.1.1 -i ~/.ssh/vmseries-tutorial"
+ VMSERIES_GUI = "https://1.1.1.1"
+ ```
+
+### Accessing the VM-Series firewall
+
+To access the VM-Series user interface, a password must be set for the `admin` user.
+
+> [!NOTE]
+> It may take an additional 10 minutes for the VM-Series to be accessible.
+
+1. Use the `VMSERIES_CLI` output to access the VM-Series CLI.
+
+ ```
+ ssh admin@1.1.1.1 -i ~/.ssh/vmseries-tutorial
+ ```
+
+
+2. On the VM-Series, set a password for the `admin` username.
+
+ ```
+ configure
+ set mgt-config users admin password
+ ```
+
+3. Commit the changes.
+
+ ```
+ commit
+ ```
+
+5. Enter `exit` twice to terminate the session.
+
+6. In a browser, use the `VMSERIES_GUI` output to access the VM-Series.
+
+
+
+## Outbound IPv4/IPv6 Traffic Configuration
+
+In this step, retrieve the required network parameters and apply them to the VM-Series configuration.
+
+> [!TIP]
+> DHCPv6 is available in PAN-OS 11.0 and eliminates the need to configure static IPv6 addresses.
+
+### Configure Interfaces
+
+Enable DHCPv4 and DHCPv6 on the VM-Series network interfaces to handle IPv4/IPv6 traffic.
+
+1. On the VM-Series, go to **Network → Zones**. Click **Add**.
+
+2. Create two zones: `untrust` & `trust`.
+
+
+
+3. Go to **Network → Interfaces → Ethernet**.
+
+4. Configure `ethernet1/1` (`untrust`) as follows:
+
+
+
+ > In IPv4 tab, **check** `Automatically create default route`.
+ > In IPv6 tab, **check** `Accept Router Advertised Route` and **uncheck** `Enable Prefix Delegation`.
+
+5. Configure `ethernet1/2` (`trust`) as follows:
+
+
+
+ > In IPv4 tab, **uncheck** `Automatically create default route`.
+ > In IPv6 tab, **uncheck** `Accept Router Advertised Route` and **uncheck** `Enable Prefix Delegation`.
+
+6. **Commit the changes.**
+
+
+### Retrieve IPv6 Parameters
+
+Retrieve the default gateways for the untrust & trust subnets and the ULA for the trust VPC.
+
+1. On `ethernet1/1`, click **Dynamic-DHCPv6 Client**.
+
+2. Record the **Server** and **IPv6 Address (Non-Temporary)** addresses.
+
+
+
+ > **Server** address is the IPv6 default gateway for the untrust network.
+ > **IPv6 Address** is the external IPv6 address assigned to the untrust interface.
+
+3. On `ethernet1/2`, click **Dynamic-DHCPv6 Client**.
+
+4. Record the **Server** address.
+
+
+
+ > **Server** address is the IPv6 default gateway of the trust network.
+
+5. In to Google Cloud, go to **VPC Networks →** `trust-vpc`.
+
+6. Record the **VPC network ULA internal IPv6 range**.
+
+
+
+ > The ULA covers all of the possible IPv6 prefixes within the trust VPC.
+
+### Configure Virtual Router
+
+On the VM-Series, create an IPv4 & IPv6 routes to correctly return traffic to the trust VPC.
+
+1. Go to **Network → Virtual Routers**. Select the `default` virtual router.
+
+2. Click **Static Routes → IPv4**. Click **+ Add**.
+
+3. Configure the IPv4 return route as follows:
+
+
+
+4. Click **Static Routes → IPv6**. Click **+ Add**.
+
+5. Configure the IPv6 return route as follows:
+
+
+
+ | | IPv4 Route | IPv6 Route |
+ |--------------------|-------------------------------------|------------------------------|
+ | **Name** | `ipv4-trust` | `ipv6-trust` |
+ | **Destination** | `IPv4 CIDR of trust network` | `ULA range of trust VPC` |
+ | **Next Hop** | `IP Address` | `IPv6 Address` |
+ | **Next Hop Value** | `eth1/2 IPv4 gateway IP` | `eth1/2 IPv6 Server Address` |
+
+6. Click **OK**.
+
+
+
+### Configure IPv4/IPv6 NAT Policies for Outbound Traffic
+
+Create a NAT rule to translate trust VPC traffic to the external IPv4/v6 addresses attached to the untrust interface.
+
+1. Go to **Policies → NAT**. Click **Add**.
+
+2. Create a NAT policy to translate outbound IPv4 traffic.
+
+
+
+3. Create a NPTv6 NAT policy to translate outbound IPv6 traffic.
+
+
+
+ >Set the **IPv6 Address (Non-Temporary)** IP on `eth1/1` as the translated address (use a `/96` prefix).
+
+### Create Security Policy
+
+For the purposes of this tutorial, create a security policy to allow `ping`, `ping6`, & `web-browsing`.
+
+>[!CAUTION]
+>This tutorial does not provide guidance on security policy implementation.
+
+
+1. Go to **Policies → Security**. Click **Add**.
+
+2. Configure the security policy to allow `ping`, `ping6`, & `web-browsing`.
+
+
+
+4. **Commit the changes**.
+
+5. In Cloud Shell, create default routes in the `trust-vpc` to steer IPv4/IPv6 traffic to the VM-Series trust interface for inspection.
+
+ ```
+ gcloud compute routes create ipv4-default \
+ --network=trust-vpc \
+ --destination-range=0.0.0.0/0 \
+ --next-hop-instance=vmseries \
+ --next-hop-instance-zone=us-central1-a
+
+ gcloud beta compute routes create ipv6-default \
+ --network=trust-vpc \
+ --destination-range=::0/0 \
+ --next-hop-instance=vmseries \
+ --next-hop-instance-zone=us-central1-a
+ ```
+
+
+
+
+
+### Test Outbound Internet Traffic
+
+Access the `internal-vm` in the trust network and generate outbound IPv4/IPv6 internet traffic.
+
+1. In Cloud Shell, SSH to the `internal-vm`.
+
+ ```
+ gcloud compute ssh paloalto@internal-vm --zone=us-central1-a
+ ```
+
+2. Ping an external IPv4 address to test outbound IPv4 traffic.
+
+ ```
+ ping 8.8.8.8
+ ```
+
+3. Ping an external IPv6 address to test outbound IPv6 traffic.
+
+ ```
+ ping6 2600::
+ ```
+
+4. On the VM-Series, go to **Monitor → Traffic**. Enter the filter below to search for the outbound traffic.
+
+ ```
+ ( app eq 'ping6' ) or ( app eq 'ping' )
+ ```
+
+
+
+ >You should see that IPv4 & IPv6 traffic from the `internal-vm` is translated correctly by the VM-Series.
+
+
+
+## Inbound IPv4/IPv6 Traffic Configuration
+In this section, you will configure the VM-Series to translate inbound internet traffic, which is distributed by an external pass-through load balancer, to reach the a web application running on the `internal-vm` in the trust VPC.
+
+>[!NOTE]
+>The Terraform plan creates an external load balancer and health check for you.
+
+
+### Configure Health Checks
+Setup a loopback interface to receive the load balancer's IPv4/IPv6 health checks. Then, create a NAT policy to translate IPv4 health checks to the IPv4 loopback address and create a security policy to allow the health checks.
+
+#### Configure loopback interface
+
+1. In Google Cloud, go to **Network Services → Load Balancers**.
+
+2. Click the `vmseries-extlb` load balancer. Record the IPv6 address assigned to the forwarding rule.
+
+
+
+3. On the VM-Series, go to **Network → Zones**. Click **Add**.
+
+4. Create a zone called `lb-checks`.
+
+
+
+5. Go to **Network → Network Profiles → Interface Mgmt**. click **Add**.
+
+6. Enable `HTTP` and add the [Health Check Ranges](https://cloud.google.com/load-balancing/docs/health-checks#fw-netlb) (`35.191.0.0/16`, `209.85.152.0/22`, `209.85.204.0/22`, `2600:1901:8001::/48`) as permitted addresses.
+
+
+
+7. Go to **Network → Interfaces → Loopback**. Click **Add**.
+
+8. In the **Config Tab**, set tunnel to `1`, **Virtual Router** to `default`, & **Zone** to `lb-checks`.
+
+
+
+9. In the **IPv4 Tab**, set `100.64.0.1/32` as the address.
+
+
+
+10. In the **IPv6 Tab**, set load balancer's IPv6 forwarding rule address.
+
+
+
+11. In the **Advanced Tab**, set the **Management Profile** to `lb-checks`
+
+
+
+
+#### Create NAT for IPv4 Health Checks
+
+1. Go to **Policies → NAT**. Click **Add**.
+
+2. Configure the policy to translate the IPv4 health check ranges to the IPv4 loopback address.
+
+
+
+#### Create Security Policy for IPv4/IPv6 Health Checks
+
+1. Go to **Policies → Security**. Click **Add**.
+
+2. Configure the policy to allow IPv4 & IPv6 health check ranges to the `lb-checks` zone.
+
+
+
+> [!Important]
+> Move the policy to the top of the rule set before committing the changes.
+
+3. **Commit the changes.**
+
+4. In Google Cloud, verify the health checks are up on the `vmseries-extlb`.
+
+
+
+
+### Configure NAT Policy for IPv4 Forwarding Rule
+
+Create a NAT policy to translate traffic destined to the IPv4 forwarding rule to a web app on the `internal-vm` in the trust VPC.
+
+1. In Google Cloud, record IPv4 & IPv6 addresses of the `internal-vm`.
+
+
+
+2. On the VM-Series, go to **Policies → NAT**. Click **Add**.
+
+3. Configure the policy to translate the IPv4 forwarding rule to the `internal-vm` IPv4 address.
+
+
+
+ | NAT Policy | | |
+ |------------------------|-----------------------|-----------------------------------------------|
+ | **Original Packet** | Source Zone | `untrust` |
+ | | Destination Zone | `untrust` |
+ | | Destination Interface | `ethernet1/1` |
+ | | Destination Address | `34.29.169.107` (IPv4 fowarding rule address) |
+ | **Source Translation** | Translation Type | `Dynamic IP and Port` |
+ | | Address Type | `Interface Address` |
+ | | Interface | `ethernet1/2` |
+ | **DST Translation** | Translation Type | `Dynamic IP` |
+ | | Translated Address | `10.0.3.10` (IPv4 of `internal-vm`) |
+
+
+> [!IMPORTANT]
+> When load balancing internet inbound traffic through multiple firewalls, source translation is necessary to ensure a synchronous response from the backend application.
+
+
+
+### Configure NPTv6 Policy for IPv6 Forwarding Rule
+Create an NPTv6 policy to translate traffic destined to the IPv6 forwarding rule to the web app on `internal-vm`.
+
+> [!NOTE]
+> NPTv6 performs stateless translation, moving traffic from one IPv6 prefix to another by eliminating the IPv6 header checksum.
+> Therefore, a checksum-neutral address must be calculated and used as the original packet's destination in the NPTv6 policy.
+
+#### Generate Checksum Neutral Address on VM-Series
+
+1. In Cloud Shell, SSH to the VM-Series using its management IP.
+
+ ```
+ ssh admin@1.1.1.1
+ ```
+
+2. Use the `test nptv6` command to generate the checksum for traffic between the IPv6 address of the `internal-vm` and the IPv6 forwarding rule address on the load balancer.
+
+ ```
+ test nptv6 cks-neutral source-ip fd20:eb0:af94:0:0:0:0:0 dest-network 2600:1900:4000:5db5:8000:1:0:0/96
+ ```
+
+ > Replace `fd20:eb0:af94:0:0:0:0:0` with the IPv6 address of your internal-vm and replace `2600:1900:4000:5db5:8000:1:0:0/96` with the IPv6 address assigned to your load balancer's forwarding rule.
+
+
+3. Record the generated checksum neutral address.
+
+ **(Output)**
+
+ The checksum neutral address of fd20:eb0:af94:: is 2600:1900:4000:5db5:8000:1:5eae:0 in 2600:1900:4000:5db5:8000:1:0:0/96 subnet
+
+
+
+#### Create NPTv6 Policy
+
+1. On the VM-Series, go to **Policies → NAT**. Click **Add**.
+
+2. Set **NAT Type** to `nptv6`.
+
+2. Configure the policy to translate the checksum IP to the `internal-vm` IPv6 address.
+
+
+
+ | NPTv6 Policy | | |
+ |------------------------|-----------------------|----------------------------------------------------------------|
+ | **Original Packet** | Source Zone | `untrust` |
+ | | Destination Zone | `untrust` |
+ | | Destination Interface | `ethernet1/1` |
+ | | Destination Address | `2600:1900:4000:5db5:8000:1:5eae:0` (checksum neutral address) |
+ | **DST Translation** | Translation Type | `Dynamic IP` |
+ | | Translated Address | `fd20:eb0:af94:0:0:0:0:0/96` (IPv6 of `internal-vm`) |
+
+
+
+
+
+### Test Inbound Internet Traffic
+
+Access the `external-vm` to test internet inbound traffic through the IPv4/IPv6 external load balancer to the web application on `internal-vm`.
+
+1. In Cloud Shell, SSH to the external VM.
+
+ ```
+ gcloud compute ssh paloalto@external-vm --zone=us-central1-a
+ ```
+
+2. Attempt to reach the web application using the load balancer's IPv4 address.
+
+ ```
+ curl http://34.29.169.107:80/?[1-3]
+ ```
+
+3. Attempt to reach the web application using the **checksum neutral** IPv6 address.
+
+ ```
+ curl -6 'http://[2600:1900:4000:5db5:8000:1:5eae:0]:80/?[1-3]'
+ ```
+
+4. On the VM-Series, go to **Monitor → Traffic**. Enter the filter below to search for the inbound traffic.
+
+ ```
+ ( zone.src eq 'untrust' ) and ( zone.dst eq 'trust' ) and ( app eq 'web-browsing' )
+ ```
+
+
+
+ > You should see that both IPv4 and IPv6 traffic is inspected and translated correctly by the VM-Series firewall.
+
+
+## Clean up
+
+1. To delete the created resources, run the commands below.
+
+ ```
+ gcloud compute routes delete ipv4-default -q
+ gcloud compute routes delete ipv6-default -q
+ terraform destroy
+ ```
+
+2. At the prompt to perform the actions, enter `yes`.
+
+ After all the resources are deleted, Terraform displays the following message:
+
+ ```
+ Destroy complete!
+ ```
+
+## Additional information
+
+* Learn about the[ VM-Series on Google Cloud](https://docs.paloaltonetworks.com/vm-series/10-2/vm-series-deployment/set-up-the-vm-series-firewall-on-google-cloud-platform/about-the-vm-series-firewall-on-google-cloud-platform).
+* Getting started with [Palo Alto Networks PAN-OS](https://docs.paloaltonetworks.com/pan-os).
+* Read about [securing Google Cloud Networks with the VM-Series](https://cloud.google.com/architecture/partners/palo-alto-networks-ngfw).
+* Learn about [VM-Series licensing on all platforms](https://docs.paloaltonetworks.com/vm-series/10-2/vm-series-deployment/license-the-vm-series-firewall/vm-series-firewall-licensing.html#id8fea514c-0d85-457f-b53c-d6d6193df07c).
+* Use the [VM-Series Terraform modules for Google Cloud](https://registry.terraform.io/modules/PaloAltoNetworks/vmseries-modules/google/latest).
diff --git a/images/diagram.png b/images/diagram.png
new file mode 100644
index 0000000..52eae63
Binary files /dev/null and b/images/diagram.png differ
diff --git a/images/image1.png b/images/image1.png
new file mode 100644
index 0000000..6d6a4f4
Binary files /dev/null and b/images/image1.png differ
diff --git a/images/image10.png b/images/image10.png
new file mode 100644
index 0000000..96400a4
Binary files /dev/null and b/images/image10.png differ
diff --git a/images/image11.png b/images/image11.png
new file mode 100644
index 0000000..a89df96
Binary files /dev/null and b/images/image11.png differ
diff --git a/images/image15.png b/images/image15.png
new file mode 100644
index 0000000..fc2e046
Binary files /dev/null and b/images/image15.png differ
diff --git a/images/image17.png b/images/image17.png
new file mode 100644
index 0000000..5611451
Binary files /dev/null and b/images/image17.png differ
diff --git a/images/image18.png b/images/image18.png
new file mode 100644
index 0000000..656f28d
Binary files /dev/null and b/images/image18.png differ
diff --git a/images/image19.png b/images/image19.png
new file mode 100644
index 0000000..b560dad
Binary files /dev/null and b/images/image19.png differ
diff --git a/images/image2.png b/images/image2.png
new file mode 100644
index 0000000..22aaa4f
Binary files /dev/null and b/images/image2.png differ
diff --git a/images/image20.png b/images/image20.png
new file mode 100644
index 0000000..8e43933
Binary files /dev/null and b/images/image20.png differ
diff --git a/images/image21.png b/images/image21.png
new file mode 100644
index 0000000..e6168f5
Binary files /dev/null and b/images/image21.png differ
diff --git a/images/image22.png b/images/image22.png
new file mode 100644
index 0000000..6977050
Binary files /dev/null and b/images/image22.png differ
diff --git a/images/image23.png b/images/image23.png
new file mode 100644
index 0000000..d7b2390
Binary files /dev/null and b/images/image23.png differ
diff --git a/images/image24.png b/images/image24.png
new file mode 100644
index 0000000..dbf2479
Binary files /dev/null and b/images/image24.png differ
diff --git a/images/image25.png b/images/image25.png
new file mode 100644
index 0000000..f97dfdf
Binary files /dev/null and b/images/image25.png differ
diff --git a/images/image26.png b/images/image26.png
new file mode 100644
index 0000000..d03603f
Binary files /dev/null and b/images/image26.png differ
diff --git a/images/image27.png b/images/image27.png
new file mode 100644
index 0000000..1b9df1e
Binary files /dev/null and b/images/image27.png differ
diff --git a/images/image28.png b/images/image28.png
new file mode 100644
index 0000000..8d05502
Binary files /dev/null and b/images/image28.png differ
diff --git a/images/image29.png b/images/image29.png
new file mode 100644
index 0000000..cad2853
Binary files /dev/null and b/images/image29.png differ
diff --git a/images/image3.png b/images/image3.png
new file mode 100644
index 0000000..8d6a3d3
Binary files /dev/null and b/images/image3.png differ
diff --git a/images/image30.png b/images/image30.png
new file mode 100644
index 0000000..cc7a2a2
Binary files /dev/null and b/images/image30.png differ
diff --git a/images/image4.png b/images/image4.png
new file mode 100644
index 0000000..df8e31e
Binary files /dev/null and b/images/image4.png differ
diff --git a/images/image5.png b/images/image5.png
new file mode 100644
index 0000000..5478603
Binary files /dev/null and b/images/image5.png differ
diff --git a/images/image6.png b/images/image6.png
new file mode 100644
index 0000000..396f36e
Binary files /dev/null and b/images/image6.png differ
diff --git a/images/image7.png b/images/image7.png
new file mode 100644
index 0000000..0f2bb7f
Binary files /dev/null and b/images/image7.png differ
diff --git a/images/image8.png b/images/image8.png
new file mode 100644
index 0000000..37ad221
Binary files /dev/null and b/images/image8.png differ
diff --git a/images/image9.png b/images/image9.png
new file mode 100644
index 0000000..17c4f10
Binary files /dev/null and b/images/image9.png differ
diff --git a/main.tf b/main.tf
new file mode 100644
index 0000000..abfac1e
--- /dev/null
+++ b/main.tf
@@ -0,0 +1,362 @@
+# --------------------------------------------------------------------------------------------
+# Provider configuration
+# --------------------------------------------------------------------------------------------
+
+terraform {}
+
+provider "google" {
+ project = local.project_id
+ region = local.region
+}
+
+data "google_compute_zones" "main" {}
+
+# --------------------------------------------------------------------------------------------
+# Local variables
+# --------------------------------------------------------------------------------------------
+
+locals {
+ project_id = var.project_id
+ region = var.region
+ vmseries_image = var.vmseries_image
+ public_key_path = var.public_key_path
+ mgmt_allow_ips = var.mgmt_allow_ips
+ prefix = var.prefix
+ subnet_cidr_mgmt = var.subnet_cidr_mgmt
+ subnet_cidr_untrust = var.subnet_cidr_untrust
+ subnet_cidr_untrust_lb = var.subnet_cidr_untrust_lb
+ subnet_cidr_trust = var.subnet_cidr_trust
+ subnet_cidr_web = var.subnet_cidr_web
+ subnet_cidr_external = var.subnet_cidr_external
+ create_test_vms = var.create_test_vms
+}
+
+# --------------------------------------------------------------------------------------------
+# Create VPC networks
+# --------------------------------------------------------------------------------------------
+
+resource "google_compute_network" "mgmt" {
+ name = "${local.prefix}mgmt-vpc"
+ routing_mode = "GLOBAL"
+ auto_create_subnetworks = false
+ enable_ula_internal_ipv6 = true
+}
+
+
+resource "google_compute_network" "untrust" {
+ name = "${local.prefix}untrust-vpc"
+ routing_mode = "GLOBAL"
+ auto_create_subnetworks = false
+ enable_ula_internal_ipv6 = true
+}
+
+
+resource "google_compute_network" "trust" {
+ name = "${local.prefix}trust-vpc"
+ routing_mode = "GLOBAL"
+ auto_create_subnetworks = false
+ enable_ula_internal_ipv6 = true
+ delete_default_routes_on_create = true
+}
+
+# --------------------------------------------------------------------------------------------
+# Create subnets
+# --------------------------------------------------------------------------------------------
+
+resource "google_compute_subnetwork" "mgmt" {
+ name = "${local.prefix}mgmt-subnet"
+ ip_cidr_range = local.subnet_cidr_mgmt
+ region = local.region
+ stack_type = "IPV4_IPV6"
+ ipv6_access_type = "EXTERNAL"
+ network = google_compute_network.mgmt.id
+}
+
+
+resource "google_compute_subnetwork" "untrust" {
+ name = "${local.prefix}untrust-subnet"
+ ip_cidr_range = local.subnet_cidr_untrust
+ region = local.region
+ stack_type = "IPV4_IPV6"
+ ipv6_access_type = "EXTERNAL"
+ network = google_compute_network.untrust.id
+}
+
+
+resource "google_compute_subnetwork" "untrust_lb" {
+ name = "${local.prefix}untrust-lb-subnet"
+ ip_cidr_range = local.subnet_cidr_untrust_lb
+ region = local.region
+ stack_type = "IPV4_IPV6"
+ ipv6_access_type = "EXTERNAL"
+ network = google_compute_network.untrust.id
+}
+
+
+resource "google_compute_subnetwork" "trust" {
+ name = "${local.prefix}trust-subnet"
+ ip_cidr_range = local.subnet_cidr_trust
+ region = local.region
+ stack_type = "IPV4_IPV6"
+ ipv6_access_type = "INTERNAL"
+ network = google_compute_network.trust.id
+}
+
+resource "google_compute_subnetwork" "web" {
+ name = "${local.prefix}web-subnet"
+ ip_cidr_range = local.subnet_cidr_web
+ region = local.region
+ stack_type = "IPV4_IPV6"
+ ipv6_access_type = "INTERNAL"
+ network = google_compute_network.trust.id
+}
+
+# --------------------------------------------------------------------------------------------
+# Create ingress firewall rules
+# --------------------------------------------------------------------------------------------
+
+resource "google_compute_firewall" "mgmt_ipv4" {
+ name = "${local.prefix}all-ingress-mgmt"
+ network = google_compute_network.mgmt.id
+ source_ranges = var.mgmt_allow_ips
+
+ allow {
+ protocol = "tcp"
+ ports = ["443", "22"]
+ }
+}
+
+
+resource "google_compute_firewall" "untrust_ipv4" {
+ name = "${local.prefix}all-ingress-untrust"
+ network = google_compute_network.untrust.id
+ source_ranges = ["0.0.0.0/0"]
+
+ allow {
+ protocol = "all"
+ }
+}
+
+
+resource "google_compute_firewall" "untrust_ipv6" {
+ name = "${local.prefix}all-ingress-untrust-ipv6"
+ network = google_compute_network.untrust.id
+ source_ranges = ["::/0"]
+ direction = "INGRESS"
+
+ allow {
+ protocol = "all"
+ }
+}
+
+
+resource "google_compute_firewall" "trust_ipv4" {
+ name = "${local.prefix}all-ingress-trust"
+ network = google_compute_network.trust.id
+ source_ranges = ["0.0.0.0/0"]
+
+ allow {
+ protocol = "all"
+ }
+}
+
+
+resource "google_compute_firewall" "trust_ipv6" {
+ name = "${local.prefix}all-ingress-trust-ipv6"
+ network = google_compute_network.trust.id
+ source_ranges = ["::/0"]
+ direction = "INGRESS"
+
+ allow {
+ protocol = "all"
+ }
+}
+
+
+# --------------------------------------------------------------------------------------------
+# Create VM-Series
+# --------------------------------------------------------------------------------------------
+
+# Service account for bootstrapping
+module "iam_service_account" {
+ source = "github.com/PaloAltoNetworks/terraform-google-vmseries-modules//modules/iam_service_account?ref=main"
+ service_account_id = "${local.prefix}vmseries-mig-sa"
+ project_id = local.project_id
+}
+
+
+# # Create the bootstrap storage bucket.
+# module "bootstrap" {
+# source = "PaloAltoNetworks/vmseries-modules/google//modules/bootstrap"
+# service_account = module.iam_service_account.email
+# location = "US"
+# files = {
+# "bootstrap_files/init-cfg.txt" = "config/init-cfg.txt"
+# "bootstrap_files/bootstrap.xml" = "config/bootstrap.xml"
+# "bootstrap_files/authcodes" = "license/authcodes"
+# }
+# }
+
+# Create VM-Series
+resource "google_compute_instance" "vmseries" {
+ name = "${local.prefix}vmseries"
+ zone = data.google_compute_zones.main.names[0]
+ machine_type = "n2-standard-4"
+ can_ip_forward = true
+ allow_stopping_for_update = true
+
+ metadata = {
+ mgmt-interface-swap = "enable"
+ # vmseries-bootstrap-gce-storagebucket = module.bootstrap.bucket_name
+ serial-port-enable = true
+ ssh-keys = "admin:${file(local.public_key_path)}"
+ }
+
+ network_interface {
+ subnetwork = google_compute_subnetwork.untrust.id
+ stack_type = "IPV4_IPV6"
+ access_config {
+ network_tier = "PREMIUM"
+ }
+ ipv6_access_config {
+ network_tier = "PREMIUM"
+ }
+ }
+
+ network_interface {
+ subnetwork = google_compute_subnetwork.mgmt.id
+ stack_type = "IPV4_IPV6"
+ access_config {
+ network_tier = "PREMIUM"
+ }
+ ipv6_access_config {
+ network_tier = "PREMIUM"
+ }
+ }
+
+ network_interface {
+ subnetwork = google_compute_subnetwork.trust.id
+ stack_type = "IPV4_IPV6"
+ }
+
+ boot_disk {
+ initialize_params {
+ image = local.vmseries_image
+ type = "pd-standard"
+ }
+ }
+
+ service_account {
+ email = module.iam_service_account.email
+ scopes = [
+ "https://www.googleapis.com/auth/compute.readonly",
+ "https://www.googleapis.com/auth/cloud.useraccounts.readonly",
+ "https://www.googleapis.com/auth/devstorage.read_only",
+ "https://www.googleapis.com/auth/logging.write",
+ "https://www.googleapis.com/auth/monitoring.write"
+ ]
+ }
+
+ depends_on = [
+ # module.bootstrap,
+ module.iam_service_account
+ ]
+}
+
+
+# Create instance group
+resource "google_compute_instance_group" "vmseries" {
+ name = "${local.prefix}vmseries"
+ zone = data.google_compute_zones.main.names[0]
+
+ instances = [
+ google_compute_instance.vmseries.id
+ ]
+}
+
+
+# --------------------------------------------------------------------------------------------
+# Create an external load balancer to distribute traffic to VM-Series trust interfaces.
+# --------------------------------------------------------------------------------------------
+
+resource "google_compute_region_health_check" "extlb" {
+ name = "${local.prefix}vmseries-extlb-hc"
+ project = var.project_id
+ region = var.region
+ check_interval_sec = 3
+ healthy_threshold = 1
+ timeout_sec = 1
+ unhealthy_threshold = 1
+
+ http_health_check {
+ port = 80
+ request_path = "/php/login.php"
+ }
+}
+
+
+resource "google_compute_address" "extlb_ipv4" {
+ name = "${local.prefix}vmseries-extlb-pip-ipv4"
+ region = local.region
+ network_tier = "PREMIUM"
+ address_type = "EXTERNAL"
+}
+
+
+resource "google_compute_address" "extlb_ipv6" {
+ name = "${local.prefix}vmseries-extlb-pip-ipv6"
+ region = local.region
+ network_tier = "PREMIUM"
+ address_type = "EXTERNAL"
+ ip_version = "IPV6"
+ ipv6_endpoint_type = "NETLB"
+ subnetwork = google_compute_subnetwork.untrust_lb.id
+}
+
+
+resource "google_compute_forwarding_rule" "extlb_ipv4" {
+ name = "${local.prefix}vmseries-extlb-rule-ipv4"
+ project = var.project_id
+ region = var.region
+ network_tier = "PREMIUM"
+ load_balancing_scheme = "EXTERNAL"
+ ip_protocol = "L3_DEFAULT"
+ all_ports = true
+ backend_service = google_compute_region_backend_service.extlb.self_link
+ ip_address = google_compute_address.extlb_ipv4.address
+}
+
+
+resource "google_compute_forwarding_rule" "extlb_ipv6" {
+ name = "${local.prefix}vmseries-extlb-rule-ipv6"
+ project = var.project_id
+ region = var.region
+ network_tier = "PREMIUM"
+ load_balancing_scheme = "EXTERNAL"
+ ip_protocol = "L3_DEFAULT"
+ all_ports = true
+ backend_service = google_compute_region_backend_service.extlb.self_link
+ ip_address = google_compute_address.extlb_ipv6.id
+ ip_version = "IPV6"
+ subnetwork = google_compute_subnetwork.untrust_lb.id
+}
+
+
+resource "google_compute_region_backend_service" "extlb" {
+ provider = google-beta
+ name = "${local.prefix}vmseries-extlb"
+ project = var.project_id
+ region = var.region
+ load_balancing_scheme = "EXTERNAL"
+ health_checks = [google_compute_region_health_check.extlb.self_link]
+ protocol = "UNSPECIFIED"
+
+ connection_tracking_policy {
+ tracking_mode = "PER_SESSION"
+ connection_persistence_on_unhealthy_backends = "NEVER_PERSIST"
+ }
+
+ backend {
+ group = google_compute_instance_group.vmseries.self_link
+ }
+}
diff --git a/outputs.tf b/outputs.tf
new file mode 100644
index 0000000..54191a0
--- /dev/null
+++ b/outputs.tf
@@ -0,0 +1,66 @@
+
+# --------------------------------------------------------------------------------------------
+# Outputs
+# --------------------------------------------------------------------------------------------
+
+output "EXTLB_IPv4" {
+ value = "${google_compute_address.extlb_ipv4.address}/32"
+}
+
+output "EXTLB_IPv6" {
+ value = "${google_compute_address.extlb_ipv6.address}/32"
+}
+
+output "VMSERIES_CLI" {
+ value = "ssh admin@${google_compute_instance.vmseries.network_interface.1.access_config.0.nat_ip} -i ${trim(local.public_key_path, ".pub")}"
+}
+
+output "VMSERIES_GUI" {
+ value = "https://${google_compute_instance.vmseries.network_interface.1.access_config.0.nat_ip}"
+}
+
+output "SSH_INTERNAL_VM" {
+ value = (var.create_test_vms == true ? "gcloud compute ssh paloalto@${google_compute_instance.internal_vm.0.name} --zone=${data.google_compute_zones.main.names[0]}" : "")
+}
+output "SSH_EXTERNAL_VM" {
+ value = (var.create_test_vms == true ? "gcloud compute ssh paloalto@${google_compute_instance.external_vm.0.name} --zone=${data.google_compute_zones.main.names[0]}" : "")
+}
+
+# output "vmseries_untrust_ipv6" {
+# value = google_compute_instance.vmseries.network_interface.0.ipv6_access_config.0.external_ipv6
+# }
+
+# data "google_compute_network" "trust" {
+# name = "${local.prefix}trust-vpc"
+# }
+
+
+
+
+
+
+
+# data google_compute_instance "vmseries" {
+# name = "vmseries"
+# zone = local.zone
+# }
+# output "test2" {
+# value = data.google_compute_instance.vmseries
+# # sensitive = true
+# }
+
+# output "test3" {
+# value = google_compute_instance.vmseries.network_interface.0.addresses[0].address_v6
+# }
+# VM IP : fd20:d42:dc76:0:0:0:0:0
+
+# FW IP : 2600:1900:4000:eba6:0:0:0:0 | 2600:1900:4000:eba6::7c32:0 | test nptv6 cks-neutral source-ip fd20:d42:dc76:0:0:0:0:0 dest-network 2600:1900:4000:eba6:0:0:0:0/96
+# LB IP : 2600:1900:4000:b1d3:8000:0:0:0 | 2600:1900:4000:b1d3:8000:0:3605:0 | test nptv6 cks-neutral source-ip fd20:d42:dc76:0:0:0:0:0 dest-network 2600:1900:4000:b1d3:8000:0:0:0/96
+
+# FW
+## curl -6 'http://[2600:1900:4000:eba6:0:0:0:0]:80/'
+## curl -6 'http://[2600:1900:4000:eba6::7c32:0]:80/'
+
+# LB
+## curl -6 'http://[2600:1900:4000:b1d3:8000:0:0:0]:80/'
+## curl -6 'http://[2600:1900:4000:b1d3:8000:0:3605:0]:80/'
diff --git a/terraform.tfvars.example b/terraform.tfvars.example
new file mode 100644
index 0000000..0a2546e
--- /dev/null
+++ b/terraform.tfvars.example
@@ -0,0 +1,15 @@
+# Required for VM-Series tutorial
+project_id = null
+public_key_path = "~/.ssh/vmseries-tutorial.pub"
+mgmt_allow_ips = ["0.0.0.0/0"]
+create_test_vms = true
+vmseries_image = "https://www.googleapis.com/compute/v1/projects/paloaltonetworksgcp-public/global/images/vmseries-flex-bundle1-1104h1" // View public images: `gcloud compute images list --project paloaltonetworksgcp-public --no-standard-images --uri`
+
+# Optional for VM-Series tutorial
+prefix = ""
+region = "us-central1"
+subnet_cidr_mgmt = "10.0.0.0/28"
+subnet_cidr_untrust = "10.0.1.0/28"
+subnet_cidr_untrust_lb = "10.0.1.16/28"
+subnet_cidr_trust = "10.0.2.0/28"
+subnet_cidr_external = "192.168.0.0/28"
\ No newline at end of file
diff --git a/variables.tf b/variables.tf
new file mode 100644
index 0000000..221a2e9
--- /dev/null
+++ b/variables.tf
@@ -0,0 +1,77 @@
+variable "project_id" {
+ description = "GCP Project ID"
+ type = string
+}
+
+variable "public_key_path" {
+ description = "Local path to public SSH key. To generate the key pair use `ssh-keygen -t rsa -C admin -N '' -f id_rsa` If you do not have a public key, run `ssh-keygen -f ~/.ssh/demo-key -t rsa -C admin`"
+ type = string
+}
+
+variable "mgmt_allow_ips" {
+ description = "A list of IP addresses to be added to the management network's ingress firewall rule. The IP addresses will be able to access to the VM-Series management interface."
+ type = list(string)
+}
+
+variable "create_test_vms" {
+ description = "If set to true, test workloads will be deployed to test IPv6 and IPv4 traffic."
+ type = bool
+}
+
+variable "vmseries_image" {
+ description = "Name of the VM-Series image within the paloaltonetworksgcp-public project. To list available images, run: `gcloud compute images list --project paloaltonetworksgcp-public --no-standard-images`. If you are using a custom image in a different project, please update `local.vmseries_iamge_url` in `main.tf`."
+ type = string
+}
+
+
+
+variable "region" {
+ description = "GCP Region"
+ default = "us-central1"
+ type = string
+}
+
+variable "prefix" {
+ description = "Prefix to GCP resource names, an arbitrary string"
+ default = ""
+ type = string
+}
+
+variable "subnet_cidr_mgmt" {
+ description = "IPv4 CIDR for the VM-Series mgmt subnetwork."
+ default = "10.0.0.0/28"
+ type = string
+}
+
+variable "subnet_cidr_untrust" {
+ description = "IPv4 CIDR for the VM-Series untrust subnetwork."
+ default = "10.0.1.0/28"
+ type = string
+}
+
+variable "subnet_cidr_untrust_lb" {
+ description = "IPv4 CIDR for the external load balancer subnetwork."
+ default = "10.0.1.16/28"
+ type = string
+}
+
+variable "subnet_cidr_trust" {
+ description = "IPv4 CIDR for the VM-Series trust subnetwork."
+ default = "10.0.2.0/28"
+ type = string
+}
+
+variable "subnet_cidr_web" {
+ description = "IPv4 CIDR for the external network for ingress testing."
+ default = "10.0.3.0/28"
+ type = string
+}
+
+
+variable "subnet_cidr_external" {
+ description = "IPv4 CIDR for the external network for ingress testing."
+ default = "192.168.0.0/28"
+ type = string
+}
+
+
diff --git a/vms.tf b/vms.tf
new file mode 100644
index 0000000..72bee41
--- /dev/null
+++ b/vms.tf
@@ -0,0 +1,111 @@
+# --------------------------------------------------------------------------------------------
+# Create external VPC & subnet
+# --------------------------------------------------------------------------------------------
+
+resource "google_compute_network" "external" {
+ count = (local.create_test_vms ? 1 : 0)
+ name = "${local.prefix}external-vpc"
+ routing_mode = "GLOBAL"
+ auto_create_subnetworks = false
+ enable_ula_internal_ipv6 = true
+}
+
+resource "google_compute_subnetwork" "external" {
+ count = (local.create_test_vms ? 1 : 0)
+ name = "${local.prefix}external-subnet"
+ ip_cidr_range = local.subnet_cidr_external
+ region = local.region
+ stack_type = "IPV4_IPV6"
+ ipv6_access_type = "EXTERNAL"
+ network = google_compute_network.external[0].id
+}
+
+# --------------------------------------------------------------------------------------------
+# Create ingress firewall rules
+# --------------------------------------------------------------------------------------------
+
+resource "google_compute_firewall" "external_ipv4" {
+ count = (local.create_test_vms ? 1 : 0)
+ name = "${local.prefix}all-ingress-external"
+ network = google_compute_network.external[0].id
+ source_ranges = ["0.0.0.0/0"]
+
+ allow {
+ protocol = "all"
+ }
+}
+
+resource "google_compute_firewall" "external_ipv6" {
+ count = (local.create_test_vms ? 1 : 0)
+ name = "${local.prefix}all-ingress-external-ipv6"
+ network = google_compute_network.external[0].id
+ source_ranges = ["::/0"]
+ direction = "INGRESS"
+
+ allow {
+ protocol = "all"
+ }
+}
+
+# --------------------------------------------------------------------------------------------
+# External VM
+# --------------------------------------------------------------------------------------------
+
+resource "google_compute_instance" "external_vm" {
+ count = (local.create_test_vms ? 1 : 0)
+ name = "${local.prefix}external-vm"
+ project = local.project_id
+ zone = data.google_compute_zones.main.names[0]
+ machine_type = "f1-micro"
+ allow_stopping_for_update = true
+
+ metadata = {
+ serial-port-enable = true
+ }
+
+ boot_disk {
+ initialize_params {
+ image = "https://www.googleapis.com/compute/v1/projects/panw-gcp-team-testing/global/images/ubuntu-2004-lts-apache"
+ }
+ }
+
+ network_interface {
+ subnetwork = google_compute_subnetwork.external[0].id
+ stack_type = "IPV4_IPV6"
+ access_config {
+ network_tier = "PREMIUM"
+ }
+ ipv6_access_config {
+ network_tier = "PREMIUM"
+ }
+ }
+}
+
+# --------------------------------------------------------------------------------------------
+# Internal VM
+# --------------------------------------------------------------------------------------------
+
+resource "google_compute_instance" "internal_vm" {
+ count = (local.create_test_vms ? 1 : 0)
+ name = "${local.prefix}internal-vm"
+ project = local.project_id
+ zone = data.google_compute_zones.main.names[0]
+ machine_type = "f1-micro"
+ allow_stopping_for_update = true
+
+ metadata = {
+ serial-port-enable = true
+ }
+
+ boot_disk {
+ initialize_params {
+ image = "https://www.googleapis.com/compute/v1/projects/panw-gcp-team-testing/global/images/ubuntu-2004-lts-jenkins"
+ }
+ }
+
+ network_interface {
+ subnetwork = google_compute_subnetwork.web.id
+ network_ip = cidrhost(local.subnet_cidr_web, 10)
+ stack_type = "IPV4_IPV6"
+ }
+}
\ No newline at end of file