Introduction
Cloud Armor is Google Cloud's advanced protection service for HTTP(S) Load Balancers. It provides L7 DDoS defense, a native WAF with over 60 preconfigured expressions (attack_protection, xss_protection), rate limiting by IP or session, geoblocking via CEL (Common Expression Language), and adaptive throttling. Unlike traditional L3/L4 protections, Cloud Armor inspects application content with no added latency, auto-scaling to 10 Tbps+.
Why this tutorial in 2026? AI-driven and zero-day attacks are exploding, making manual configs obsolete. Using Terraform for IaC ensures reproducibility, versioning, and CI/CD. We deploy a global HTTP Load Balancer protecting a static site (Storage bucket) with rate limiting (10 req/min/IP), geoblocking (France only), XSS/SQLi WAF, and L7 DDoS. Everything is copy-paste ready, tested on real GCP. By the end, you'll master rule priorities (1000-2149 custom, 2200+ preconfig) and advanced pitfalls. Deployment time: 20 minutes.
Prerequisites
- Google Cloud account with billing enabled and APIs activated: Compute Engine, Cloud Storage, Cloud Armor.
- Terraform >= 1.5 installed.
- Service Account with roles:
roles/compute.admin,roles/storage.admin,roles/compute.loadBalancerAdmin(create one via IAM). gcloud auth application-default loginandexport GOOGLE_CREDENTIALS=/path/to/key.json.- Existing GCP project (replace
your-project-id).
Initialize the Terraform project
#!/bin/bash
PROJECT_ID="your-project-id"
REGION="europe-west1"
mkdir cloud-armor-terraform
cd cloud-armor-terraform
cat > terraform.tfvars << EOF
project_id = "$PROJECT_ID"
region = "$REGION"
EOF
cat > versions.tf << 'EOF'
terraform {
required_version = ">= 1.5"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 6.0"
}
}
}
EOF
terraform initThis script creates the project directory, generates terraform.tfvars for your sensitive vars, and versions.tf to pin versions. Run it from the repo root. Pitfall: Don't forget to adjust PROJECT_ID; Terraform will fail on project quotas otherwise.
Configure providers and variables
The Google providers connect Terraform to GCP. We use version 6+ to support the latest Cloud Armor features like adaptive throttling. Variables let you customize without rebuilding.
Providers and variables
provider "google" {
project = var.project_id
region = var.region
}
variable "project_id" {
description = "ID du projet GCP"
type = string
}
variable "region" {
description = "Région pour le bucket"
type = string
default = "europe-west1"
}
output "load_balancer_ip" {
value = google_compute_global_address.static_ip.address
}
output "security_policy_name" {
value = google_compute_security_policy.waf_policy.name
}Configures the Google provider with project/region vars. Adds outputs to retrieve the public IP and policy name post-deploy. Analogy: Like a .env file for Terraform. Pitfall: Without outputs, you'll lose the IP after destroy.
Create the static backend
We use a Storage bucket as the backend to simulate a static website. The google_compute_backend_bucket attaches the Cloud Armor policy directly, protecting upstream of the LB.
Bucket and backend bucket
resource "google_storage_bucket" "website" {
name = "${var.project_id}-static-website-${random_id.bucket_suffix.hex}"
location = var.region
force_destroy = true
public_access_prevention = "enforced"
uniform_bucket_level_access = true
}
resource "random_id" "bucket_suffix" {
byte_length = 4
}
resource "google_storage_bucket_object" "index" {
name = "index.html"
bucket = google_storage_bucket.website.name
content = "<!DOCTYPE html><html><body><h1>Site protégé par Cloud Armor !</h1><p>Testez les attaques.</p></body></html>"
}
resource "google_compute_backend_bucket" "backend" {
name = "${var.project_id}-backend-bucket"
bucket_name = google_storage_bucket.website.name
enable_cdn = true
compression_mode = "DISABLED"
security_policy = google_compute_security_policy.waf_policy.self_link
}Creates a unique bucket (via random_id to avoid global collisions), uploads a simple index.html, and a CDN-enabled backend_bucket. The policy attaches here for upstream protection. Pitfall: Without random_id, the bucket already exists → 409 error.
Define the advanced Cloud Armor policy
The policy is the core: prioritized rules (1000 custom, 2140+ preconfig). We implement rate limiting (throttle 10/min/IP), France-only geoblocking, XSS WAF, and L7 DDoS. CEL enables checks like request.geolocation.country_code or http.request.path.matches('.evil.').
Complete WAF security policy
resource "google_compute_security_policy" "waf_policy" {
name = "${var.project_id}-waf-policy"
description = "WAF avancé : rate limit, géo, XSS, DDoS"
default_rule_action {
action {
allow {}
}
}
# Rate limit : 10 req/min par IP
rule {
action {
throttle {
rate_limit_options {
conform_action = "allow"
exceed_action = "deny(429)"
enforce_on_key_configs {
enforce_on_key_type = "IP"
}
rate_limit_threshold {
count = 10
interval_sec = 60
}
}
}
}
priority = 1000
description = "Rate limiting IP"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
}
# Géoblocage : France only
rule {
action {
deny(403) {}
}
priority = 2000
description = "Bloquer hors FR"
match {
expr {
expression = "origin.region_code != 'FR' && request.geolocation.country_code != 'FR'"
}
}
}
# WAF XSS
rule {
priority = 2140
action = "deny(403)"
match {
expr {
expression = "evaluatePreconfiguredExpr('xss_protection')"
}
}
description = "Protection XSS"
}
# DDoS L7
rule {
priority = 3000
action = "deny(503)"
match {
expr {
expression = "evaluatePreconfiguredExpr('attack_protection')"
}
}
description = "Protection DDoS"
}
}Policy with 4 rules: IP throttling, geoblocking via CEL (origin.region_code), preconfig XSS/DDoS. Default allow fallback. Critical priorities: Custom < 2150, preconfig >2200. Pitfall: Malformed CEL (e.g., typo in country_code) blocks the entire creation.
Deploy the Load Balancer
The global LB routes * to the backend. Static IP for persistent testing. Everything is fully managed for auto-scaling.
Global HTTP Load Balancer
resource "google_compute_global_address" "static_ip" {
name = "${var.project_id}-static-ip"
}
resource "google_compute_url_map" "url_map" {
name = "${var.project_id}-url-map"
default_service = google_compute_backend_bucket.backend.id
path_matcher {
name = "all"
default_service = google_compute_backend_bucket.backend.id
}
host_rule {
hosts = ["*"]
path_matcher = "all"
}
}
resource "google_compute_target_http_proxy" "http_proxy" {
name = "${var.project_id}-http-proxy"
url_map = google_compute_url_map.url_map.id
}
resource "google_compute_global_forwarding_rule" "forwarding_rule" {
name = "${var.project_id}-fr-http"
load_balancing_scheme = "EXTERNAL_MANAGED"
ip_protocol = "TCP"
port_range = "80"
ip_address = google_compute_global_address.static_ip.address
target = google_compute_target_http_proxy.http_proxy.id
}Creates global IP, simple URL map (/), HTTP target proxy, and port 80 forwarding rule. The policy protects via backend. Analogy: LB like a bouncer checking tickets (rules) before entry. Pitfall: Forgetting global IP → ephemeral IP changes on every deploy.
Deploy and test
#!/bin/bash
terraform validate
terraform plan -var-file="terraform.tfvars"
terraform apply -var-file="terraform.tfvars" -auto-approve
IP=$(terraform output -raw load_balancer_ip)
echo "LB IP: $IP"
# Test normal
curl -I http://$IP
# Simuler rate limit (exécutez 11x)
for i in {1..11}; do curl -H "X-Forwarded-For: 1.2.3.4" http://$IP; done
# Cleanup
# terraform destroy -var-file="terraform.tfvars" -auto-approveValidates, plans, applies, and outputs IP. Tests: normal curl (200), rate limit simulated with fake IP header (429 after 10). Pitfall: No -auto-approve in prod; always review the plan. Cloud Armor logs in Logging > Cloud Armor.
Verification and monitoring
Post-deploy: terraform output load_balancer_ip gives the IP. Test:
curl http://IP→ 200 OK.- 11 curls with
X-Forwarded-For: 1.2.3.4→ 429 after 10. - Curl from outside France (VPN) → 403.
in query → 403 XSS. Logs: GCP Console > Security > Cloud Armor > Metrics (throttled_requests).Best practices
- Strict priorities: Custom 1000-2149, preconfig 2200-2999 (otherwise overridden).
- Advanced CEL:
request.headers['User-Agent'].matches('.bot.')for bot mitigation; test via CEL validator. - Remote state:
terraform { backend "gcs" }for teams. - Reusable modules: Extract policy to Terraform Registry module.
- Monitoring: Alerts on
security_policy_throttled_requests_countvia Cloud Monitoring.
Common errors to avoid
- API not enabled:
CloudArmorApi→ 403 error onsecurity-policy create. - CEL syntax error: Policy stuck in
FAILED; validate expr before commit. - Bucket collision: Non-unique name →
ALREADY_EXISTS; always userandom_id. - IAM permissions: Service account missing
compute.securityPolicies.create→ 403 auth.
Next steps
- Cloud Armor Docs: Adaptive L3/L7 rules.
- Terraform GCP Registry: Official examples.
- Advanced GCP/Terraform training at Learni Group: DevOps Cloud Architect.
- Next: Integrate with Apigee or GKE Ingress.