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
- You need a managed, low-latency cache or session store on GCP and you do not want to run Redis on GKE or GCE yourself.
- You want every Memorystore instance to be reachable only over a private IP via VPC peering, never a public endpoint.
- You want HA (automatic failover) and read replicas to be a one-line decision (
tier = "STANDARD_HA",read_replicas_mode = "READ_REPLICAS_ENABLED"). - You’re standardising caches across many services and want consistent AUTH, TLS, maintenance windows, and labels.
- Skip it if you specifically need Memorystore for Redis Cluster (the sharded
google_redis_clusterresource) or Memorystore for Valkey — those are different resources with a different topology and deserve their own module.
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 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/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
- Default to STANDARD_HA in production.
BASICis a single node with no failover and no SLA; the cost delta toSTANDARD_HAis small relative to a cache outage taking down checkout. ReserveBASICfor ephemeral/dev caches. - Always enable AUTH and
SERVER_AUTHENTICATION. A private IP is not authentication. Treatauth_stringas a secret, never echo it in CI logs, and push it into Secret Manager rather than env vars baked into images. - Right-size memory and pick the eviction policy deliberately.
memory_size_gbalso caps network throughput, so scale it for bandwidth, not just keys. Useallkeys-lrufor a pure cache andnoevictiononly when losing data is unacceptable and you’ve sized headroom. - Pin the Redis version and schedule maintenance. Set
redis_versionexplicitly (don’t drift onto a new major silently) and always set amaintenance_windowin a low-traffic hour so Google-initiated failovers are predictable. - Reserve a dedicated /29 PSA range and reuse it in shared VPC. Let the module create the peering in standalone projects, but set
create_private_service_access = falseand passreserved_ip_rangewhen the range is managed centrally, to avoid duplicate/overlapping peerings. - Label everything for cost allocation. Memorystore is billed per GB-hour per node (primary + replicas), so consistent
team/environment/cost-centerlabels make it easy to attribute spend and catch oversized idle caches.