IaC GCP

Terraform Module: GCP Memorystore (Redis) — private, HA-ready cache in one block

Quick take — A reusable hashicorp/google ~> 5.0 module for google_redis_instance: STANDARD_HA tiers, private services access, AUTH, in-transit TLS, maintenance windows, and read replicas — wired up for production. 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 "memorystore" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-memorystore?ref=v1.0.0"

  project_id = "..."  # Project that owns the instance.
  name       = "..."  # Instance ID; 2-40 chars, starts with a letter.
  region     = "..."  # Region, e.g. `asia-south1`.
  network_id = "..."  # Authorized VPC network self-link/ID.
}

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

What this module is

Memorystore for Redis is Google Cloud’s fully managed, in-memory Redis service. You get a Redis-protocol-compatible endpoint with Google handling patching, failover, and replication, and you reach it over a private IP inside your VPC rather than a public endpoint. It is the default choice on GCP for session stores, rate-limiter counters, hot-key caches in front of Cloud SQL or Spanner, and Pub/Sub-style fan-out at low latency.

A single google_redis_instance looks deceptively simple, but a correct production instance pulls in a cluster of decisions that are easy to get wrong and tedious to repeat: choosing STANDARD_HA over BASIC so you actually get a replica and an SLA, reserving an IP range and creating the private services access peering so the instance is reachable, turning on AUTH and transit_encryption_mode = SERVER_AUTHENTICATION so the cache isn’t an unauthenticated plaintext free-for-all, pinning a Redis version, setting redis_configs like maxmemory-policy, and scheduling a maintenance window so Google reboots happen at 3 AM and not during your peak.

Wrapping all of that in a module means every cache your teams stand up is HA-by-default, private-by-default, and authenticated-by-default — and you flip tier or replica_count with one variable instead of re-litigating networking in every repo.

When to use it

Module structure

terraform-module-gcp-memorystore/
├── versions.tf      # provider + Terraform version pins
├── main.tf          # google_redis_instance + private services access
├── variables.tf     # var-driven inputs with validation
└── outputs.tf       # id/name, host, port, auth string, read endpoint

versions.tf

terraform {
  required_version = ">= 1.5.0"

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

main.tf

locals {
  # Memorystore needs a /29 reserved range for private services access.
  create_psa = var.create_private_service_access

  redis_configs = merge(
    {
      "maxmemory-policy" = var.maxmemory_policy
    },
    var.redis_configs
  )
}

# --- Private services access (VPC peering) ------------------------------------
# Memorystore connects via a reserved IP range that is peered to the
# servicenetworking.googleapis.com service. Created only when requested so the
# module can also consume a range that already exists in shared-VPC setups.
resource "google_compute_global_address" "psa_range" {
  count = local.create_psa ? 1 : 0

  name          = "${var.name}-psa-range"
  project       = var.project_id
  network       = var.network_id
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  prefix_length = var.reserved_ip_prefix_length
}

resource "google_service_networking_connection" "psa" {
  count = local.create_psa ? 1 : 0

  network                 = var.network_id
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.psa_range[0].name]
}

# --- The Redis instance -------------------------------------------------------
resource "google_redis_instance" "this" {
  project = var.project_id
  name    = var.name
  region  = var.region

  display_name   = var.display_name
  tier           = var.tier
  memory_size_gb = var.memory_size_gb
  redis_version  = var.redis_version

  # Networking: PRIVATE_SERVICE_ACCESS keeps the endpoint off the
  # consumer subnet and inside the peered range above.
  authorized_network      = var.network_id
  connect_mode            = "PRIVATE_SERVICE_ACCESS"
  reserved_ip_range       = local.create_psa ? google_compute_global_address.psa_range[0].name : var.reserved_ip_range
  location_id             = var.location_id
  alternative_location_id = var.tier == "STANDARD_HA" ? var.alternative_location_id : null

  # Read replicas (STANDARD_HA only). replica_count is honored only when
  # read replicas are enabled.
  read_replicas_mode = var.read_replicas_mode
  replica_count = (
    var.read_replicas_mode == "READ_REPLICAS_ENABLED" ? var.replica_count : null
  )

  # Security
  auth_enabled            = var.auth_enabled
  transit_encryption_mode = var.transit_encryption_mode
  customer_managed_key    = var.customer_managed_key

  redis_configs = local.redis_configs

  # Persistence (RDB snapshots). Off by default for pure caches.
  dynamic "persistence_config" {
    for_each = var.persistence_mode == "DISABLED" ? [] : [1]
    content {
      persistence_mode        = var.persistence_mode
      rdb_snapshot_period     = var.rdb_snapshot_period
      rdb_snapshot_start_time = var.rdb_snapshot_start_time
    }
  }

  # Maintenance window: pin Google-initiated reboots to a quiet hour.
  dynamic "maintenance_policy" {
    for_each = var.maintenance_window == null ? [] : [var.maintenance_window]
    content {
      weekly_maintenance_window {
        day = maintenance_policy.value.day
        start_time {
          hours   = maintenance_policy.value.hours
          minutes = maintenance_policy.value.minutes
          seconds = 0
          nanos   = 0
        }
      }
    }
  }

  labels = var.labels

  # The peering must exist before the instance can attach to the range.
  depends_on = [google_service_networking_connection.psa]
}

variables.tf

variable "project_id" {
  description = "Project ID that will own the Memorystore instance."
  type        = string
}

variable "name" {
  description = "Instance ID. Lowercase letters, numbers and hyphens; must start with a letter."
  type        = string

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{0,38}[a-z0-9]$", var.name))
    error_message = "name must be 2-40 chars, lowercase alphanumeric/hyphen, start with a letter."
  }
}

variable "region" {
  description = "Region for the instance, e.g. asia-south1."
  type        = string
}

variable "display_name" {
  description = "Human-friendly name shown in the console."
  type        = string
  default     = null
}

variable "network_id" {
  description = "Self-link or ID of the authorized VPC network (e.g. projects/p/global/networks/vpc)."
  type        = string
}

variable "tier" {
  description = "BASIC (single node, no SLA) or STANDARD_HA (replica + automatic failover)."
  type        = string
  default     = "STANDARD_HA"

  validation {
    condition     = contains(["BASIC", "STANDARD_HA"], var.tier)
    error_message = "tier must be BASIC or STANDARD_HA."
  }
}

variable "memory_size_gb" {
  description = "Memory capacity in GB (1-300). Capacity also caps network throughput."
  type        = number
  default     = 5

  validation {
    condition     = var.memory_size_gb >= 1 && var.memory_size_gb <= 300
    error_message = "memory_size_gb must be between 1 and 300."
  }
}

variable "redis_version" {
  description = "Redis engine version, e.g. REDIS_7_2."
  type        = string
  default     = "REDIS_7_2"

  validation {
    condition     = can(regex("^REDIS_[0-9]+(_[0-9]+)?$", var.redis_version))
    error_message = "redis_version must look like REDIS_7_2."
  }
}

variable "location_id" {
  description = "Primary zone for the instance (e.g. asia-south1-a). Defaults to a region-chosen zone when null."
  type        = string
  default     = null
}

variable "alternative_location_id" {
  description = "Secondary zone for the STANDARD_HA replica. Must differ from location_id."
  type        = string
  default     = null
}

variable "read_replicas_mode" {
  description = "READ_REPLICAS_DISABLED or READ_REPLICAS_ENABLED (STANDARD_HA only)."
  type        = string
  default     = "READ_REPLICAS_DISABLED"

  validation {
    condition     = contains(["READ_REPLICAS_DISABLED", "READ_REPLICAS_ENABLED"], var.read_replicas_mode)
    error_message = "read_replicas_mode must be READ_REPLICAS_DISABLED or READ_REPLICAS_ENABLED."
  }
}

variable "replica_count" {
  description = "Number of read replicas (1-5) when read_replicas_mode is enabled."
  type        = number
  default     = 1

  validation {
    condition     = var.replica_count >= 1 && var.replica_count <= 5
    error_message = "replica_count must be between 1 and 5."
  }
}

variable "auth_enabled" {
  description = "Enable Redis AUTH (generates an auth string). Strongly recommended."
  type        = bool
  default     = true
}

variable "transit_encryption_mode" {
  description = "SERVER_AUTHENTICATION (TLS) or DISABLED."
  type        = string
  default     = "SERVER_AUTHENTICATION"

  validation {
    condition     = contains(["SERVER_AUTHENTICATION", "DISABLED"], var.transit_encryption_mode)
    error_message = "transit_encryption_mode must be SERVER_AUTHENTICATION or DISABLED."
  }
}

variable "customer_managed_key" {
  description = "Optional CMEK KMS key resource ID for encryption at rest."
  type        = string
  default     = null
}

variable "maxmemory_policy" {
  description = "Eviction policy applied via redis_configs (e.g. allkeys-lru, noeviction, volatile-ttl)."
  type        = string
  default     = "allkeys-lru"
}

variable "redis_configs" {
  description = "Extra redis.conf overrides merged with maxmemory-policy (e.g. notify-keyspace-events)."
  type        = map(string)
  default     = {}
}

variable "persistence_mode" {
  description = "DISABLED or RDB (point-in-time snapshots)."
  type        = string
  default     = "DISABLED"

  validation {
    condition     = contains(["DISABLED", "RDB"], var.persistence_mode)
    error_message = "persistence_mode must be DISABLED or RDB."
  }
}

variable "rdb_snapshot_period" {
  description = "RDB snapshot cadence: ONE_HOUR, SIX_HOURS, TWELVE_HOURS or TWENTY_FOUR_HOURS."
  type        = string
  default     = "TWENTY_FOUR_HOURS"
}

variable "rdb_snapshot_start_time" {
  description = "RFC3339 start time for the first RDB snapshot, e.g. 2026-06-09T20:00:00Z."
  type        = string
  default     = null
}

variable "maintenance_window" {
  description = "Weekly maintenance window. Null lets Google pick. day is e.g. SUNDAY; hours/minutes are UTC."
  type = object({
    day     = string
    hours   = number
    minutes = number
  })
  default = {
    day     = "SUNDAY"
    hours   = 21
    minutes = 0
  }
}

variable "create_private_service_access" {
  description = "Create the reserved range + service networking peering. Set false to reuse an existing PSA range."
  type        = bool
  default     = true
}

variable "reserved_ip_prefix_length" {
  description = "Prefix length for the reserved PSA range. /29 is the Memorystore minimum."
  type        = number
  default     = 29
}

variable "reserved_ip_range" {
  description = "Name of an existing reserved range to use when create_private_service_access is false."
  type        = string
  default     = null
}

variable "labels" {
  description = "Resource labels for cost allocation and ownership."
  type        = map(string)
  default     = {}
}

outputs.tf

output "id" {
  description = "Fully qualified instance ID (projects/<p>/locations/<region>/instances/<name>)."
  value       = google_redis_instance.this.id
}

output "name" {
  description = "Instance ID (short name)."
  value       = google_redis_instance.this.name
}

output "host" {
  description = "Private IP of the primary endpoint."
  value       = google_redis_instance.this.host
}

output "port" {
  description = "Port the Redis endpoint listens on (default 6379)."
  value       = google_redis_instance.this.port
}

output "read_endpoint" {
  description = "Read-only endpoint IP (populated when read replicas are enabled)."
  value       = google_redis_instance.this.read_endpoint
}

output "read_endpoint_port" {
  description = "Port for the read-only endpoint."
  value       = google_redis_instance.this.read_endpoint_port
}

output "auth_string" {
  description = "Generated AUTH string (empty when auth_enabled is false)."
  value       = google_redis_instance.this.auth_string
  sensitive   = true
}

output "current_location_id" {
  description = "Zone currently hosting the primary node."
  value       = google_redis_instance.this.current_location_id
}

output "server_ca_certs" {
  description = "Server CA certificate(s) for verifying the TLS connection."
  value       = google_redis_instance.this.server_ca_certs
  sensitive   = true
}

How to use it

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

  project_id = "kv-prod-apps"
  name       = "sessions-cache-prod"
  region     = "asia-south1"
  network_id = "projects/kv-prod-network/global/networks/prod-vpc"

  # HA with a hot read replica for read-heavy session lookups.
  tier                    = "STANDARD_HA"
  memory_size_gb          = 10
  redis_version           = "REDIS_7_2"
  location_id             = "asia-south1-a"
  alternative_location_id = "asia-south1-c"
  read_replicas_mode      = "READ_REPLICAS_ENABLED"
  replica_count           = 2

  # Security defaults made explicit.
  auth_enabled            = true
  transit_encryption_mode = "SERVER_AUTHENTICATION"

  maxmemory_policy = "volatile-ttl"

  maintenance_window = {
    day     = "TUESDAY"
    hours   = 19 # 00:30 IST
    minutes = 0
  }

  labels = {
    team        = "platform"
    environment = "prod"
    cost-center = "cc-4471"
  }
}

# Downstream: hand the private host + AUTH string to a Cloud Run service.
resource "google_cloud_run_v2_service" "api" {
  name     = "checkout-api"
  location = "asia-south1"
  project  = "kv-prod-apps"

  template {
    containers {
      image = "asia-south1-docker.pkg.dev/kv-prod-apps/svc/checkout:latest"

      env {
        name  = "REDIS_HOST"
        value = module.memorystore_redis_sessions.host
      }
      env {
        name  = "REDIS_PORT"
        value = tostring(module.memorystore_redis_sessions.port)
      }
      # AUTH string is sensitive — store it in Secret Manager and reference it.
      env {
        name = "REDIS_AUTH"
        value_source {
          secret_key_ref {
            secret  = google_secret_manager_secret.redis_auth.secret_id
            version = "latest"
          }
        }
      }
    }
  }
}

resource "google_secret_manager_secret" "redis_auth" {
  secret_id = "sessions-cache-auth"
  project   = "kv-prod-apps"
  replication {
    auto {}
  }
}

resource "google_secret_manager_secret_version" "redis_auth" {
  secret      = google_secret_manager_secret.redis_auth.id
  secret_data = module.memorystore_redis_sessions.auth_string
}

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/memorystore/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders()
}

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

inputs = {
  project_id = "..."
  name = "..."
  region = "..."
  network_id = "..."
}

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

cd live/prod/memorystore && 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 Project that owns the instance.
name string yes Instance ID; 2-40 chars, starts with a letter.
region string yes Region, e.g. asia-south1.
display_name string null no Friendly name in the console.
network_id string yes Authorized VPC network self-link/ID.
tier string "STANDARD_HA" no BASIC or STANDARD_HA.
memory_size_gb number 5 no Capacity in GB (1-300).
redis_version string "REDIS_7_2" no Engine version, e.g. REDIS_7_2.
location_id string null no Primary zone.
alternative_location_id string null no Replica zone (STANDARD_HA).
read_replicas_mode string "READ_REPLICAS_DISABLED" no Enable/disable read replicas.
replica_count number 1 no Read replicas (1-5) when enabled.
auth_enabled bool true no Enable Redis AUTH.
transit_encryption_mode string "SERVER_AUTHENTICATION" no TLS in transit or DISABLED.
customer_managed_key string null no CMEK KMS key for encryption at rest.
maxmemory_policy string "allkeys-lru" no Eviction policy via redis_configs.
redis_configs map(string) {} no Extra redis.conf overrides.
persistence_mode string "DISABLED" no DISABLED or RDB.
rdb_snapshot_period string "TWENTY_FOUR_HOURS" no RDB snapshot cadence.
rdb_snapshot_start_time string null no RFC3339 first-snapshot time.
maintenance_window object {day=SUNDAY,hours=21,minutes=0} no Weekly maintenance window (UTC); null lets Google pick.
create_private_service_access bool true no Create the reserved range + peering.
reserved_ip_prefix_length number 29 no Prefix length for the PSA range.
reserved_ip_range string null no Existing range name when not creating PSA.
labels map(string) {} no Resource labels.

Outputs

Name Description
id Fully qualified instance ID.
name Short instance ID.
host Private IP of the primary endpoint.
port Redis port (default 6379).
read_endpoint Read-only endpoint IP (when replicas enabled).
read_endpoint_port Port for the read-only endpoint.
auth_string Generated AUTH string (sensitive).
current_location_id Zone currently hosting the primary.
server_ca_certs Server CA cert(s) for TLS verification (sensitive).

Enterprise scenario

A retail platform runs its checkout and catalog APIs on Cloud Run in asia-south1 and needs a shared session/rate-limit cache that survives a zone outage during festival-sale traffic. Using this module, the platform team stands up a STANDARD_HA instance with two read replicas in asia-south1-a/asia-south1-c, AUTH and TLS enforced, maxmemory-policy = volatile-ttl, and a Tuesday 00:30-IST maintenance window so Google reboots never collide with peak. The instance is reachable only over the peered private range from the production VPC, and the generated AUTH string flows straight into Secret Manager — so application teams get a hardened cache without ever touching VPC peering or key rotation themselves.

Best practices

TerraformGCPMemorystore (Redis)ModuleIaC
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