IaC GCP

Terraform Module: GCP Cloud IDS — managed IDS endpoints wired to packet mirroring in one call

Quick take — Build a reusable Terraform module for GCP Cloud IDS: a private-services-access-backed google_cloud_ids_endpoint with tunable threat severity, wired to packet mirroring so you get east-west IDS without managing sensors. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.

Quickstart (copy-paste)

Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):

provider "google" {
  project = "my-project"
  region  = "us-central1"
}

module "cloud_ids" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-cloud-ids?ref=v1.0.0"

  project_id = "..."  # GCP project ID hosting the endpoint and VPC.
  name       = "..."  # Endpoint name (RFC1035), also the PSA range prefix.
  location   = "..."  # Region for the endpoint (must support Cloud IDS).
  network    = "..."  # Self-link/name of the VPC to monitor.
}

Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.

What this module is

Google Cloud IDS (Intrusion Detection System) is a managed, network-based threat detection service built on Palo Alto Networks’ threat-analysis engine. You deploy a Cloud IDS endpoint into a VPC region, point a packet mirroring policy at it, and Cloud IDS inspects a mirrored copy of your VPC traffic for intrusions, malware, spyware, and command-and-control activity — surfacing findings in the Cloud IDS console and Cloud Logging without you ever provisioning, patching, or scaling a single sensor VM.

The catch is that a working Cloud IDS deployment is never just the endpoint. The google_cloud_ids_endpoint resource has a hard prerequisite: the consuming VPC must already have Private Services Access configured (a reserved IP range plus a VPC peering to Google’s service producer network) before the endpoint can be created. The endpoint also does nothing on its own until a packet mirroring policy forwards traffic to it. Wrapping all of this in a module means a team can stand up regional IDS coverage by passing a network self-link and a severity threshold — instead of re-deriving the PSA-then-endpoint-then-mirroring dance every time, and getting the ordering subtly wrong.

This module provisions the reserved range, the service-networking connection, and the google_cloud_ids_endpoint itself, and exposes the endpoint’s self-link as the forwarding rule that a packet mirroring policy targets.

When to use it

Module structure

terraform-module-gcp-cloud-ids/
├── versions.tf
├── main.tf
├── variables.tf
└── outputs.tf

versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }
}

main.tf

locals {
  # Cloud IDS requires the Service Networking + Cloud IDS APIs. We expose a
  # toggle so platform teams that manage APIs centrally can opt out.
  required_services = var.enable_apis ? toset([
    "servicenetworking.googleapis.com",
    "ids.googleapis.com",
  ]) : toset([])
}

resource "google_project_service" "this" {
  for_each = local.required_services

  project            = var.project_id
  service            = each.value
  disable_on_destroy = false
}

# ---------------------------------------------------------------------------
# Private Services Access — hard prerequisite for a Cloud IDS endpoint.
# We reserve an IP range in the consuming VPC and peer it to Google's
# service producer network via service-networking.
# ---------------------------------------------------------------------------
resource "google_compute_global_address" "psa_range" {
  project       = var.project_id
  name          = "${var.name}-psa-range"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = var.psa_prefix_length
  network       = var.network

  depends_on = [google_project_service.this]
}

resource "google_service_networking_connection" "psa" {
  network                 = var.network
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.psa_range.name]
}

# ---------------------------------------------------------------------------
# The Cloud IDS endpoint. Inspects mirrored traffic at or above the
# configured minimum severity. This is a regional, zonal-anchored resource.
# ---------------------------------------------------------------------------
resource "google_cloud_ids_endpoint" "this" {
  project  = var.project_id
  name     = var.name
  location = var.location
  network  = var.network
  severity = var.severity

  # When true, threat exceptions/allowlisted IPs configured in the IDS
  # console are honoured; useful to suppress known-benign scanners.
  threat_exceptions = var.threat_exceptions

  description = var.description

  depends_on = [google_service_networking_connection.psa]
}

variables.tf

variable "project_id" {
  description = "GCP project ID that hosts the Cloud IDS endpoint and the consuming VPC."
  type        = string
}

variable "name" {
  description = "Name of the Cloud IDS endpoint. Also used as a prefix for the PSA reserved range."
  type        = string

  validation {
    condition     = can(regex("^[a-z]([-a-z0-9]{0,61}[a-z0-9])?$", var.name))
    error_message = "name must be 1-63 chars, lowercase RFC1035: start with a letter, contain only lowercase letters, digits and hyphens."
  }
}

variable "location" {
  description = "Region for the Cloud IDS endpoint (e.g. asia-south1, us-central1). Must be a region where Cloud IDS is available."
  type        = string
}

variable "network" {
  description = "Self-link or name of the VPC network to monitor. Packet mirroring sources must live in this network."
  type        = string
}

variable "severity" {
  description = "Minimum threat severity the endpoint generates alerts for. Lower thresholds detect more but are noisier."
  type        = string
  default     = "INFORMATIONAL"

  validation {
    condition     = contains(["INFORMATIONAL", "LOW", "MEDIUM", "HIGH", "CRITICAL"], var.severity)
    error_message = "severity must be one of INFORMATIONAL, LOW, MEDIUM, HIGH, CRITICAL."
  }
}

variable "threat_exceptions" {
  description = "List of threat IDs to exclude from alerting (suppress known-benign signatures). Empty means alert on everything at or above severity."
  type        = list(string)
  default     = []
}

variable "description" {
  description = "Free-text description stored on the endpoint."
  type        = string
  default     = "Managed by Terraform — KloudVin cloud-ids module"
}

variable "psa_prefix_length" {
  description = "Prefix length of the reserved Private Services Access range. /24 is the recommended minimum for Cloud IDS."
  type        = number
  default     = 24

  validation {
    condition     = var.psa_prefix_length >= 16 && var.psa_prefix_length <= 24
    error_message = "psa_prefix_length must be between 16 and 24; Cloud IDS requires at least a /24."
  }
}

variable "enable_apis" {
  description = "When true, the module enables the servicenetworking and ids APIs on the project. Set false if APIs are managed centrally."
  type        = bool
  default     = true
}

outputs.tf

output "endpoint_id" {
  description = "Fully qualified resource ID of the Cloud IDS endpoint."
  value       = google_cloud_ids_endpoint.this.id
}

output "endpoint_name" {
  description = "Short name of the Cloud IDS endpoint."
  value       = google_cloud_ids_endpoint.this.name
}

output "endpoint_forwarding_rule" {
  description = "Self-link of the IDS forwarding rule. Use this as the collector_ilb of a google_compute_packet_mirroring policy."
  value       = google_cloud_ids_endpoint.this.endpoint_forwarding_rule
}

output "endpoint_ip" {
  description = "Internal IP address allocated to the Cloud IDS endpoint."
  value       = google_cloud_ids_endpoint.this.endpoint_ip
}

output "psa_range_name" {
  description = "Name of the reserved Private Services Access range backing the endpoint."
  value       = google_compute_global_address.psa_range.name
}

How to use it

The module gives you a working endpoint; you still attach a packet mirroring policy downstream to actually feed it traffic. The endpoint’s endpoint_forwarding_rule output is exactly the collector_ilb a mirroring policy needs.

module "cloud_ids" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-cloud-ids?ref=v1.0.0"

  project_id        = "kv-prod-network"
  name              = "ids-asia-south1"
  location          = "asia-south1"
  network           = google_compute_network.prod.id
  severity          = "LOW"          # alert on LOW and above in production
  psa_prefix_length = 24

  # Suppress a noisy internal vulnerability scanner's signatures
  threat_exceptions = ["41040", "41041"]
}

# Downstream: mirror subnet traffic into the IDS endpoint using its
# forwarding-rule output as the collector.
resource "google_compute_packet_mirroring" "to_ids" {
  name    = "mirror-to-ids-asia-south1"
  project = "kv-prod-network"
  region  = "asia-south1"

  network {
    url = google_compute_network.prod.id
  }

  collector_ilb {
    url = module.cloud_ids.endpoint_forwarding_rule
  }

  mirrored_resources {
    subnetworks {
      url = google_compute_subnetwork.workloads.id
    }
  }

  filter {
    ip_protocols = ["tcp", "udp"]
    direction    = "BOTH"
  }
}

With Terragrunt

Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.

1. Root config — live/terragrunt.hcl (inherited by every module):

remote_state {
  backend = "gcs"
  generate = { path = "backend.tf", if_exists = "overwrite" }
  config = {
    # ...gcs state bucket/container + key per path...
  }
}

2. Module config — live/prod/cloud_ids/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-cloud-ids?ref=v1.0.0"
}

inputs = {
  project_id = "..."
  name = "..."
  location = "..."
  network = "..."
}

3. Deploy one environment, or roll out all modules together:

cd live/prod/cloud_ids && terragrunt apply        # this module
terragrunt run-all apply                      # every module under live/prod

Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.

Inputs

Name Type Default Required Description
project_id string — Yes GCP project ID hosting the endpoint and VPC.
name string — Yes Endpoint name (RFC1035), also the PSA range prefix.
location string — Yes Region for the endpoint (must support Cloud IDS).
network string — Yes Self-link/name of the VPC to monitor.
severity string "INFORMATIONAL" No Minimum alert severity: INFORMATIONAL, LOW, MEDIUM, HIGH, CRITICAL.
threat_exceptions list(string) [] No Threat IDs to suppress from alerting.
description string "Managed by Terraform — KloudVin cloud-ids module" No Free-text description on the endpoint.
psa_prefix_length number 24 No Prefix length of the reserved PSA range (16–24; ≥/24 required).
enable_apis bool true No Enable servicenetworking + ids APIs on the project.

Outputs

Name Description
endpoint_id Fully qualified resource ID of the Cloud IDS endpoint.
endpoint_name Short name of the endpoint.
endpoint_forwarding_rule Self-link of the IDS forwarding rule; use as collector_ilb of a packet mirroring policy.
endpoint_ip Internal IP allocated to the endpoint.
psa_range_name Name of the reserved Private Services Access range.

Enterprise scenario

A fintech running its payment-processing workloads in a Shared VPC must demonstrate continuous intrusion detection on its PCI-DSS in-scope subnets across asia-south1 and asia-south1 (Mumbai) plus a us-central1 DR region. The platform team calls this module three times — once per region — from the network host project, then attaches per-environment packet mirroring policies that scope mirroring to only the cardholder-data subnetworks. Cloud IDS findings flow into a central Cloud Logging sink and on to Chronicle SIEM, giving auditors a code-defined, reproducible IDS control with the severity set to LOW so reconnaissance scans against the CDE are caught early.

Best practices

TerraformGCPCloud IDSModuleIaC
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

Keep Reading