IaC GCP

Terraform Module: GCP Memorystore for Memcached — a private, multi-node cache in one call

Quick take — Provision Google Cloud Memorystore for Memcached with Terraform: a reusable google_memcache_instance module covering node count, per-node CPU/RAM, maintenance windows, and private VPC peering. 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 "memcached" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-memcached?ref=v1.0.0"

  project_id         = "..."  # GCP project ID that owns the instance.
  name               = "..."  # Instance ID (lowercase, starts with a letter, ≤40 chars…
  region             = "..."  # Region, e.g. `asia-south1`.
  authorized_network = "..."  # VPC to peer into (short name, path, or self_link).
}

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

What this module is

Memorystore for Memcached is Google Cloud’s fully managed, Memcached-compatible in-memory cache. Unlike Memorystore for Redis, it is a horizontally-scaled, multi-node cache: you choose how many nodes you want (1–20) and the vCPU/RAM per node, and Google fans your keyspace out across them and exposes a single discovery endpoint that an auto-discovery-capable client (the gcloud Memcached client, spymemcached, dalli, or pymemcache with the GCP discovery patch) uses to learn the individual node addresses. It speaks the classic Memcached text/binary protocol on port 11211, runs inside your VPC with a private RFC 1918 IP, and has no public endpoint.

Because it is multi-node, a Memcached instance has more moving parts than a typical cache: node_count, the node_config block (vCPU + memory per node), memcache_version, an optional memcache_parameters map (Memcached flags like max-item-size), the authorized_network to peer into, and a maintenance_policy window. Hand-writing the google_memcache_instance resource and getting the node geometry, the parameter map, and the network reference right for every environment is repetitive and error-prone. Wrapping it in a module fixes the safe defaults (private network, sensible node config, a defined maintenance window), validates the inputs that Google will otherwise reject at apply time (node count range, region-versus-zone placement), and hands back the discovery endpoint and node list so application stacks can consume the cache without copy-pasting the resource.

When to use it

Module structure

terraform-module-gcp-memcached/
├── versions.tf      # provider + Terraform version pins
├── main.tf          # google_memcache_instance + locals
├── variables.tf     # var-driven inputs with validation
└── outputs.tf       # id, discovery endpoint, node list

versions.tf

terraform {
  required_version = ">= 1.5.0"

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

main.tf

locals {
  # Memcached defaults to port 11211; surface it for downstream connection strings.
  memcache_port = 11211

  # Build the network self_link unless the caller passed a full self_link/ID already.
  authorized_network = (
    can(regex("^projects/", var.authorized_network)) || can(regex("^https://", var.authorized_network))
    ? var.authorized_network
    : "projects/${var.project_id}/global/networks/${var.authorized_network}"
  )

  default_labels = {
    managed-by = "terraform"
    module     = "gcp-memcached"
  }
}

resource "google_memcache_instance" "this" {
  provider = google

  project        = var.project_id
  name           = var.name
  region         = var.region
  display_name   = var.display_name != null ? var.display_name : var.name
  zones          = var.zones
  node_count     = var.node_count
  memcache_version = var.memcache_version

  authorized_network = local.authorized_network

  labels = merge(local.default_labels, var.labels)

  node_config {
    cpu_count      = var.node_cpu_count
    memory_size_mb = var.node_memory_size_mb
  }

  # Memcached runtime flags (e.g. max-item-size, track-sizes). Omitted block when empty.
  dynamic "memcache_parameters" {
    for_each = length(var.memcache_parameters) > 0 ? [1] : []
    content {
      params = var.memcache_parameters
    }
  }

  # Weekly maintenance window so Google applies node updates predictably.
  dynamic "maintenance_policy" {
    for_each = var.maintenance_window != null ? [var.maintenance_window] : []
    content {
      description = "Managed by Terraform"
      weekly_maintenance_window {
        day = maintenance_policy.value.day
        duration = maintenance_policy.value.duration
        start_time {
          hours   = maintenance_policy.value.start_hours
          minutes = maintenance_policy.value.start_minutes
          seconds = 0
          nanos   = 0
        }
      }
    }
  }

  timeouts {
    create = "30m"
    update = "30m"
    delete = "30m"
  }
}

variables.tf

variable "project_id" {
  description = "GCP project ID that will own the Memcached instance."
  type        = string
}

variable "name" {
  description = "Instance ID. Lowercase letters, digits and hyphens; must start with a letter and be 1-40 chars."
  type        = string

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{0,39}$", var.name))
    error_message = "name must start with a lowercase letter and contain only lowercase letters, digits and hyphens (max 40 chars)."
  }
}

variable "display_name" {
  description = "Human-readable name shown in the console. Defaults to var.name when null."
  type        = string
  default     = null
}

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

variable "zones" {
  description = "Zones within the region across which nodes are distributed. Leave empty to let GCP spread them automatically."
  type        = list(string)
  default     = []
}

variable "node_count" {
  description = "Number of Memcached nodes (1-20). Keyspace is sharded across them."
  type        = number
  default     = 1

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

variable "node_cpu_count" {
  description = "vCPUs per node (1-32). Combined with node_memory_size_mb this defines per-node capacity."
  type        = number
  default     = 1

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

variable "node_memory_size_mb" {
  description = "Memory per node in MB. Must be a multiple of 1024 and between 1024 and 262144 (1 GiB - 256 GiB)."
  type        = number
  default     = 1024

  validation {
    condition = (
      var.node_memory_size_mb >= 1024 &&
      var.node_memory_size_mb <= 262144 &&
      var.node_memory_size_mb % 1024 == 0
    )
    error_message = "node_memory_size_mb must be a multiple of 1024 between 1024 and 262144."
  }
}

variable "memcache_version" {
  description = "Memcached version: MEMCACHE_1_5 or MEMCACHE_1_6_15."
  type        = string
  default     = "MEMCACHE_1_5"

  validation {
    condition     = contains(["MEMCACHE_1_5", "MEMCACHE_1_6_15"], var.memcache_version)
    error_message = "memcache_version must be MEMCACHE_1_5 or MEMCACHE_1_6_15."
  }
}

variable "authorized_network" {
  description = "VPC the instance peers into. Pass a short network name, a projects/<p>/global/networks/<n> path, or a full self_link."
  type        = string
}

variable "memcache_parameters" {
  description = "Map of Memcached runtime flags, e.g. { max-item-size = \"4m\", track-sizes = \"true\" }."
  type        = map(string)
  default     = {}
}

variable "maintenance_window" {
  description = "Weekly maintenance window. Set to null to omit a policy."
  type = object({
    day            = string # MONDAY..SUNDAY
    start_hours    = number # 0-23 (window start hour, UTC)
    start_minutes  = number # 0-59
    duration       = string # e.g. \"3600s\"
  })
  default = {
    day           = "SUNDAY"
    start_hours   = 18
    start_minutes = 0
    duration      = "3600s"
  }

  validation {
    condition = var.maintenance_window == null ? true : (
      contains(
        ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"],
        var.maintenance_window.day
      ) &&
      var.maintenance_window.start_hours >= 0 && var.maintenance_window.start_hours <= 23 &&
      var.maintenance_window.start_minutes >= 0 && var.maintenance_window.start_minutes <= 59
    )
    error_message = "maintenance_window.day must be a valid weekday and start_hours 0-23, start_minutes 0-59."
  }
}

variable "labels" {
  description = "Additional labels merged onto the instance."
  type        = map(string)
  default     = {}
}

outputs.tf

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

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

output "discovery_endpoint" {
  description = "Auto-discovery endpoint host:port. Point a discovery-capable Memcached client at this."
  value       = google_memcache_instance.this.discovery_endpoint
}

output "memcache_full_version" {
  description = "Full Memcached version string actually running, e.g. memcached-1.5.16."
  value       = google_memcache_instance.this.memcache_full_version
}

output "memcache_nodes" {
  description = "List of individual nodes with node_id, zone, host and port."
  value       = google_memcache_instance.this.memcache_nodes
}

output "port" {
  description = "Memcached port (11211)."
  value       = local.memcache_port
}

How to use it

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

  project_id = "kv-prod-shared"
  name       = "kv-prod-session-cache"
  region     = "asia-south1"
  zones      = ["asia-south1-a", "asia-south1-b"]

  # 4 nodes x 2 vCPU x 8 GiB = 32 GiB of usable cache, sharded.
  node_count          = 4
  node_cpu_count      = 2
  node_memory_size_mb = 8192

  memcache_version   = "MEMCACHE_1_6_15"
  authorized_network = "kv-prod-vpc"

  memcache_parameters = {
    "max-item-size" = "4m"
    "track-sizes"   = "true"
  }

  maintenance_window = {
    day           = "SUNDAY"
    start_hours   = 19   # 19:00 UTC ~ 00:30 IST Monday
    start_minutes = 30
    duration      = "3600s"
  }

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

# Downstream: hand the discovery endpoint to a GKE workload as an env var
# so the app's auto-discovery Memcached client can enumerate the nodes.
resource "kubernetes_config_map" "cache_config" {
  metadata {
    name      = "memcached-discovery"
    namespace = "web"
  }

  data = {
    MEMCACHED_DISCOVERY_ENDPOINT = module.memorystore_for_memcached.discovery_endpoint
    MEMCACHED_PORT               = tostring(module.memorystore_for_memcached.port)
  }
}

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

include "root" {
  path = find_in_parent_folders()
}

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

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

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

cd live/prod/memcached && 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 that owns the instance.
name string Yes Instance ID (lowercase, starts with a letter, ≤40 chars).
display_name string null No Console display name; defaults to name.
region string Yes Region, e.g. asia-south1.
zones list(string) [] No Zones to spread nodes across; empty lets GCP choose.
node_count number 1 No Number of nodes (1–20); keyspace is sharded across them.
node_cpu_count number 1 No vCPUs per node (1–32).
node_memory_size_mb number 1024 No Memory per node in MB; multiple of 1024, 1024–262144.
memcache_version string "MEMCACHE_1_5" No MEMCACHE_1_5 or MEMCACHE_1_6_15.
authorized_network string Yes VPC to peer into (short name, path, or self_link).
memcache_parameters map(string) {} No Memcached runtime flags (e.g. max-item-size).
maintenance_window object(...) Sunday 18:00 UTC, 1h No Weekly maintenance window; null to omit.
labels map(string) {} No Extra labels merged onto the instance.

Outputs

Name Description
id Fully-qualified instance ID (projects/<p>/locations/<region>/instances/<name>).
name Short instance ID.
discovery_endpoint Auto-discovery host:port for discovery-capable clients.
memcache_full_version Actual Memcached version running, e.g. memcached-1.5.16.
memcache_nodes List of nodes with node_id, zone, host, port.
port Memcached port (11211).

Enterprise scenario

A retail platform runs its product catalogue and rendered fragment cache on a fleet of GKE services in asia-south1. During festive sales the catalogue working set grows to roughly 30 GiB — too large for a single Redis node to hold cheaply — so the platform team deploys this module with node_count = 4, node_cpu_count = 2, node_memory_size_mb = 8192, peered into the production VPC. The discovery endpoint is published into a Kubernetes ConfigMap by the same Terraform stack, the dalli client in each Rails pod auto-discovers all four nodes, and the Sunday-night maintenance window keeps Google’s node updates well clear of weekday traffic.

Best practices

TerraformGCPMemorystore for MemcachedModuleIaC
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