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
- You want east-west and north-south IDS for workloads in a VPC without deploying or managing NGFW sensor VMs.
- You are running PCI-DSS, HIPAA, or similar regimes that require documented intrusion detection on in-scope subnets, and you want that control expressed as code.
- You need IDS coverage that scales automatically with traffic up to the endpoint’s throughput tier rather than capacity-planning appliances.
- You operate a hub-and-spoke or Shared VPC topology and want a repeatable per-region IDS endpoint that platform teams can attach packet mirroring to.
- You do not need inline prevention/blocking — Cloud IDS is detection-only (it inspects mirrored traffic). If you need inline IPS that drops packets, use Cloud NGFW / a firewall appliance instead.
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
- Tune
severityper environment, not globally. Run production atLOWto catch reconnaissance, but considerMEDIUM/HIGHin noisy dev VPCs — Cloud IDS bills per endpoint-hour and per GiB inspected, so flooding low-value traffic at INFORMATIONAL wastes spend and buries real findings. - Scope packet mirroring tightly. Mirror only the subnetworks or tagged instances that matter (PCI/PHI subnets, internet-facing tiers). Mirroring an entire VPC inflates inspection volume and cost, and the mirrored traffic counts against the endpoint’s throughput tier.
- Treat the PSA range as permanent infrastructure. The reserved
/24and service-networking peering underpin the endpoint; deleting them orphans the endpoint. Keep the range in a stable, documented CIDR block and never reuse it for other peered services. - One endpoint per region, shared across workloads. A Cloud IDS endpoint is regional — don’t create one per app. Stand up a single endpoint per region in the network host project and let multiple mirroring policies feed it.
- Pin the module by tag and the provider by
~> 5.0. Cloud IDS endpoints can take 20+ minutes to create/destroy; a pinnedref=v1.0.0and a pinned provider keep replacements predictable and prevent an accidental destroy/recreate during a routine apply. - Route findings off-platform. Export Cloud IDS logs to a dedicated Cloud Logging bucket with a sink to your SIEM (Chronicle/Splunk). Detection without alerting delivery is not a control auditors will accept.