Exposing GCP VMs to the Internet with Cloudflare Tunnel

Somebody who likes to code
Search for a command to run...

Somebody who likes to code
No comments yet. Be the first to comment.
Google processes tens of thousands of code changes every day across a codebase of hundreds of millions of lines. The secret isn't magic tooling — it's a documented, principled approach to code review

Amazon Simple Notification Service (SNS) looks straightforward on the surface — publish a message, deliver it to subscribers — but the gap between a topic that "works in dev" and one that is productio

Amazon Simple Queue Service (SQS) is deceptively simple to get started with, but surprisingly easy to misconfigure in ways that cause silent message loss, runaway costs, duplicate processing storms, o

HTTP API is the right default choice for the majority of .NET serverless workloads. At 70% less cost than REST API, lower latency, and native JWT authorization, it covers most production requirements

AWS Lambda has become a cornerstone of modern serverless architectures, but writing a function that "just works" is very different from writing one that is performant, cost-effective, secure, and read

In the previous article, we deployed a virtual machine (VM) on Google Cloud Platform (GCP) with a simple NGINX web server. While functional, exposing a server directly to the internet with a public IP address and firewall rules can introduce security vulnerabilities and management overhead. Today, we will build on our previous setup and introduce a more secure and robust way to expose our web server: Cloudflare Tunnel.
Cloudflare Tunnel creates a secure, outbound-only connection between our VM and the Cloudflare network. A lightweight daemon, cloudflared, runs on our VM and establishes this connection. This means we can have a VM running in our GCP project without a public IP address, making it inaccessible from the public internet and less vulnerable to direct attacks.

Here are some key benefits of using Cloudflare Tunnel:
Enhanced security: Our server's IP address is hidden, which protects it from direct attacks.
No inbound ports: We no longer need to open ports on our firewall.
DDoS protection: All traffic to our server goes through Cloudflare's network, which offers DDoS protection.
Free tier: Cloudflare provides a generous free tier for its Tunnel service, making it a cost-effective solution for personal projects and small applications.
Google Cloud CLI installed and authenticated.
A Cloudflare Account with a registered domain.
Download the initial code.
First, let's update our Terraform code to remove the public IP address and the firewall rule since they are no longer needed. Modify the terraform/main.tf file as shown below:
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.34.0"
}
}
}
resource "google_compute_instance" "free_tier_vm" {
project = var.project_id
machine_type = "e2-micro"
zone = "us-central1-a"
name = "my-first-vm"
boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
}
}
network_interface {
network = "default"
access_config {}
}
metadata = {
startup-script = file("${path.module}/startup-script.sh")
}
}
variable "project_id" {
type = string
description = "The GCP project ID to deploy resources into."
}
Notice that the tags property has been removed from the google_compute_instance resource, and the entire google_compute_firewall resource has been deleted. We need to keep the access_config block to assign a public IP address to the VM so it can still access the internet.
Setting up a Cloudflare Tunnel involves a few manual steps that we only need to do once.
Install cloudflared on our local machine.
Authenticate with Cloudflare by running cloudflared tunnel login.
Create a tunnel by running cloudflared tunnel create my-gcp-tunnel.
Create a CNAME record in Cloudflare DNS to link our subdomain to our tunnel:
cloudflared tunnel route dns my-gcp-tunnel nginx.<MY_DOMAIN>
Now, let's update the terraform/startup-script.sh to install and run cloudflared on the VM when it starts. To get the token and ID for our tunnel, run the following command:
cloudflared tunnel token my-gcp-tunnel
cloudflared tunnel info my-gcp-tunnel
Update the startup script with the following content:
#!/bin/bash
set -e
echo "=== Startup script started at $(date) ==="
apt-get update -y
apt-get install -y nginx
systemctl start nginx
systemctl enable nginx
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
dpkg -i cloudflared-linux-amd64.deb
mkdir -p /etc/cloudflared/
cat > /etc/cloudflared/config.yml << EOF
tunnel: <MY_TUNNEL_ID>
credentials-file: /root/.cloudflared/<MY_TUNNEL_ID>.json
ingress:
- hostname: nginx.<MY_DOMAIN>
service: http://127.0.0.1:80
- service: http_status:404
EOF
cloudflared service install <MY_TUNNEL_TOKEN> --config /etc/cloudflared/config.yml
systemctl start cloudflared
systemctl enable cloudflared
echo "=== Startup script completed at $(date) ==="
This script will now perform the following actions:
Install NGINX.
Download and install the cloudflared daemon.
Configure and start the cloudflared service using our tunnel token. This will connect our VM to the Cloudflare network and route traffic from our chosen hostname to the local NGINX server on port 80.
Now, we can apply the changes using Infrastructure Manager:
gcloud infra-manager deployments apply my-first-deployment --location=us-central1 --local-source=./terraform --input-values=project_id=<MY_PROJECT_ID> --service-account=projects/<MY_PROJECT_ID>/serviceAccounts/infra-manager-sa@<MY_PROJECT_ID>.iam.gserviceaccount.com
Once the deployment is complete, our NGINX server will be accessible at the hostname you configured (nginx.<MY_DOMAIN> in our example), secured behind the Cloudflare network.
To remove all the resources created, we can run the same cleanup command as in the previous article:
gcloud infra-manager deployments delete my-deployment --location=us-central1
To remove the Cloudflare tunnel, we can run:
cloudflared tunnel delete my-gcp-tunnel
By using Cloudflare Tunnel, we've greatly enhanced the security of our deployment, showing a more production-ready way to expose services running on GCP. You can find all the code here. Thanks, and happy coding.