GCP Lesson 6 of 98

Three-Tier Web Application on GCP: The Foundational Pattern

In a nutshell

A three-tier web application is the most common shape for any website that real people log into, and it works by splitting the job into three separate roles so each one can be scaled, secured, and repaired on its own. Picture a busy restaurant. The dining room and the waiters take your order and bring your food — that is the presentation/web tier. The kitchen actually cooks the meal by following the recipes — that is the application/logic tier. And the locked pantry and walk-in fridge at the back, where all the ingredients are safely kept, is the data tier. On a busy night you hire more waiters without touching the pantry, and you keep the pantry locked in the back where no customer ever wanders — which is exactly how a well-built web app treats its database.

On Google Cloud those three roles map onto specific managed services. A global load balancer is the front door that greets every visitor and spreads them evenly across many identical, interchangeable web servers — the waiters. Those web servers run your application code (either a managed instance group of virtual machines, or Cloud Run containers) — the kitchen. And a Cloud SQL database, sitting on a private network with no public address, is the pantry where the durable truth lives. Wrapped around that spine are the boring-but-essential parts: a VPC with private subnets so the tiers talk to each other but not to the open internet; IAM service accounts so each tier gets only the permissions it truly needs; health checks and autoscaling so a broken server is replaced automatically and a busy hour adds capacity on its own; and Secret Manager so the database password lives in a vault instead of a text file.

The reason this 40-year-old pattern refuses to die is that the separation itself is the payoff. When traffic surges you add web servers and leave the database untouched; when you want the database safe you hide it behind a private network and the web servers never notice the difference. Get this foundational build right on GCP and a service that used to fall over under load becomes a non-event — which is the entire story of this lesson. The architecture diagram in the “Architecture overview” section traces the full request path; everything below fleshes out each piece and then hands you exercises to build it yourself.

Level: Junior · Time: ~33 min

Prerequisites: You should be comfortable with the very basics of a cloud account — that a project is your billing-and-resource boundary, and that resources live in regions and zones. It helps to know roughly what a virtual machine and a relational database are. You do not need to be an expert in any single service; this lesson is the map that ties them together. If GCP’s fundamentals or the load balancer feel unfamiliar, skim Cloud fundamentals: resource hierarchy and pricing and Cloud Load Balancing deep dive first.

After this lesson you can:

A mid-sized further-education college — the kind that runs Moodle for 18,000 students across three campuses — has a problem every August. Enrollment opens, every applicant and returning student logs in within the same fortnight, and the single virtual machine that has hosted the learning portal for six years falls over under the load. Last year the registrar’s office took 2,000 phone calls in a week because students could not submit coursework before a deadline, and the IT team spent three nights restarting a server by hand. The principal’s brief to the new platform engineer is blunt and unglamorous: “make the portal stay up in August, stop keeping the database password in a config file, and do it without hiring a team I cannot afford.” This is not a machine-learning moonshot. It is the most common architecture in the world — a three-tier web application — and getting the foundational version right on Google Cloud is what turns three sleepless nights into a non-event. This article is that reference build.

The three tiers are a separation of concerns that has survived four decades because it keeps working. The presentation/web tier takes HTTP requests and renders responses. The application/logic tier runs the business rules — in a small Moodle-style app these two often live in the same process, and we will treat the web+app tier as one horizontally scalable layer. The data tier is the database of record, where the durable truth lives. The whole point of separating them is that each tier fails, scales, and gets secured on its own terms: you add web servers when traffic spikes without touching the database, and you harden the database behind a private network without slowing down the web servers.

Why not just a bigger single server

The college’s instinct — and the honest starting point for most teams — is “the VM is too small, buy a bigger VM.” Naming why that fails matters, because someone will propose it in every planning meeting.

A single large VM is a single point of failure: when it reboots for a kernel patch, or its zone has an incident, the entire portal is down, and August is exactly when you cannot afford that. It scales only vertically — you can buy a bigger machine right up until you hit the largest machine, and you are still paying for that peak capacity in February when nobody is enrolling. It couples the tiers: a runaway web process can starve the database of CPU on the same box. And it tends to keep secrets on disk — the database password ends up in a config.php, which is the exact thing the principal asked to stop, and the exact thing that leaks when a backup or a repo goes somewhere it should not.

The three-tier pattern on GCP fixes all four: a load balancer spreads traffic across many small, identical, replaceable web instances in multiple zones; the web tier scales horizontally up in August and back down in February so you pay for what you use; the data tier is a separate managed service with its own failover; and the database credential lives in Secret Manager, never on a web server’s disk.

Architecture overview

Three-Tier Web Application on GCP: The Foundational Pattern — architecture

Follow the diagram as a single request’s journey, edge to database and back: a student’s browser → Cloud DNS → the global HTTPS Load Balancer (screened by Cloud Armor) → the stateless web tier (a managed instance group or Cloud Run) → the private, highly-available Cloud SQL database — with Secret Manager handing the web tier its DB credential and Memorystore plus Cloud Storage holding session and file state off to the side. The one fact that makes the whole picture click: everything toward the front is public and replaceable, and everything toward the back is private and durable. Trace it once and the whole architecture makes sense.

  1. Edge & DNS. A student opens learn.college.edu. Cloud DNS resolves it to a single global anycast IP that fronts the application. For this college, Akamai sits in front as the CDN and edge: it caches the static course assets (PDFs, lecture videos, CSS/JS) close to students so those bytes never travel to GCP at all, terminates TLS at the edge, and absorbs a first layer of volumetric and bot traffic — which keeps the August spike of cacheable requests off the origin entirely.

  2. Global HTTPS Load Balancer + WAF. Requests that are not served from cache reach Cloud Load Balancing (the global external Application Load Balancer). It terminates HTTPS with a Google-managed certificate, and every request first passes through Cloud Armor, GCP’s WAF and DDoS shield, attached as a security policy on the backend. Cloud Armor runs preconfigured OWASP rules (SQL injection, XSS), per-IP rate limiting to stop a single client hammering the login endpoint, and geo or IP allow/deny lists. This is the single front door — one place to enforce edge security for every request that reaches the app.

  3. Web/App tier. The load balancer distributes traffic across the web tier, which is where the two valid foundational choices diverge (covered in the next section): either a managed instance group (MIG) of identical Compute Engine VMs running Moodle behind the LB, or Cloud Run running Moodle as a container that scales to zero. Either way, the web tier is stateless — it holds no durable data — so any instance can serve any request and instances are freely added or destroyed.

  4. Data tier. The web tier reads and writes to Cloud SQL (MySQL for Moodle), running as a regional, highly-available instance with a standby in a second zone and automated backups. Critically, Cloud SQL has no public IP: the web tier reaches it over Private Service Access on the VPC’s private subnet, so the database is never exposed to the internet.

  5. Secrets, not config files. Before the web tier can connect to Cloud SQL, it needs the DB password. It does not read a file. It fetches the credential at runtime from Secret Manager, authorized by the instance’s or service’s service account — no password is baked into an image, a container, or a repo. For the college’s heavier compliance and rotation needs (and to manage non-GCP secrets like the Akamai API token), HashiCorp Vault can sit alongside as the central secrets broker with dynamic, short-lived database credentials and a full audit trail; Secret Manager is the GCP-native floor, Vault is the enterprise ceiling.

  6. State that is not the database. User sessions and Moodle’s cache go to Memorystore for Redis so that a student’s logged-in session survives any single web instance being replaced — the web tier stays genuinely stateless. Uploaded assignments and course files go to a Cloud Storage bucket, not a VM disk, so files persist independently of any instance and can be served via Akamai.

The defining property to internalize: the only thing exposed to the internet is the load balancer’s IP behind Cloud Armor. The web instances have no public IPs, the database has no public IP, and everything talks over a private VPC. That single fact is what makes this defensible rather than just available.

The one real decision: MIG VMs vs. Cloud Run

For a foundational three-tier app on GCP the web tier has two legitimate answers, and a Junior engineer should be able to argue both. They are not equally good for every case; the table is the honest comparison.

Dimension Managed Instance Group (Compute Engine) Cloud Run (containers)
Mental model “Many identical VMs behind the LB” “Run my container, you handle the machines”
Scaling Autoscaler adds/removes VMs on CPU/LB load (seconds–minutes) Scales per-request, to zero when idle (sub-second cold start tax)
Idle cost You pay for the minimum running VMs 24/7 ~Zero when no traffic — ideal for nights/off-season
Fit for Moodle Strong: Moodle is a long-running PHP app, easy to lift onto a VM image Strong if you containerize Moodle and keep it stateless
Ops burden You own the OS: patching, the base image, OS-level agents No OS to manage; Google patches the platform
Spiky August load Pre-warm + autoscale; predictable Effortless burst, but watch DB connection storms on scale-up
When it wins Steady baseline traffic, OS-level control, existing VM images Bursty/seasonal traffic, small team, “no servers to babysit”

For this college the honest recommendation is Cloud Run if they are willing to containerize Moodle: the workload is intensely seasonal, scale-to-zero saves real money in the quiet months, and “no OS to patch” is exactly what a one-person platform team needs. If they want to lift the existing VM image with minimal change and value OS-level control, the MIG is a perfectly correct foundational choice. Both sit behind the same Load Balancer + Cloud Armor + Cloud SQL + Secret Manager skeleton — that skeleton is the architecture; the web-tier engine is swappable.

One trap with either choice: when the web tier scales out fast, every new instance opens database connections, and Cloud SQL has a connection ceiling. Put a connection pooler (the Cloud SQL Auth Proxy with pooling, or ProxySQL) in the path so an autoscaling event does not become a database connection storm — the failure that turns a traffic spike into an outage.

Networking and the VPC, concretely

The network is where Junior builds most often go wrong, so be deliberate. A single VPC with purpose-built subnets is enough:

Firewall rules express the tiering as policy, not hope: allow the Load Balancer’s health-check and proxy ranges into the web tier on 443/8080; allow the web tier to reach Cloud SQL’s private IP on 3306; deny everything else, and especially never open 3306 or 22 to 0.0.0.0/0. A minimal Terraform sketch of the data tier shows the intent — private IP only, HA, backups, deletion protection:

resource "google_sql_database_instance" "moodle" {
  name             = "moodle-prod"
  database_version = "MYSQL_8_0"
  region           = "europe-west2"

  settings {
    tier              = "db-custom-4-15360"
    availability_type = "REGIONAL"          # HA standby in a second zone
    disk_autoresize   = true
    backup_configuration {
      enabled                        = true
      binary_log_enabled             = true # enables point-in-time recovery
      transaction_log_retention_days = 7
    }
    ip_configuration {
      ipv4_enabled    = false               # NO public IP
      private_network = google_compute_network.vpc.id
    }
  }
  deletion_protection = true                # don't let a typo drop prod
}

And the credential the app uses to connect is created in Secret Manager, never written to an image:

echo -n "$DB_PASSWORD" | gcloud secrets create moodle-db-pass --data-file=-
# the web tier's service account is granted read access, and nothing else:
gcloud secrets add-iam-policy-binding moodle-db-pass \
  --member="serviceAccount:moodle-web@PROJECT.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"

Identity: students, and the people who run it

There are two distinct identity problems and it is worth separating them.

Students authenticate to Moodle itself, typically against the college’s existing directory. If the college standardizes workforce and student SSO on Okta (or Microsoft Entra ID), Moodle federates to it over SAML/OIDC so a student has one login across the portal and other campus systems, with MFA enforced centrally — and crucially, account de-provisioning is one action in the IdP when a student leaves.

The engineers and admins who operate the GCP project authenticate to Google Cloud, and their human identities should come from the same IdP: federate Okta/Entra → Google Cloud so that access to the project is granted to IdP groups, governed by least-privilege IAM roles, and revoked centrally. No standing personal gcloud keys; no shared admin password. The web tier and Cloud Run service each run as a dedicated service account with the narrowest roles that work — read this secret, connect to this Cloud SQL instance, write to this bucket — and nothing more.

Security posture

For a foundational app the security story is layered and mostly built from defaults done correctly:

The principal’s specific ask — “stop keeping the database password in a config file” — is satisfied the moment the credential moves to Secret Manager and the web tier reads it via its service account. That is the single highest-value security change in the whole project.

Cost

A three-tier app’s bill is dominated by what runs 24/7, so the architecture choice is also a cost choice.

Lever Mechanism Typical effect
Scale to zero Cloud Run for the web tier idles to ~0 in the off-season Largest saving for seasonal traffic like enrollment
Right-size + autoscale MIG min instances low; scale out only in August Avoid paying February capacity all year
Cache at the edge Akamai/CDN serves static course assets Cuts origin egress and compute on cacheable load
Committed-use discounts 1-year CUD on the steady Cloud SQL + baseline web ~Up to ~55% off the always-on tier
Storage tiering Old course archives to Nearline/Coldline buckets Cheap long-term retention of past terms

For the college the big lever is obvious: traffic is near zero for months, so scale-to-zero compute plus edge caching is the difference between a bill sized for August and a bill sized for the actual usage curve. The one cost you should not trim is the regional HA Cloud SQL — paying for a standby is cheap insurance against the exact outage that started this project.

Scaling, failure modes, and reliability

Scaling is per-tier by design. The web tier scales horizontally — the autoscaler (MIG) or per-request scaling (Cloud Run) adds capacity on load and removes it after — and because the tier is stateless, this is safe. The data tier scales differently and more slowly: read replicas offload read-heavy traffic (Moodle dashboards, course listings) from the primary, and you scale the primary up (a bigger tier) rather than out. Recognizing that the database does not scale like the web tier — and protecting it with a connection pooler and read replicas — is the single most important scaling lesson in this pattern.

Failure modes to name before they page you at 2 a.m.:

Reliability targets (RTO/RPO). For this college a pragmatic, affordable goal is RTO ~5 minutes, RPO ~minutes: regional Cloud SQL fails over automatically within the region, point-in-time recovery bounds data loss to minutes, and the stateless web tier is recreated from its image/container in moments. True multi-region (a cross-region read replica promotable on a regional outage) is the next maturity step — worth naming, not worth buying for a single college on day one.

Observability

You cannot keep the portal up in August if you cannot see it. Cloud Monitoring and Cloud Logging are the native floor: LB latency and 5xx rates, web-tier CPU and instance count, Cloud SQL connection count and replication lag, and Cloud Armor blocked-request counts — with alerting policies that page before students do. For a richer operating picture the college can run Dynatrace (or Datadog) across the stack for distributed tracing of a request from edge to database, automatic dependency mapping, and anomaly detection that flags a latency or error regression on its own. The metrics that actually matter here are p95 page latency, login success rate, Cloud SQL connection saturation, and cache hit-rate at Akamai — the four numbers that predict an August outage.

Build and deploy

Treat the whole thing as code so it is reproducible and reviewable. Terraform provisions the VPC, subnets, firewall rules, Load Balancer, Cloud Armor policy, MIG or Cloud Run service, Cloud SQL, Memorystore, the bucket, and the IAM bindings — the network and security are deliverables, not afterthoughts, and Wiz Code scans those Terraform plans in the pull request. Ansible can handle OS-level configuration on the MIG image (installing the PHP runtime, the Moodle app, OS agents) if you go the VM route. The application pipeline runs in GitHub Actions or Jenkins: build the Moodle container or VM image, run tests, and deploy — authenticating to GCP via Workload Identity Federation so there is no stored service-account key to leak. For teams that want declarative, auditable rollouts, Argo CD can drive deployments from Git as the source of truth. Whatever the tool, the principle is the same: no human clicks in the console for a production change, and every change is a reviewable, revertible commit. New environment changes pass a ServiceNow change approval before they go live, giving the college a documented gate.

Explicit tradeoffs

Accept these, or build the single VM and own its risks. The three-tier pattern adds more moving parts than one server — a load balancer, a separate managed database, a VPC with private networking, a secrets store, a cache — and each is one more thing to understand and provision. The private networking that makes it secure (no public IPs, Cloud NAT for egress, Private Service Access for Cloud SQL) is exactly where Junior builds get stuck, and a missing firewall rule or NAT looks like a silent hang, not a clear error. Splitting state out to Memorystore and Cloud Storage to keep the web tier stateless is discipline you must hold — the moment someone writes a session to a local disk, scale-to-zero and rolling updates start losing user data.

When the simpler thing is genuinely right: for a 50-student internal tool with no traffic spikes and no compliance pressure, a single well-backed-up VM (or a managed App Engine / Cloud Run service with a small Cloud SQL and nothing else) is the correct, cheaper answer — do not build a global load balancer for it. And at the other end, when the college grows to multi-region resilience, microservices, or GKE-based container orchestration, this foundational pattern is the base camp you grow from, not a dead end. The three tiers, the WAF at the edge, the private data tier, and the secret-not-in-a-file rule all carry forward unchanged.

Going deeper

The sections above are the map. This one is for the reader who will actually build it and wants to know where the sharp edges are — the internals, the version caveats, and the handful of settings that quietly decide whether August is calm or on fire.

What the “global” load balancer is actually doing

At the beginner level the load balancer is just “the front door.” Under the hood it is a fleet of Google Front Ends (GFEs) sitting in Google’s edge points-of-presence around the world, all answering the same anycast IP. A student in Manchester and a student in Singapore type the same address; the network routes each to the nearest GFE. That GFE terminates TLS, applies your Cloud Armor security policy, and then forwards the request over Google’s private backbone to a healthy backend in your region. That is precisely why it is called global: one IP, one certificate, one WAF policy, many edge locations.

Two correctness details trip up almost every first build:

Reaching a private database — the mechanisms, and the Cloud Run twist

“Cloud SQL has no public IP” is the goal; how the web tier reaches it depends on the tier engine. See Cloud SQL HA, read replicas, and private connectivity for the full treatment; the short version:

The connection-storm problem, in numbers

This is the single most important scaling nuance in the whole pattern: the web tier scales out, but the database scales up. A Cloud SQL instance has a finite max_connections that grows with the machine tier. Now do the arithmetic on a scale-out. If each web instance holds a pool of 20 connections and August takes you from 5 instances to 60, you have gone from 100 to 1,200 connections in minutes — and a mid-tier Cloud SQL may cap well below that. The instance starts refusing connections, and a traffic spike becomes an outage. This is exactly why Cloud Run’s effortless burst needs a watchful eye (Cloud Run deep dive covers its scaling knobs).

Three mitigations, in order of leverage:

  1. A connection pooler in the pathPgBouncer (Postgres) or ProxySQL (MySQL), or Cloud SQL’s own managed connection pooling where available — multiplexes thousands of thin client connections down onto a small pool of real database connections.
  2. Cap the multiplication at the source. On Cloud Run, --max-instances × per-instance pool size is your worst case; set both deliberately and raise concurrency so each instance serves more requests before a new one spins up. On a MIG, the autoscaler ceiling plays the same role.
  3. Offload reads to replicas. Moodle dashboards and course listings are read-heavy; Cloud SQL read replicas take that traffic off the primary so the primary’s connection budget is spent on writes.

Better than a stored password: IAM database authentication

Secret Manager removes the password from disk, but the password still exists — it can be read, logged, or leaked, and it must be rotated. The next rung up is to have no password at all. With IAM database authentication enabled (cloudsql_iam_authentication=on), the web tier’s service account authenticates to Cloud SQL with a short-lived OAuth token minted at connect time (the Auth Proxy or connector handles the token exchange for you). There is no static credential to store, rotate, or leak; access is granted and revoked through IAM like everything else in the project. For a security-conscious build this is the “even better” answer that the Secret Manager step is a stepping stone toward — and it is the subject of Practice challenge 6.

The tier you can’t blue/green: schema migrations

You can replace every web instance in seconds because they are stateless and identical. You cannot do that to the database — it holds the single copy of the truth. That is why a schema change is the riskiest deploy in a three-tier app, and why the expand/contract (a.k.a. parallel-change) pattern exists:

Never ship a migration that a still-running old instance cannot tolerate — during a rolling update, old and new run at the same time. This discipline is what lets the stateless tiers stay fast and fearless on top of a stateful one.

Quotas and limits to size before August, not during it

Autoscaling only helps if the ceilings above it are set high enough. Before a known spike, check and raise as needed: regional CPU and in-use IP-address quota (a MIG scaling to 60 VMs needs the vCPUs and the internal addresses), the Cloud SQL connection limit for the chosen tier, Serverless VPC connector throughput (it has a maximum Gbps and instance-count band), and Cloud Run max-instances per service and per region. Every one of these is a silent ceiling that turns “autoscaling will handle it” into “why did it stop at 40 instances at 09:00 on results day?” A ten-minute quota review in July is the cheapest reliability work you will ever do.

The shape of the win

Next August, enrollment opens and the portal does not fall over. Cloud Load Balancing spreads the surge across web instances that autoscale on their own; Akamai serves the cacheable course assets so most of the spike never reaches GCP; Cloud Armor turns away the bots and rate-limits the login floods; Cloud SQL’s standby is ready if a zone hiccups; and the database password the principal worried about lives in Secret Manager, read by a service account, present in no config file and no repo. The IT team spends August watching a Dynatrace dashboard instead of restarting a server at 3 a.m. None of this is exotic — it is the foundational three-tier pattern, built honestly on GCP. That is precisely why it is worth getting right: it is the architecture you will reach for again and again, and the day you stop losing sleep over August is the day it has paid for itself.

Practice challenges

Work these against a scratch project, or just write each command and check it against the solution. They escalate from wiring up private connectivity to eliminating the database password entirely. Placeholders (PROJECT_ID, service-account emails, IP ranges) are yours to fill in.

1. Give Cloud SQL a private address (beginner). Before an instance can have a private IP, the VPC needs a reserved range peered to Google’s service-networking. Reserve a /16 range named google-managed-services and create the private connection on a VPC called moodle-vpc.

<details> <summary>Solution</summary>

# 1. Reserve an internal range for Google-managed services to allocate from
gcloud compute addresses create google-managed-services \
  --global --purpose=VPC_PEERING --prefix-length=16 \
  --network=projects/PROJECT_ID/global/networks/moodle-vpc

# 2. Create the private connection (VPC peering) to servicenetworking
gcloud services vpc-peerings connect \
  --service=servicenetworking.googleapis.com \
  --ranges=google-managed-services \
  --network=moodle-vpc --project=PROJECT_ID

Why: Private Service Access works by peering your VPC with Google’s producer network. Without the reserved range and the peering, --network on a Cloud SQL instance has nowhere to draw a private IP from, and the create fails. </details>

2. Create the data tier with no public IP, HA, and point-in-time recovery (beginner→intermediate). Create a MySQL 8.0 Cloud SQL instance moodle-prod in europe-west2, private IP only, regional HA, backups on, and deletion protection.

<details> <summary>Solution</summary>

gcloud sql instances create moodle-prod \
  --database-version=MYSQL_8_0 \
  --region=europe-west2 \
  --tier=db-custom-4-15360 \
  --availability-type=REGIONAL \
  --no-assign-ip \
  --network=projects/PROJECT_ID/global/networks/moodle-vpc \
  --enable-bin-log \
  --backup-start-time=03:00 \
  --retained-transaction-log-days=7 \
  --deletion-protection

Why: --no-assign-ip + --network makes the instance reachable only over the private VPC (it depends on the peering from challenge 1). --availability-type=REGIONAL adds the standby in a second zone, and --enable-bin-log with backups is what unlocks point-in-time recovery. --deletion-protection stops a fat-fingered delete from dropping prod. </details>

3. Move the password into a vault the web tier alone can read (intermediate). Store the database password in Secret Manager as moodle-db-pass, then grant only the web tier’s service account read access — nothing broader.

<details> <summary>Solution</summary>

echo -n "$DB_PASSWORD" | gcloud secrets create moodle-db-pass \
  --replication-policy=automatic --data-file=-

gcloud secrets add-iam-policy-binding moodle-db-pass \
  --member="serviceAccount:moodle-web@PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"

Why: the value is piped from stdin (--data-file=-) so it never lands in a file or your shell history as an argument. Granting secretAccessor on the single secret to one service account is least privilege — the web tier can read this credential and nothing else, and no human or image ever holds the password. </details>

4. Express the tiering as firewall rules (intermediate). Write two rules on moodle-vpc: allow the load balancer’s health-check ranges into the web tier on tcp:8080, and allow the web tier (tag web) to egress to the Cloud SQL private range on tcp:3306.

<details> <summary>Solution</summary>

# Let Google's health-check + LB proxy ranges probe the web tier
gcloud compute firewall-rules create allow-hc-to-web \
  --network=moodle-vpc --direction=INGRESS --action=ALLOW \
  --rules=tcp:8080 \
  --source-ranges=35.191.0.0/16,130.211.0.0/22 \
  --target-tags=web

# Let the web tier reach Cloud SQL's private range on the MySQL port
gcloud compute firewall-rules create allow-web-to-sql \
  --network=moodle-vpc --direction=EGRESS --action=ALLOW \
  --rules=tcp:3306 \
  --destination-ranges=10.60.0.0/24 \
  --target-tags=web

Why: those two source ranges are the Google health-check/proxy ranges — omit them and every backend shows unhealthy and the LB serves 502s. Scoping the DB rule to the web tag and the SQL range (not 0.0.0.0/0) is the whole point of tiering: only the web tier may talk to the database, and only on 3306. </details>

5. Put a WAF and a login rate-limit in front (intermediate→advanced). Create a Cloud Armor policy moodle-armor that blocks SQL-injection attempts and throttles any single client IP to 100 requests/minute, then attach it to the backend service moodle-web-backend.

<details> <summary>Solution</summary>

gcloud compute security-policies create moodle-armor \
  --description="WAF + rate limiting for Moodle"

# Preconfigured OWASP SQL-injection ruleset
gcloud compute security-policies rules create 1000 \
  --security-policy=moodle-armor \
  --expression="evaluatePreconfiguredWaf('sqli-v33-stable')" \
  --action=deny-403

# Per-IP rate limit: >100 req/60s from one IP gets 429s
gcloud compute security-policies rules create 2000 \
  --security-policy=moodle-armor \
  --src-ip-ranges="*" --action=throttle \
  --rate-limit-threshold-count=100 \
  --rate-limit-threshold-interval-sec=60 \
  --conform-action=allow --exceed-action=deny-429 \
  --enforce-on-key=IP

gcloud compute backend-services update moodle-web-backend \
  --security-policy=moodle-armor --global

Why: the WAF rule is evaluated at Google’s edge before traffic reaches your backends, so injection probes never touch Moodle. The throttle rule keyed on IP is what stops a single machine from brute-forcing the login endpoint during enrollment — the rate limit is enforced at the edge, not by your app. </details>

6. Delete the password entirely with IAM database authentication (advanced). Turn on IAM auth on moodle-prod, create a database user that is the web tier’s service account, and grant it the roles to log in and connect — so there is no stored password at all.

<details> <summary>Solution</summary>

# Enable IAM authentication on the instance
gcloud sql instances patch moodle-prod \
  --database-flags=cloudsql_iam_authentication=on

# Create a DB user that maps to the service account
# (drop the .gserviceaccount.com suffix for the username)
gcloud sql users create moodle-web@PROJECT_ID.iam \
  --instance=moodle-prod \
  --type=CLOUD_IAM_SERVICE_ACCOUNT

# Grant the SA the roles to authenticate and connect
gcloud projects add-iam-policy-binding PROJECT_ID \
  --member="serviceAccount:moodle-web@PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/cloudsql.instanceUser"
gcloud projects add-iam-policy-binding PROJECT_ID \
  --member="serviceAccount:moodle-web@PROJECT_ID.iam.gserviceaccount.com" \
  --role="roles/cloudsql.client"

Why: with IAM auth on, the app connects using a short-lived OAuth token (minted by the Auth Proxy/connector) instead of a static password — there is nothing to store in Secret Manager, nothing to rotate, and nothing to leak. instanceUser lets the SA log in as a database principal; client lets it open the connection. This is the security ceiling that challenge 3’s Secret Manager step points toward. </details>

Common beginner mistakes

Glossary

GCPCloud Load BalancingCloud SQLCloud ArmorThree-TierFoundational
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments