Elasticache Redis with Terraform: lessons from production
Redis has become the default choice for caching layers across modern applications, but deploying it on AWS requires careful consideration of networking, replication, encryption, and configuration. We've learned hard lessons managing Elasticache Redis at scale—from production incidents caused by misconfigured failover to compliance violations from unencrypted data in transit. This article distills what we wish we'd known before our first production deployment, covering the architectural patterns and Terraform code that have kept our caching layers reliable and secure.
Subnet group — Private subnets only
Your Redis cluster should never be directly accessible from the internet. We configure Elasticache to live in private subnets within your VPC, ensuring that application servers connect to Redis through internal networking. This is not optional in production environments.
When you create a subnet group, you're telling Elasticache which subnets are eligible for cluster placement. The subnet group should span multiple availability zones for redundancy. Here's how we define it:
resource "aws_elasticache_subnet_group" "redis" {
name = "app-redis-subnet-group"
subnet_ids = var.private_subnet_ids
tags = {
Name = "app-redis-subnet-group"
}
}
variable "private_subnet_ids" {
description = "List of private subnet IDs across multiple AZs"
type = list(string)
}
The critical pattern here: subnet_ids should only contain private subnets. We've seen teams accidentally add public subnets, which creates a pathway for external access. Private subnets have no direct internet gateway route, so your Redis nodes remain isolated.
Pair the subnet group with a security group that explicitly allows inbound traffic only from your application tier—never from 0.0.0.0/0. We typically use security group source references rather than CIDR blocks:
resource "aws_security_group" "redis" {
name = "app-redis-sg"
vpc_id = var.vpc_id
ingress {
from_port = 6379
to_port = 6379
protocol = "tcp"
security_groups = [var.app_security_group_id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "app-redis-sg"
}
}
This way, only instances with the app security group can connect. If your architecture changes and you spin up new application servers, you simply attach that security group to them, and Redis connectivity works automatically.
Replication group — Primary with replicas and automatic failover
A single Redis node is a single point of failure. We use Elasticache replication groups to run a primary node with one or more read replicas, combined with automatic failover. If the primary fails, the replica is promoted with minimal downtime.
The replication group is the core construct in Elasticache. It manages the primary and replica nodes as a logical unit. Here's our production pattern:
resource "aws_elasticache_replication_group" "redis" {
replication_group_description = "App cache layer with failover"
engine = "redis"
engine_version = "7.0"
node_type = "cache.r7g.large"
num_cache_clusters = 3
automatic_failover_enabled = true
parameter_group_name = aws_elasticache_parameter_group.redis.name
port = 6379
subnet_group_name = aws_elasticache_subnet_group.redis.name
security_group_ids = [aws_security_group.redis.id]
at_rest_encryption_enabled = true
transit_encryption_enabled = true
auth_token = random_password.redis_token.result
automatic_minor_version_upgrade = true
maintenance_window = "mon:03:00-mon:04:00"
log_delivery_configuration {
destination = aws_cloudwatch_log_group.redis_slow_log.name
destination_type = "cloudwatch-logs"
log_format = "json"
log_type = "slow-log"
enabled = true
}
tags = {
Name = "app-redis"
}
depends_on = [
aws_elasticache_subnet_group.redis,
aws_elasticache_parameter_group.redis
]
}
resource "random_password" "redis_token" {
length = 32
special = true
}
Key decisions here:
- num_cache_clusters = 3: One primary, two replicas. This gives you redundancy and read scaling. The primary handles writes; replicas handle reads.
- automatic_failover_enabled = true: When the primary fails, Elasticache automatically promotes a replica. Your application doesn't need to know or care which node is primary.
- engine_version: We pin a specific version rather than using "latest" because Redis updates can change behavior. You control when upgrades happen.
- node_type: We use graviton2-based instances (r7g) where possible because they offer better price-per-unit performance. For smaller deployments, cache.t4g.small works well.
The at_rest_encryption_enabled, transit_encryption_enabled, and auth_token lines are covered in the next section, but they're non-negotiable in production.
Encryption — In-transit and at-rest encryption with auth tokens
Redis doesn't enforce authentication by default, and data travels unencrypted on the wire. In a compliance-sensitive environment—and frankly, all production environments should be—you need both.
In-transit encryption uses TLS to encrypt data flowing between your application and Redis. At-rest encryption protects data stored on disk. Auth tokens add password-based authentication. Together, they prevent unauthorized access and eavesdropping.
Here's how we generate and manage the auth token:
resource "random_password" "redis_token" {
length = 32
special = true
override_special = "!&#$^<>-"
}
resource "aws_secretsmanager_secret" "redis_token" {
name = "app-redis-auth-token"
recovery_window_in_days = 7
}
resource "aws_secretsmanager_secret_version" "redis_token" {
secret_id = aws_secretsmanager_secret.redis_token.id
secret_string = random_password.redis_token.result
}
Store the token in Secrets Manager rather than plaintext or environment variables. Your application retrieves it at runtime. This decouples the token from your infrastructure code and allows rotation without redeploying.
Your application's Redis client configuration looks like this (pseudocode for a Node.js example):
// Fetch token from Secrets Manager at application startup
const token = await secretsManager.getSecretValue('app-redis-auth-token');
const redis = new Redis({
host: process.env.REDIS_ENDPOINT,
port: 6379,
password: token.SecretString,
tls: {
rejectUnauthorized: false // For self-signed certs in development
}
});
The TLS connection uses Elasticache's managed certificates, so you don't need to provision or rotate your own. In-transit encryption is transparent once configured.
At-rest encryption is enabled at replication group creation and cannot be modified afterward. Plan this upfront. If you need to enable it on an existing cluster, you'll destroy and recreate it. In Terraform, use lifecycle { prevent_destroy = true } if you want to safeguard against accidental recreation.
Parameter groups — Tune eviction policies explicitly
A parameter group is a collection of Redis configuration parameters. The default group is permissive but often wrong for production workloads. We create custom parameter groups to enforce explicit eviction policies, connection limits, and memory management.
resource "aws_elasticache_parameter_group" "redis" {
name_prefix = "app-redis-"
family = "redis7"
parameter {
name = "maxmemory-policy"
value = "allkeys-lru"
}
parameter {
name = "timeout"
value = "300"
}
parameter {
name = "tcp-keepalive"
value = "60"
}
parameter {
name = "maxclients"
value = "65000"
}
tags = {
Name = "app-redis-params"
}
lifecycle {
create_before_destroy = true
}
}
Let's break down the critical parameters:
- maxmemory-policy = allkeys-lru: When Redis reaches memory capacity, it evicts the least-recently-used key. This is suitable for a general cache layer. Alternatives include
volatile-lru(only evict keys with TTL) ornoeviction(fail on out-of-memory). Choose based on your use case. - timeout = 300: Idle client connections close after 300 seconds. This prevents connection leaks. Set it based on your application's behavior; we typically use 5-10 minutes.
- tcp-keepalive = 60: Send TCP keepalive probes every 60 seconds. This detects dead connections and prevents NAT timeouts on long-lived connections.
- maxclients = 65000: Upper limit on concurrent client connections. Adjust based on your application tier size. If you run 100 instances each opening 10 connections, you need at least 1000, but we recommend headroom.
Parameter group family must match your Redis engine version. Redis 7 uses redis7, Redis 6 uses redis6`, and so on. Mismatches cause apply failures.
Use create_before_destroy = true in the lifecycle block because parameter groups can't be modified in place for certain parameters. Terraform destroys the old group only after the new one is created, reducing downtime.
Common mistakes
Enabling encryption on an existing cluster: Elasticache doesn't support toggling encryption without recreating the cluster. Plan this upfront. If you have a production cluster without encryption, the cost of recreation—in terms of downtime and data loss—usually exceeds the cost of setting it up correctly the first time.
Underestimating node size: Choosing a node type that's too small forces evictions and increases latency. Monitor eviction rates in CloudWatch and rightsize after a week of production traffic. It's cheaper to start large and downsize later than to deal with cache misses and slow queries.
Mixing public and private subnets: A single public subnet in your subnet group defeats the purpose of private networking. Audit your subnet configuration and ensure all IDs are from private subnets only.
Forgetting maintenance windows: AWS applies patches during the maintenance window. If you don't set one, AWS chooses a time for you—potentially during business hours. Always define an explicit window outside peak traffic.
Ignoring logs: Slow-log tells you which commands are taking time. Configure it and actually check it. Many performance issues are visible in Redis logs before they impact application metrics.
Key takeaways
- Place Elasticache in private subnets only, protected by a security group that references your application tier's security group.
- Use replication groups with at least 3 nodes (1 primary + 2 replicas) and automatic failover enabled for production resilience.
- Enable both in-transit and at-rest encryption, and store auth tokens in Secrets Manager, not in code.
- Create explicit parameter groups to enforce eviction policies, connection limits, and keepalive settings suited to your workload.
- Encryption decisions are immutable; plan them before cluster creation.
- Monitor eviction rates and slow-log to catch performance issues early.






Comments
Loading comments…