I Have 6 GCP Certs But Never Wrote Terraform — Here's My First Landing Zone
2026-03-06 · Jazz Lien
The Gap on My Resume
I have 6 Google Cloud Professional certifications. I can whiteboard a VPC peering architecture in my sleep. I've explained Private Services Access to dozens of customers.
But I'd never written a single line of Terraform.
In cloud interviews, "I know the concepts" without "I've done it" is a gap that's hard to hide. So I spent a week building a GCP landing zone from scratch with Terraform — and the things that tripped me up weren't what I expected.
What I Built
A complete 3-tier application infrastructure:
Internet
|
Global HTTPS Load Balancer + Cloud CDN
|
+---+---+
| |
nginx Flask API ← GCE Managed Instance Groups
| |
| +---+---+
| | |
| Cloud SQL Cloud Storage
| (private) (IAM-controlled)
7 Terraform modules, 54 GCP resources, deployable from zero with one command. The "app" is a placeholder — the point is the infrastructure.
Modules: network (VPC + NAT + firewall), IAM (least-privilege SAs), compute (MIGs + autoscaling), database (Cloud SQL private IP), storage (GCS), secrets (Secret Manager), load-balancer (Global LB + CDN).
The full code is on GitHub.
The 3 Gotchas That Humbled Me
1. "Public Subnet" Doesn't Mean "Has Internet"
I created a VPC with two subnets: public (frontend) and private (backend). Set up Cloud NAT for the private subnet so backend instances could reach the internet for package installs.
First deploy: backend started fine. Frontend failed.
Cannot initiate the connection to deb.debian.org:443
E: Failed to fetch .../nginx_1.22.1-9+deb12u3_amd64.deb
The frontend instances couldn't install nginx because they had no internet access. But they're in the "public" subnet — why?
Because instances don't have public IPs. Traffic reaches them through the load balancer, not directly. Without an external IP, they can't make outbound connections. The subnet name is just a label — it doesn't grant internet access.
Fix: Add the "public" subnet to Cloud NAT alongside the private one. Both subnets need NAT because no instance has an external IP.
I've explained this concept to customers dozens of times. Still got it wrong when I built it myself.
2. Cloud Armor Quota = 0 on Free Trial
I included Cloud Armor (WAF) with rate limiting in my load balancer module. Clean code, proper policy. terraform apply failed:
Error: Quota 'SECURITY_POLICIES' exceeded. Limit: 0.0 globally.
Free trial GCP projects have zero quota for security policies. Not "limited" — zero. No warning in the documentation, no hint during project creation.
Fix: Comment it out, document why, move on. Not every GCP service is available on every account type.
3. Terraform Is More Resilient Than You Think
The Cloud Armor failure crashed my first terraform apply partway through. Cloud SQL was already created, but Terraform hadn't saved it to state yet. On retry:
Error 409: The Cloud SQL instance already exists., instanceAlreadyExists
I panicked for about 30 seconds. Then I found terraform import:
terraform import \
'module.database.google_sql_database_instance.main' \
'projects/my-project/instances/landing-zone-db-demo'
Re-ran apply. Terraform checked what existed, skipped the 14 resources already created, and built the remaining 40. Clean.
This is actually Terraform's superpower — it reconciles desired state with actual state. Partial failures are a normal part of infrastructure. The tool is designed for it.
What I'd Do Differently
Start with terraform validate and terraform plan before ever running apply. I was eager to see resources created and skipped the dry run. The plan would have shown me the Cloud Armor quota issue before it crashed partway.
Use a custom VM image instead of startup scripts. Startup scripts run on every boot and depend on internet access. A Packer-built image with nginx/Flask pre-installed would be faster and more reliable.
Budget alerts first. I set up a $5 budget alert before deploying, which gave me peace of mind to experiment. Cloud SQL is ~$1/day — it adds up if you forget to destroy.
Enterprise Patterns Worth Knowing
Beyond the gotchas, here's what I deliberately included because interviewers care about it:
- Private Services Access — Cloud SQL connected via VPC peering (
google_service_networking_connection), not just "private IP checkbox"
- Least-privilege IAM — Separate service account per tier. Frontend gets logging only. Backend gets logging + secrets + SQL + storage.
- Secret Manager — DB password stored in Secret Manager, accessed at runtime via SA permissions. No env vars, no key files.
- Org policy awareness — Free trial has no org node, but I documented which constraints (
compute.vmExternalIpAccess, sql.restrictPublicIp) an enterprise would enforce.
- Workload Identity — Not implemented in v1, but noted as the production path. Using instance metadata + scoped SAs is the pragmatic middle ground.
The Numbers
| What |
Count |
| Terraform modules |
7 |
| GCP resources created |
54 |
| Time to deploy (from zero) |
~15 minutes |
| Time to destroy |
~8 minutes |
| Daily cost (preemptible) |
~$3-5 |
| Gotchas that humbled me |
3 |
| GCP certs that didn't prevent the gotchas |
6 |
Try It Yourself
git clone https://github.com/jazzpujols34/gcp-landing-zone.git
cd gcp-landing-zone
./setup.sh
The interactive setup walks you through everything. You need a GCP project with billing enabled, Terraform, and gcloud CLI. Total cost: a few dollars for an afternoon of learning.
Remember to terraform destroy when you're done.
The gap between "I know the concepts" and "I've built it" is smaller than you think — but it's filled with gotchas that no certification prepares you for. That's exactly why it's worth crossing.