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 configlive/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 configlive/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