Terraform for a Standard Tier Memorystore for Redis instance on private services access, with AUTH, in-transit encryption, read replicas and persistence, the AUTH string in Secret Manager, and a Cloud Run service that reaches it through Direct VPC egress, plus how Encore provisions Memorystore from code.

A Memorystore for Redis instance takes five lines of Terraform with the defaults, which give you a Basic Tier instance with no replica, no AUTH, no TLS and a direct peering connection. A setup you would put sessions or login codes in needs a replicated tier, private services access, AUTH and in-transit encryption, a home for the AUTH string, and a Cloud Run service that can reach a private IP and trust the instance's certificate.
This guide covers each of those with the Google provider 7.x, the settings worth deciding before the first apply, and how Encore provisions Memorystore from a cache declared in Go.
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 7.0"
}
}
}
variable "project" { type = string }
variable "region" {
type = string
default = "europe-west1"
}
variable "image" {
type = string
description = "Container image for the API service"
}
provider "google" {
project = var.project
region = var.region
}
resource "google_project_service" "apis" {
for_each = toset([
"redis.googleapis.com",
"servicenetworking.googleapis.com",
"run.googleapis.com",
"secretmanager.googleapis.com",
])
service = each.key
disable_on_destroy = false
}
resource "google_compute_network" "main" {
name = "main"
auto_create_subnetworks = false
}
# Cloud Run sends traffic for private ranges into the VPC from this subnet.
resource "google_compute_subnetwork" "run" {
name = "run-egress"
network = google_compute_network.main.id
region = var.region
ip_cidr_range = "10.20.0.0/24"
}
# Private services access: Memorystore takes its IPs from this range.
resource "google_compute_global_address" "psa" {
name = "google-managed-services"
purpose = "VPC_PEERING"
address_type = "INTERNAL"
prefix_length = 20
network = google_compute_network.main.id
}
resource "google_service_networking_connection" "psa" {
network = google_compute_network.main.id
service = "servicenetworking.googleapis.com"
reserved_peering_ranges = [google_compute_global_address.psa.name]
depends_on = [google_project_service.apis]
}
Memorystore for Redis defaults to DIRECT_PEERING, where Memorystore creates a VPC peering between your network and a Google-managed one. With PRIVATE_SERVICE_ACCESS, the instance draws its IP from the reserved range above, the same connection that Cloud SQL private IP uses, and Google's Memorystore networking docs list it as a prerequisite for instances on a Shared VPC. A /20 leaves room for more instances and other Google-managed services in the same network.
resource "google_redis_instance" "sessions" {
name = "sessions"
tier = "STANDARD_HA" # replica in another zone, automatic failover
memory_size_gb = 5
region = var.region
redis_version = "REDIS_7_2"
authorized_network = google_compute_network.main.id
connect_mode = "PRIVATE_SERVICE_ACCESS"
auth_enabled = true
transit_encryption_mode = "SERVER_AUTHENTICATION" # TLS, clients need the server CA
replica_count = 1
read_replicas_mode = "READ_REPLICAS_ENABLED"
redis_configs = {
"maxmemory-policy" = "volatile-lru"
}
persistence_config {
persistence_mode = "RDB"
rdb_snapshot_period = "TWELVE_HOURS"
}
maintenance_policy {
weekly_maintenance_window {
day = "SUNDAY"
start_time {
hours = 3
minutes = 0
}
}
}
depends_on = [google_service_networking_connection.psa]
}
# The AUTH string is generated by Memorystore and is also stored in Terraform
# state, so the state bucket needs the same protection as the secret.
resource "google_secret_manager_secret" "redis_auth" {
secret_id = "redis-auth"
replication {
auto {}
}
depends_on = [google_project_service.apis]
}
resource "google_secret_manager_secret_version" "redis_auth" {
secret = google_secret_manager_secret.redis_auth.id
secret_data = google_redis_instance.sessions.auth_string
}
The settings that change behavior later:
tier = "STANDARD_HA" replicates across zones with automatic failover. Basic Tier has no replica and no automatic failover, according to the Memorystore for Redis overview, which also lists the 300 GB limit and up to five read replicas.read_replicas_mode exposes read_endpoint, which spreads reads across replicas with replication lag, so writes and reads that must see the latest value still go to host.auth_enabled makes Memorystore generate a UUID as the AUTH string, and without in-transit encryption that string is not guaranteed to be encrypted on the wire, as the AUTH docs point out.transit_encryption_mode = "SERVER_AUTHENTICATION" turns on TLS, and clients have to trust the instance's certificate authority, exported as server_ca_certs, per the in-transit encryption docs.maxmemory-policy = "volatile-lru" evicts only keys that have a TTL, which protects keys written without one at the cost of write errors if those fill memory.auth_string is a sensitive attribute of the instance resource, so it lives in Terraform state whatever you do with it afterwards. Copying it into Secret Manager keeps the value out of the Cloud Run service definition and lets access be granted per service account, and the state bucket needs the same access controls as the secret.
resource "google_service_account" "api" {
account_id = "api-service"
display_name = "API service on Cloud Run"
}
resource "google_secret_manager_secret_iam_member" "api_redis_auth" {
secret_id = google_secret_manager_secret.redis_auth.id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.api.email}"
}
resource "google_cloud_run_v2_service" "api" {
name = "api"
location = var.region
template {
service_account = google_service_account.api.email
# Direct VPC egress: only private ranges go through the VPC.
vpc_access {
network_interfaces {
network = google_compute_network.main.id
subnetwork = google_compute_subnetwork.run.id
}
egress = "PRIVATE_RANGES_ONLY"
}
containers {
image = var.image
env {
name = "REDIS_ADDR"
value = "${google_redis_instance.sessions.host}:${google_redis_instance.sessions.port}"
}
env {
name = "REDIS_READ_ADDR"
value = "${google_redis_instance.sessions.read_endpoint}:${google_redis_instance.sessions.read_endpoint_port}"
}
env {
name = "REDIS_CA_PEM"
value = google_redis_instance.sessions.server_ca_certs[0].cert
}
env {
name = "REDIS_AUTH"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.redis_auth.secret_id
version = "latest"
}
}
}
}
}
depends_on = [google_secret_manager_secret_iam_member.api_redis_auth]
}
Memorystore has no public IP, and Google recommends Direct VPC egress over a Serverless VPC Access connector for reaching it from Cloud Run, in its Cloud Run connection guide. PRIVATE_RANGES_ONLY sends only private destinations through the VPC, so calls to public APIs keep using Cloud Run's default egress. The application still has to build a TLS config from REDIS_CA_PEM, send AUTH with the secret, and route reads to REDIS_READ_ADDR. The configuration passes terraform validate with Terraform 1.13 and the Google provider 7.x.
server_ca_certs before the old authority is retired, so the CA should not be baked into an image.Encore is an infrastructure-from-code platform for TypeScript and Go: a cache cluster is declared in the service that uses it and the Encore platform provisions it as its own Redis instance in your GCP project when you deploy. The GCP infrastructure docs describe Memorystore for Redis in a high-availability setup, starting from the smallest size and able to scale up, with Redis AUTH credentials managed and rotated by the platform, and services running on Cloud Run in a private network.

Run in your terminal to get started locally.
Email login codes are a good fit for a cache with expiry: each code lives for ten minutes, is used once, and a counter limits guesses. The cluster and two typed keyspaces replace the instance, secret, CA and IAM resources above:
// auth/codes.go
package auth
import (
"context"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"math/big"
"strings"
"time"
"encore.dev/beta/errs"
"encore.dev/storage/cache"
)
// Memorystore for Redis on GCP, ElastiCache on AWS, in-memory Redis locally.
var cluster = cache.NewCluster("auth-cache", cache.ClusterConfig{
EvictionPolicy: cache.NoEviction, // login codes must not be evicted early
})
// Hashed login codes, keyed by email, gone after 10 minutes.
var codes = cache.NewStringKeyspace[string](cluster, cache.KeyspaceConfig{
KeyPattern: "login-code/:key",
DefaultExpiry: cache.ExpireIn(10 * time.Minute),
})
// Failed verification attempts per email in a fixed 10-minute window.
// The window is part of the key, so later requests cannot extend the lockout.
var attempts = cache.NewIntKeyspace[string](cluster, cache.KeyspaceConfig{
KeyPattern: "login-attempts/:key",
DefaultExpiry: cache.ExpireIn(10 * time.Minute),
})
// Code sends per email in the same fixed window, independent of verifies.
var sends = cache.NewIntKeyspace[string](cluster, cache.KeyspaceConfig{
KeyPattern: "login-sends/:key",
DefaultExpiry: cache.ExpireIn(10 * time.Minute),
})
func windowKey(email string) string {
return email + "/" + time.Now().UTC().Truncate(10*time.Minute).Format("2006-01-02T15:04")
}
type RequestParams struct {
Email string `json:"email"`
}
//encore:api public method=POST path=/auth/code
func RequestCode(ctx context.Context, p *RequestParams) error {
email := strings.ToLower(strings.TrimSpace(p.Email))
key := windowKey(email)
sent, err := sends.Increment(ctx, key, 1)
if err != nil {
return err
}
if sent > 3 {
return &errs.Error{Code: errs.ResourceExhausted, Message: "try again later"}
}
n, err := rand.Int(rand.Reader, big.NewInt(1_000_000))
if err != nil {
return err
}
code := fmt.Sprintf("%06d", n.Int64())
// Replace the previous unused code, but leave the attempt counter in place.
if err := codes.Set(ctx, email, hash(code)); err != nil {
return err
}
return sendCode(ctx, email, code)
}
type VerifyParams struct {
Email string `json:"email"`
Code string `json:"code"`
}
//encore:api public method=POST path=/auth/verify
func VerifyCode(ctx context.Context, p *VerifyParams) error {
email := strings.ToLower(strings.TrimSpace(p.Email))
key := windowKey(email)
n, err := attempts.Increment(ctx, key, 1)
if err != nil {
return err
}
if n > 5 {
return &errs.Error{Code: errs.ResourceExhausted, Message: "too many attempts, try again later"}
}
want, err := codes.Get(ctx, email)
if errors.Is(err, cache.Miss) {
return &errs.Error{Code: errs.Unauthenticated, Message: "code expired or invalid"}
} else if err != nil {
return err
}
if subtle.ConstantTimeCompare([]byte(want), []byte(hash(p.Code))) != 1 {
return &errs.Error{Code: errs.Unauthenticated, Message: "code expired or invalid"}
}
// Single use: remove the code once it has been accepted.
_, err = codes.Delete(ctx, email)
return err
}
func hash(code string) string {
sum := sha256.Sum256([]byte(code))
return hex.EncodeToString(sum[:])
}
// sendCode hands the code to your email provider.
func sendCode(ctx context.Context, email, code string) error { return nil }
NoEviction makes Redis return errors when memory is full instead of dropping a code before it expires, and each keyspace's DefaultExpiry sets the TTL on every write. Get returns an error matching cache.Miss once a code has expired, and sendCode is where your email provider's client goes. The caching docs cover the other keyspace types and eviction policies.
encore run runs the cache in memory on your machine, so the request, verify and lockout paths can be tried locally with the same code that runs against Memorystore, with no TLS config or AUTH string in the application. When a coding agent adds a keyspace, for example to remember trusted devices, the change is a few lines in this file and the next deploy needs nothing new in the cloud. The instance size and tier can be adjusted per environment afterwards, as described in the infrastructure configuration docs, and the same code becomes ElastiCache if you connect an AWS account, per the AWS infrastructure docs.
Best fit: Go and TypeScript teams on GCP who want a replicated Memorystore instance next to their Cloud Run services, created and connected from the code that reads and writes it.
google_cloud_run_v2_service defaults to deletion_protection = true in the Google provider, so a terraform destroy fails until you set it to false and apply.memory_size_gb on a Standard Tier instance causes a short interruption, under a minute according to Google's overview, so schedule it like maintenance.persistence_config to avoid snapshot overhead.Use google_redis_instance with a name, tier, memory_size_gb, region and authorized_network. For production, set tier = "STANDARD_HA" for a cross-zone replica with automatic failover, auth_enabled = true, transit_encryption_mode = "SERVER_AUTHENTICATION", and connect_mode = "PRIVATE_SERVICE_ACCESS" with a google_service_networking_connection created first.
DIRECT_PEERING, the default, has Memorystore create a VPC peering between your network and a Google-managed network. PRIVATE_SERVICE_ACCESS takes the instance IP from a range you reserve for Google services and reuses the same service networking connection as Cloud SQL, and Google requires it for instances on a Shared VPC network.
Memorystore has only a private IP, so the Cloud Run service needs Direct VPC egress or a Serverless VPC Access connector on the instance's network. Google recommends Direct VPC egress. The service then connects to the instance host and port with the AUTH string and, when in-transit encryption is on, the server CA certificate.
With Encore, declare cache.NewCluster("auth-cache", cache.ClusterConfig{EvictionPolicy: cache.NoEviction}) and typed keyspaces such as cache.NewStringKeyspace and cache.NewIntKeyspace. On deploy to GCP, Encore provisions Memorystore for Redis in a high-availability setup with AUTH credentials it manages and rotates, and under encore run the cache runs in memory.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.