Skip to content
Learni
View all tutorials
Google Cloud

How to Configure Cloud Armor with Terraform in 2026

Lire en français

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 login and export GOOGLE_CREDENTIALS=/path/to/key.json.
  • Existing GCP project (replace your-project-id).

Initialize the Terraform project

setup.sh
#!/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 init

This 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

providers.tf
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

storage.tf
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

security-policy.tf
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

load-balancer.tf
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

deploy.sh
#!/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-approve

Validates, 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.
Inject 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_count via Cloud Monitoring.

Common errors to avoid

  • API not enabled: CloudArmorApi → 403 error on security-policy create.
  • CEL syntax error: Policy stuck in FAILED; validate expr before commit.
  • Bucket collision: Non-unique name → ALREADY_EXISTS; always use random_id.
  • IAM permissions: Service account missing compute.securityPolicies.create → 403 auth.

Next steps

How to Configure Cloud Armor with Terraform 2026 | Learni