Terraform
Terraform ↗ is an infrastructure as code tool that lets you define and manage your tunnels alongside other infrastructure. This guide deploys:
- A GCP virtual machine that runs a web server
- A Cloudflare Tunnel that makes the server available over the Internet
- (Optional) A Cloudflare Access policy that defines who can connect
Refer to the Terraform installation guide ↗ for your operating system.
-
Install the gcloud CLI ↗ so that Terraform can interact with your GCP account.
-
Authenticate with the CLI by running:
Terminal window gcloud auth application-default login
Create an API token so that Terraform can interact with your Cloudflare account. At minimum, your token should include the following permissions:
| Type | Item | Permission |
|---|---|---|
| Account | Cloudflare Tunnel | Edit |
| Account | Access: Apps and Policies | Edit |
| Zone | DNS | Edit |
Terraform functions through a working directory that contains configuration files. You can store your configuration in multiple files or just one — Terraform will evaluate all of the configuration files in the directory as if they were in a single document.
-
Create a folder for your Terraform configuration:
Terminal window mkdir cloudflare-tf -
Change into the directory:
Terminal window cd cloudflare-tf
The following variables will be passed into your GCP and Cloudflare configuration.
-
In your configuration directory, create a
.tffile:Terminal window touch variables.tf -
Open the file in a text editor and copy and paste the following:
# GCP variablesvariable "gcp_project_id" {description = "Google Cloud Platform (GCP) project ID"type = string}variable "zone" {description = "Geographical zone for the GCP VM instance"type = string}variable "machine_type" {description = "Machine type for the GCP VM instance"type = string}# Cloudflare variablesvariable "cloudflare_zone" {description = "Domain used to expose the GCP VM instance to the Internet"type = string}variable "cloudflare_zone_id" {description = "Zone ID for your domain"type = string}variable "cloudflare_account_id" {description = "Account ID for your Cloudflare account"type = stringsensitive = true}variable "cloudflare_email" {description = "Email address for your Cloudflare account"type = stringsensitive = true}variable "cloudflare_token" {description = "Cloudflare API token"type = stringsensitive = true}
-
In your configuration directory, create a
.tfvarsfile:Terminal window touch terraform.tfvarsTerraform will automatically use these variables if the file is named
terraform.tfvars, otherwise the variable file will need to be manually passed in. -
Add the following variables to
terraform.tfvars. Be sure to modify the example with your own values.cloudflare_zone = "example.com"cloudflare_zone_id = "023e105f4ecef8ad9ca31a8372d0c353"cloudflare_account_id = "372e67954025e0ba6aaa6d586b9e0b59"cloudflare_email = "user@example.com"cloudflare_token = "y3AalHS_E7Vabk3c3lX950F90_Xl7YtjSlzyFn_X"gcp_project_id = "testvm-123"zone = "us-central1-a"machine_type = "e2-medium"
You will need to declare the providers ↗ used to provision the infrastructure.
-
In your configuration directory, create a
.tffile:Terminal window touch providers.tf -
Add the following providers to
providers.tf. Therandomprovider is used to generate a tunnel secret.terraform {required_providers {cloudflare = {source = "cloudflare/cloudflare"version = ">= 5.8.2"}google = {source = "hashicorp/google"}}required_version = ">= 1.2"}# Providersprovider "cloudflare" {api_token = var.cloudflare_token}provider "google" {project = var.gcp_project_id}provider "random" {}terraform {required_providers {cloudflare = {source = "cloudflare/cloudflare"version = ">= 4.40.0, < 5.0.0"}google = {source = "hashicorp/google"}random = {source = "hashicorp/random"}}required_version = ">= 1.2"}# Providersprovider "cloudflare" {api_token = var.cloudflare_token}provider "google" {project = var.gcp_project_id}provider "random" {}
The following configuration will modify settings in your Cloudflare account.
-
In your configuration directory, create a
.tffile:Terminal window touch Cloudflare-config.tf -
Add the following resources to
Cloudflare-config.tf:# Creates a new remotely-managed tunnel for the GCP VM.resource "cloudflare_zero_trust_tunnel_cloudflared" "gcp_tunnel" {account_id = var.cloudflare_account_idname = "Terraform GCP tunnel"config_src = "cloudflare"}# Reads the token used to run the tunnel on the server.data "cloudflare_zero_trust_tunnel_cloudflared_token" "gcp_tunnel_token" {account_id = var.cloudflare_account_idtunnel_id = cloudflare_zero_trust_tunnel_cloudflared.gcp_tunnel.id}# Creates the CNAME record that routes http_app.${var.cloudflare_zone} to the tunnel.resource "cloudflare_dns_record" "http_app" {zone_id = var.cloudflare_zone_idname = "http_app"content = "${cloudflare_zero_trust_tunnel_cloudflared.gcp_tunnel.id}.cfargotunnel.com"type = "CNAME"ttl = 1proxied = true}# Configures tunnel with a published application for clientless access.resource "cloudflare_zero_trust_tunnel_cloudflared_config" "gcp_tunnel_config" {tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.gcp_tunnel.idaccount_id = var.cloudflare_account_idconfig = {ingress = [{hostname = "http_app.${var.cloudflare_zone}"service = "http://httpbin:80"},{service = "http_status:404"}]}}# (Optional) Routes internal IP of GCP instance through the tunnel for private network access using WARP.resource "cloudflare_zero_trust_tunnel_cloudflared_route" "example_tunnel_route" {account_id = var.cloudflare_account_idtunnel_id = cloudflare_zero_trust_tunnel_cloudflared.gcp_tunnel.idnetwork = google_compute_instance.http_server.network_interface.0.network_ipcomment = "Example tunnel route"}# Creates a reusable Access policy.resource "cloudflare_zero_trust_access_policy" "allow_emails" {account_id = var.cloudflare_account_idname = "Allow email addresses"decision = "allow"include = [{email = {email = var.cloudflare_email}},{email_domain = {domain = "@example.com"}}]}# Creates an Access application to control who can connect to the public hostname.resource "cloudflare_zero_trust_access_application" "http_app" {account_id = var.cloudflare_account_idtype = "self_hosted"name = "Access application for http_app.${var.cloudflare_zone}"domain = "http_app.${var.cloudflare_zone}"policies = [{id = cloudflare_zero_trust_access_policy.allow_emails.idprecedence = 1}]}# Generates a 32-byte secret for the tunnel.resource "random_bytes" "tunnel_secret" {byte_length = 32}# Creates a new remotely-managed tunnel for the GCP VM.resource "cloudflare_zero_trust_tunnel_cloudflared" "gcp_tunnel" {account_id = var.cloudflare_account_idname = "Terraform GCP tunnel"secret = random_bytes.tunnel_secret.base64}# Creates the CNAME record that routes http_app.${var.cloudflare_zone} to the tunnel.resource "cloudflare_record" "http_app" {zone_id = var.cloudflare_zone_idname = "http_app"content = "${cloudflare_zero_trust_tunnel_cloudflared.gcp_tunnel.cname}"type = "CNAME"proxied = true}# Configures tunnel with a published application for clientless access.resource "cloudflare_zero_trust_tunnel_cloudflared_config" "gcp_tunnel_config" {tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.gcp_tunnel.idaccount_id = var.cloudflare_account_idconfig {ingress_rule {hostname = "${cloudflare_record.http_app.hostname}"service = "http://httpbin:80"}ingress_rule {service = "http_status:404"}}}# (Optional) Route internal IP of GCP instance through the tunnel for private network access using WARP.resource "cloudflare_zero_trust_tunnel_route" "example_tunnel_route" {account_id = var.cloudflare_account_idtunnel_id = cloudflare_zero_trust_tunnel_cloudflared.gcp_tunnel.idnetwork = google_compute_instance.http_server.network_interface.0.network_ipcomment = "Example tunnel route"}# Creates an Access application to control who can connect to the public hostname.resource "cloudflare_zero_trust_access_application" "http_app" {account_id = var.cloudflare_account_idname = "Access application for http_app.${var.cloudflare_zone}"domain = "http_app.${var.cloudflare_zone}"}# Creates a (legacy) Access policy for the Access application.resource "cloudflare_zero_trust_access_policy" "allow_emails" {application_id = cloudflare_zero_trust_access_application.http_app.idaccount_id = var.cloudflare_account_idname = "Example policy for http_app.${var.cloudflare_zone}"precedence = "1"decision = "allow"include {email = [var.cloudflare_email]}}
To learn more about these resources, refer to the Cloudflare provider documentation ↗.
The following configuration defines the specifications for the GCP virtual machine and configures a startup script to run upon boot.
-
In your configuration directory, create a
.tffile:Terminal window touch GCP-config.tf -
Add the following content to
GCP-config.tf:# OS the server will usedata "google_compute_image" "image" {family = "ubuntu-2204-lts"project = "ubuntu-os-cloud"}# GCP Instance resourceresource "google_compute_instance" "http_server" {name = "test"machine_type = var.machine_typezone = var.zonetags = []boot_disk {initialize_params {image = data.google_compute_image.image.self_link}}network_interface {network = "default"access_config {//Ephemeral IP}}// Optional config to make instance ephemeral/* scheduling {preemptible = trueautomatic_restart = false} */// Pass the tunnel token to the GCP server so that the server can install and run the tunnel upon startup.metadata_startup_script = templatefile("./install-tunnel.tftpl",{tunnel_token = data.cloudflare_zero_trust_tunnel_cloudflared_token.gcp_tunnel_token.token})}# OS the server will usedata "google_compute_image" "image" {family = "ubuntu-2204-lts"project = "ubuntu-os-cloud"}# GCP Instance resourceresource "google_compute_instance" "http_server" {name = "test"machine_type = var.machine_typezone = var.zonetags = []boot_disk {initialize_params {image = data.google_compute_image.image.self_link}}network_interface {network = "default"access_config {//Ephemeral IP}}// Optional config to make instance ephemeral/* scheduling {preemptible = trueautomatic_restart = false} */// Pass the tunnel token to the GCP server so that the server can install and run the tunnel upon startup.metadata_startup_script = templatefile("./install-tunnel.tftpl",{tunnel_token = cloudflare_zero_trust_tunnel_cloudflared.gcp_tunnel.tunnel_token})}
The following script will install cloudflared and run the tunnel as a service. This example also installs a lightweight HTTP application that you can use to test connectivity.
-
In your configuration directory, create a Terraform template file:
Terminal window touch install-tunnel.tftpl -
Open the file in a text editor and copy and paste the following bash script:
Terminal window # Script to install Cloudflare Tunnel and Docker resources# Docker configurationcd /tmpsudo apt-get install software-properties-common# Retrieving the docker repository for this OScurl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"# The OS is updated and docker is installedsudo apt update -y && sudo apt upgrade -ysudo apt install docker docker-compose -y# Add the HTTPBin application and run it on localhost:8080.cat > /tmp/docker-compose.yml << "EOF"version: '3'services:httpbin:image: kennethreitz/httpbinrestart: alwayscontainer_name: httpbinports:- 8080:80cloudflared:image: cloudflare/cloudflared:latestrestart: alwayscontainer_name: cloudflaredcommand: tunnel run --token ${tunnel_token}EOFcd /tmpsudo docker-compose up -d
To deploy the configuration files:
-
Initialize your configuration directory:
Terminal window terraform init -
Preview everything that will be created:
Terminal window terraform plan -
Apply the configuration:
Terminal window terraform apply
It may take several minutes for the GCP instance and tunnel to come online. You can view your new tunnel in the Cloudflare dashboard ↗ under Networking > Tunnels.
-
In the Cloudflare dashboard ↗, go to Networking > Tunnels and verify that your tunnel is Active.
-
(Optional) If you configured Access, go to Security > Access > Applications and verify that your Cloudflare email is allowed by the Access policy.
-
From any device, open a browser and go to
http_app.<CLOUDFLARE_ZONE>(for example,http_app.example.com).If you configured Access, you will see the Access login page. Log in with your Cloudflare email.
-
You should see the HTTPBin homepage, confirming that your tunnel is routing traffic correctly.