← Back to Blog RSS

How to Deploy a Highly Available Redis Cluster with Terraform on AWS

Terraform AWS Infrastructure State Management

A single-node Redis cluster has no automatic failover, which means losing one availability zone can take the cache down with it. High availability is the ability to survive that failure without an outage, not just a label you add to the configuration.

TL;DR
$ cat terraform-redis-cluster.tldr
• Why `aws_elasticache_cluster` and `aws_elasticache_replication_group` produce two different availability outcomes, and which one to use for this build.
• The full replication group configuration for Multi-AZ Redis with automatic failover, correctly sized node groups, and cluster mode where the workload needs it.
• Where resizing a cluster-mode-enabled deployment stops being a normal Terraform update, and what AWS actually does underneath your `plan`.
• The snapshot and teardown arguments that decide whether a `terraform destroy` is recoverable or permanent.

A Terraform Redis cluster usually refers to a single cache cluster, a security group, and an apply that finishes in under three minutes. It runs, but it has no replica to fail over to and no way to survive AWS losing an availability zone underneath it (even though it needs to be highly available).

This article builds the version that is highly available: the right resource, sized correctly and backed up in a way that survives a destroy.

We also cover where a managed cache service drifts from your declared config even if nobody touched the HCL.

A Redis cluster is not one AWS resource

The AWS provider offers two ways to provision Redis on ElastiCache, each with different levels of availability.

For a genuinely highly available Terraform-Redis cluster, aws_elasticache_replication_group is the one you want.

Creating an ElastiCache Redis cluster with real availability

The Terraform state for this resource holds more than a node ID: node group topology, endpoint addresses, and every failover argument live in the same state file as the rest of your infrastructure, worth remembering once you reach the drift section below.

Here's a baseline replication group with genuine HA from the first apply, not bolted on afterward:

resource "aws_elasticache_replication_group" "cache" {
replication_group_id = "app-cache"
description = "Primary Redis cache for the app tier"
engine = "redis"
engine_version = "7.1"
node_type = "cache.r7g.large"
port = 6379
num_cache_clusters = 2
automatic_failover_enabled = true
multi_az_enabled = true
subnet_group_name = aws_elasticache_subnet_group.cache.name
security_group_ids = [aws_security_group.cache.id]
parameter_group_name = "default.redis7"
maintenance_window = "sun:05:00-sun:06:00"
}
resource "aws_elasticache_subnet_group" "cache" {
name = "app-cache-subnets"
subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id, aws_subnet.private_c.id]
}
resource "aws_security_group" "cache" {
name = "app-cache-sg"
description = "Allow Redis traffic from the app tier only"
vpc_id = var.vpc_id
}
resource "aws_vpc_security_group_ingress_rule" "cache_from_app" {
security_group_id = aws_security_group.cache.id
from_port = 6379
to_port = 6379
ip_protocol = "tcp"
referenced_security_group_id = var.app_security_group_id
}
resource "aws_vpc_security_group_egress_rule" "cache_all" {
security_group_id = aws_security_group.cache.id
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}

num_cache_clusters = 2 gives the replication group a primary, and one replica spread across the availability zones your subnet group covers; automatic_failover_enabled is what actually promotes that replica if the primary goes down, and multi_az_enabled builds on it by placing the standby so AWS can fail over without waiting on a replica sync from scratch.

Both need to be true for the guarantee this article opened with, and the subnet group needs at least two subnets in different zones; otherwise, multi_az_enabled has nothing to spread across.

The parameter_group_name above points at AWS's own default parameter group for the engine family; a custom one only earns its keep once a workload needs specific Redis config overrides.

node_type is also where running two or three nodes instead of one actually shows up on the bill, a straightforward case for AWS cost optimization in Terraform: size it deliberately rather than defaulting to whatever instance class a tutorial happened to use.

Sizing cluster mode for real scale

With cluster mode enabled, pure replication is traded for sharding: data splits across multiple node groups instead of every node holding the full dataset, which is what lets a terraform redis cluster scale past the memory ceiling of a single instance class.

resource "aws_elasticache_replication_group" "sharded_cache" {
replication_group_id = "app-cache-sharded"
description = "Sharded Redis cache, cluster mode enabled"
engine = "redis"
engine_version = "7.1"
node_type = "cache.r7g.large"
port = 6379
num_node_groups = 3
replicas_per_node_group = 1
automatic_failover_enabled = true
multi_az_enabled = true
subnet_group_name = aws_elasticache_subnet_group.cache.name
security_group_ids = [aws_security_group.cache.id]
parameter_group_name = "default.redis7.cluster.on"
}

Three node groups with one replica each puts six nodes on the ground and exposes a configuration_endpoint_address instead of the primary and reader endpoints a non-cluster-mode replication group gives you; client libraries need to speak the Redis cluster protocol to route requests to the right shard from that single endpoint.

Resizing this later isn't like bumping a desired_count on an autoscaling group.

Changing num_node_groups triggers AWS's own resharding underneath your apply, either online (the cluster keeps serving traffic while data migrates between shards) or offline.

Terraform requests the resize and waits; it doesn't control which path AWS takes or how long the migration runs, so that diff is a different category of change than most Terraform updates. It's worth reading AWS's resharding guidance before running against production.

Going the other direction, cluster mode disabled to enabled on an existing group, is more restrictive still: AWS requires an intermediate compatibility step, not a single attribute flip, so plan for a migration window rather than a routine apply.

Automatic cache cluster snapshots and a destroy you can recover from

Three arguments determine whether a terraform destroy, accidental or intended, is recoverable. Add them to either replication group above:

snapshot_retention_limit = 7
snapshot_window = "03:00-04:00"
final_snapshot_identifier = "app-cache-final"

Restoring into a new replication group from an ElastiCache snapshot, including the final one a destroy leaves behind, uses snapshot_name. The separate snapshot_arns argument covers a different path, seeding a new cluster from an RDB file you have exported to your own S3 bucket.

Where the declared config and a live cluster quietly diverge

A Terraform Redis cluster drifts in ways most Terraform-managed resources don't, because ElastiCache keeps making decisions on your infrastructure's behalf between applies.

auto_minor_version_upgrade (true by default) lets AWS apply minor engine upgrades inside the maintenance window automatically, making it useful for security patches, but the running engine version can move without a matching change to your HCL, and the next plan shows a diff Terraform didn't cause.

The same applies to automatic failover: if AWS promotes a replica to primary after a health check failure, nothing about your declared config changed, but which physical node serves as primary did.

A third of the practitioners surveyed for Firefly's State of IaC 2026 report tied drift directly to a costly production incident, and 8% said it caused significant downtime.

None of this is due to a flaw in the resource; it's what happens when a config only gets evaluated at apply time against a service that keeps deciding things in between.

Configuration drift of this kind stays invisible until the next plan happens to run, while the same is true of the blast radius of a change: resizing the subnet group or tightening the security group ripples into anything else referencing them, and a plan on the cache resource alone won't show it.

Alternatives to consider

Google Cloud Memorystore and Azure Managed Redis solve the same problem on other clouds, with the same Multi-AZ and sharding concepts under provider-specific resource names, worth knowing if a multi-cloud footprint is already a given.

Memcached is an option when a workload genuinely doesn't need Redis's data structures or persistence: pure key-value caching, none of the replication group's failover complexity or built-in replication.

KeyDB, a multi-threaded Redis fork, targets higher throughput per node on the same protocol, making it a real option for CPU-bound workloads – though ElastiCache doesn't run it, so it means managing the instances yourself.

Where graph-aware state management helps once this is running

Stategraph doesn't do anything specific to ElastiCache, and that's the point: the drift and blast radius gaps above happen to any resource in a state file a cloud provider can change without a corresponding apply.

Stategraph Velocity stores the Terraform dependency graph in a database instead of a flat state file, making resource-level locking possible: an engineer resizing the cache's security group and a pipeline touching an unrelated node group stop queuing behind the same whole-file lock.

Blast radius analysis can run directly against that graph, so the security-group ripple above shows up before an apply rather than after an incident, and scheduled refreshes catch the auto-minor-version-upgrade case on a fixed cadence rather than whenever someone happens to run plan again.

This setup is for teams already running enough ElastiCache and everything around it that drift and blast radius are recurring costs, not a requirement for one cluster backing one app.

Conclusion

aws_elasticache_replication_group, not aws_elasticache_cluster, is the resource that most people searching for it actually want: automatic failover, Multi-AZ placement, and cluster mode sized correctly if the workload needs to shard.

Get the snapshot arguments right and a terraform destroy stops being a one-way door. However, AWS makes its own changes to a live cluster between applies, which you should plan for rather than discovering it during an incident.

If drift and blast radius across ElastiCache and everything else in your state have already become a recurring cost rather than a hypothetical, try Stategraph free and see the same replication group running against a graph instead of a flat file.

Terraform Redis cluster FAQs

Does cluster mode being enabled require more than one node group?

Not strictly, but there's little reason to enable it for a single node group.

Cluster mode's value is sharding data across multiple node groups; with num_node_groups = 1 you get the client-side complexity of the cluster protocol without the sharding benefit, so a non-cluster-mode group with num_cache_clusters set for replicas is simpler until there's a real reason to shard.

What port does Redis use by default on ElastiCache?

6379, for both a non-cluster-mode replication group's endpoints and the configuration endpoint a cluster-mode-enabled deployment exposes.

The port argument can override it, but changing it from the default has no benefit inside a VPC where a security group already restricts access, and it means updating every client's configuration to match.

Can an existing single-node cache cluster be migrated to a Multi-AZ replication group?

No, as the two are different resource types in the AWS provider with no Terraform-native migration path between them.

In practice, this means standing up the replication group as a new resource, pointing traffic at it, and decommissioning the old cache cluster once the cutover is verified – not modifying the existing resource's arguments.

Is an auth token required for encryption in transit?

No, but the dependency runs the other way: an auth_token can only be set when transit_encryption_enabled = true. Transit encryption on its own encrypts the connection without authenticating clients, so pair it with either an auth_token or an RBAC user group.

Treat it as sensitive data, sourced from a Secrets Manager rather than a literal string in version-controlled HCL, and rotate it via a two-step apply so no client gets locked out mid-rotation.