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
- You need a shared, read-through/look-aside cache sized in the tens-to-hundreds of GB that a single Redis node cannot economically hold, and you want it sharded across nodes.
- Your workload already speaks Memcached (session stores, fragment caches, Hibernate/Rails second-level caches, ML feature caches) and you do not need Redis data structures or persistence.
- You want the cache private to a VPC with no public exposure, reachable from GKE, Compute Engine, or Cloud Run (via Serverless VPC Access).
- You are standing up the same cache shape across dev/staging/prod and want node geometry, maintenance windows, and Memcached parameters expressed once as code.
- You do not need durability or replication — Memcached is volatile by design; if a node restarts its slice of the keyspace is lost. If you need persistence or HA replicas, use Memorystore for Redis instead.
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 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/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
- Keep it private and reachable. Memcached has no AUTH or in-transit encryption, so the only real boundary is the network: peer it into a dedicated VPC, restrict reachability with firewall rules to just the app subnets/service accounts, and never expose it via a bastion or public route. From Cloud Run, front it with Serverless VPC Access.
- Size by working set, not by node count alone. Usable capacity is
node_count × node_memory_size_mb, but a single hot key still lands on one node. Use more, smaller nodes to spread load and shorten the blast radius of a node restart; reserve large single-node configs for low-shard-pressure caches. - Treat the cache as disposable. There is no persistence or replication — a node restart drops its shard. Design every read path as a look-aside that can repopulate from the source of truth, set sane TTLs, and never make Memcached the only copy of any datum.
- Pin and stage the Memcached version. Set
memcache_versionexplicitly (e.g.MEMCACHE_1_6_15) rather than drifting on defaults, and roll version bumps through dev/staging first since a version change recreates nodes. - Control updates with the maintenance window. Always set
maintenance_windowto an off-peak slot in UTC (mind the IST offset) so Google’s node maintenance — which can briefly evict a node’s keyspace — happens when traffic and cache-miss cost are lowest. - Name and label for accountability. Use a predictable
<org>-<env>-<purpose>-cacheinstance ID and carryenvironment,team, andcost-centerlabels so the multi-node spend (billed per vCPU-hour and GB-hour) is attributable in billing exports.