Skip to content

Latest commit

Β 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🟧 AWS ElastiCache Terraform Module

A secure-by-default Amazon ElastiCache deployment β€” encrypted at rest, TLS-required in transit, Multi-AZ with automatic failover, and snapshot-backed β€” delivering a Redis/Valkey replication group (cluster mode disabled or enabled) or a Memcached cluster, with its subnet group, parameter group, and optional RBAC users/user group, from a single composite call. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • ⚑ Provisions an Amazon ElastiCache cache β€” the keystone is aws_elasticache_replication_group for Redis/Valkey, or aws_elasticache_cluster for Memcached. The two paths are mutually exclusive and selected by var.engine.
  • 🌐 Owns the subnet group (private subnets, β‰₯ 2 AZs for Multi-AZ) and an optional parameter group so a single call yields a complete, private topology.
  • πŸ‘₯ Optionally creates RBAC users (aws_elasticache_user) as a for_each map keyed by user id, bundled into one user group (aws_elasticache_user_group) and bound to the replication group β€” the modern alternative to a shared auth_token.
  • πŸ”’ Secure by default: at-rest encryption on (AWS-managed key, or a caller CMK), in-transit TLS on, Multi-AZ + automatic failover on, and 7-day snapshot retention β€” all opt-out, never opt-in.
  • 🧩 Consumes networking, security, and KMS by reference β€” it never creates a VPC, security group, or KMS key itself.
  • πŸ”‘ Secrets (AUTH token, RBAC passwords) are referenced from Secrets Manager rather than inlined; the module emits no plaintext credential.

πŸ’‘ Why it matters: ElastiCache routinely holds session state, tokens, and cached PII under privacy-regulation. An unencrypted or publicly-reachable cache is a far larger blast radius than the convenience saved by a loose default β€” so this module ships locked down and makes you opt out explicitly.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


πŸ—ΊοΈ Where this fits in the family

flowchart LR
 VPC["terraform-aws-vpc"]
 SG["terraform-aws-security-group"]
 KMS["terraform-aws-kms"]
 SM["terraform-aws-secrets-manager"]
 CWL["terraform-aws-cloudwatch-log-group"]
 EC["terraform-aws-elasticache"]
 APP["Application tier<br/>(ECS / EKS / EC2)"]

 VPC -->|subnet_ids| EC
 SG -->|security_group_ids| EC
 KMS -->|kms_key_arn| EC
 SM -->|auth_token / user passwords| EC
 EC -->|slow-log / engine-log| CWL
 EC -->|primary / reader / configuration endpoint| APP

 style EC fill:#FF9900,color:#fff
Loading

ElastiCache sits at the data / caching tier. It is downstream of the networking, security-group, and KMS foundations and upstream of the application tier that connects to its endpoints. Secrets Manager supplies the AUTH token / RBAC passwords; CloudWatch Logs (or Kinesis Firehose) is the optional log destination.


🧬 What this module builds

flowchart TD
 subgraph caller["Caller-supplied (by reference)"]
 SUBNETS["subnet_ids<br/>(terraform-aws-vpc)"]
 SGS["security_group_ids<br/>(terraform-aws-security-group)"]
 CMK["kms_key_arn<br/>(terraform-aws-kms)"]
 SECRET["auth_token / passwords<br/>(terraform-aws-secrets-manager)"]
 end

 subgraph mod["terraform-aws-elasticache"]
 SUB["aws_elasticache_subnet_group.this"]
 PG["aws_elasticache_parameter_group.this<br/>(optional)"]
 U["aws_elasticache_user.this<br/>for_each β€” RBAC users (Redis/Valkey)"]
 UG["aws_elasticache_user_group.this<br/>(optional)"]
 RG["aws_elasticache_replication_group.this<br/>keystone A β€” Redis/Valkey"]
 MC["aws_elasticache_cluster.this<br/>keystone B β€” Memcached"]
 end

 SUBNETS --> SUB
 SUB --> RG
 SUB --> MC
 PG --> RG
 PG --> MC
 SGS --> RG
 SGS --> MC
 CMK --> RG
 SECRET --> RG
 SECRET --> U
 U --> UG
 UG --> RG

 style RG fill:#FF9900,color:#fff
Loading
Resource Role
aws_elasticache_replication_group.this Keystone A β€” Redis/Valkey replication group (cluster mode disabled or enabled). Encryption, HA, backups, RBAC binding
aws_elasticache_cluster.this Keystone B β€” Memcached cluster (selectable alternative, mutually exclusive with A)
aws_elasticache_subnet_group.this Placement across caller-supplied private subnets (β‰₯ 2 AZs for Multi-AZ)
aws_elasticache_parameter_group.this Engine parameters (created only when parameter_group is set; e.g. cluster-enabled = yes)
aws_elasticache_user.this RBAC users, for_each over var.users (Redis/Valkey only)
aws_elasticache_user_group.this RBAC user group bound to the replication group (created only when user_group is set)

βœ… Provider / Versions

Requirement Version
Terraform >= 1.12.0
hashicorp/aws >= 6.0, < 7.0

No provider {} block is declared inside the module β€” it inherits the caller's configured provider (and credential chain / Region). See the AWS Prerequisites Region note below.


πŸ”‘ Required IAM Permissions

The Terraform identity needs the following (least-privilege). RBAC and KMS actions are needed only on the Redis/Valkey path with their respective features enabled.

Action Required for Notes
elasticache:CreateReplicationGroup, elasticache:ModifyReplicationGroup, elasticache:DeleteReplicationGroup, elasticache:DescribeReplicationGroups Redis/Valkey lifecycle Keystone A
elasticache:CreateCacheCluster, elasticache:ModifyCacheCluster, elasticache:DeleteCacheCluster, elasticache:DescribeCacheClusters Memcached lifecycle Keystone B
elasticache:CreateCacheSubnetGroup, elasticache:ModifyCacheSubnetGroup, elasticache:DeleteCacheSubnetGroup, elasticache:DescribeCacheSubnetGroups Subnet group Module-owned
elasticache:CreateCacheParameterGroup, elasticache:ModifyCacheParameterGroup, elasticache:DeleteCacheParameterGroup, elasticache:DescribeCacheParameterGroups Parameter group Only when parameter_group is set
elasticache:CreateUser, elasticache:ModifyUser, elasticache:DeleteUser, elasticache:DescribeUsers RBAC users Redis/Valkey; only when users is set
elasticache:CreateUserGroup, elasticache:ModifyUserGroup, elasticache:DeleteUserGroup, elasticache:DescribeUserGroups RBAC user group Redis/Valkey; only when user_group is set
elasticache:AddTagsToResource, elasticache:RemoveTagsFromResource, elasticache:ListTagsForResource Tagging All taggable resources
elasticache:CreateSnapshot, elasticache:DescribeSnapshots Final / manual snapshots final_snapshot_identifier on destroy (Redis/Valkey)
kms:DescribeKey, kms:CreateGrant, kms:RetireGrant CMK at-rest encryption Only when kms_key_arn (CMK) supplied (Redis/Valkey)
iam:CreateServiceLinkedRole First-time AWSServiceRoleForElastiCache creation One-time per account; harmless if it already exists

⚠️ Scope the resource ARNs in your policy to arn:aws:elasticache:<region>:<account>:replicationgroup:* / :cluster:* / :subnetgroup:* / :parametergroup:* / :user:* / :usergroup:* patterns rather than "*" where your governance allows.


πŸ“‹ AWS Prerequisites

  • Service-linked role: AWSServiceRoleForElastiCache is auto-created on first ElastiCache use (iam:CreateServiceLinkedRole). It lets the service manage ENIs and resources inside your VPC.
  • Networking: the supplied subnet_ids must be private (no public endpoint β€” PII/privacy-regulation baseline) and span at least two Availability Zones when Multi-AZ / automatic failover is enabled (both default true). The security groups must allow the cache port (6379 Redis/Valkey, 11211 Memcached) from the application security group only β€” never 0.0.0.0/0.
  • Customer-managed KMS key (optional, Redis/Valkey): to use a CMK for at-rest encryption, its key policy must allow the ElastiCache service principal (elasticache.amazonaws.com) the standard kms:Encrypt/Decrypt/GenerateDataKey*/CreateGrant set. Toggling at_rest_encryption_enabled is FORCE-NEW.
  • In-transit encryption + AUTH: transit_encryption_enabled = true (default) is required to use an auth_token or RBAC. AUTH tokens are 16–128 printable characters. On engine versions < 7.0.5 enabling in-transit encryption is FORCE-NEW; from 7.0.5+ use transit_encryption_mode (preferred β†’ required) for a no-downtime migration.
  • RBAC default user: ElastiCache requires the default user to be a member of any user group β€” include it in users or via user_group.additional_user_ids.
  • Durability mode (optional): durability (Valkey) requires Valkey 9.0+, cluster mode enabled, Multi-AZ, and one replica per shard. See ElastiCache durability limitations.
  • Engine / node availability: the chosen engine (redis/valkey/memcached), engine_version, and node_type must be offered in the target Region. data_tiering_enabled requires an r6gd node type.
  • Region model: the module relies on provider inheritance β€” there is no region variable. The caller's provider block (or alias) sets the Region. ElastiCache is a regional service β€” no us-east-1 global-service constraint applies.
  • Service quotas: default soft limit of 300 nodes per Region (and per cluster/replication-group limits), all raisable via Service Quotas. See Quotas for ElastiCache.

πŸ“ Module Structure

terraform-aws-elasticache/
β”œβ”€β”€ providers.tf # terraform{} + required_providers (aws >= 6.0, < 7.0); no provider{} block
β”œβ”€β”€ variables.tf # typed inputs: identity β†’ required β†’ optional β†’ tags β†’ timeouts
β”œβ”€β”€ main.tf # subnet group, parameter group, RBAC users/group, replication group (this) / cluster (this)
β”œβ”€β”€ outputs.tf # id + arn, endpoints, RBAC maps, subnet/parameter group, tags_all
β”œβ”€β”€ SCOPE.md # boundary, IAM, prerequisites, gotchas, secure defaults
└── README.md # this file

βš™οΈ Quick Start

The smallest working call β€” networking, security, and KMS wired from upstream modules:

module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-core-redis"
  engine             = "redis"
  node_type          = "cache.t4g.medium"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  tags = {
    Environment = "prod"
    DataClass   = "PII"
    CostCenter  = "platform-data"
  }
}

Everything not shown inherits the secure baseline: at-rest encryption on, TLS on, Multi-AZ + automatic failover on (num_cache_clusters = 2), and 7-day snapshot retention.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
subnet_ids list(string) terraform-aws-vpc (private subnets, β‰₯ 2 AZs)
security_group_ids list(string) terraform-aws-security-group
kms_key_arn string (KMS key ARN, optional) terraform-aws-kms
auth_token / user passwords string (sensitive, optional) terraform-aws-secrets-manager
notification_topic_arn string (SNS ARN, optional) app-integration module
log_delivery_configuration[*].destination string (log group / Firehose name) terraform-aws-cloudwatch-log-group

Emits

Output Description Consumed by
id Replication group id (Redis/Valkey) or cluster id (Memcached) references
arn ElastiCache ARN β€” cross-resource reference type IAM policies, monitoring, AWS Backup
name Cache identifier CLI / console
engine redis / valkey / memcached conditionals
engine_version_actual Running engine version drift / audit
primary_endpoint_address Redis/Valkey primary (write) endpoint β€” cluster mode disabled application write config
reader_endpoint_address Redis/Valkey reader endpoint β€” cluster mode disabled read traffic
configuration_endpoint_address Cluster-mode-enabled / Memcached configuration endpoint client auto-discovery
cluster_address Memcached DNS name without port application config
port Cache port (6379 / 11211) application config
cluster_enabled Whether sharding is on (Redis/Valkey) tooling
member_clusters Member node identifiers (Redis/Valkey) monitoring
cache_nodes Per-node objects (Memcached) monitoring / DNS
subnet_group_name / subnet_group_arn Cache subnet group references
parameter_group_name / parameter_group_arn Parameter group references
user_ids / user_arns RBAC users created (id list / id→ARN map) audit
user_group_id / user_group_arn RBAC user group bound to the group references
tags_all All tags incl. provider default_tags governance / audit

πŸ“š Example Library

1 Β· Minimal Redis (secure defaults)
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-min"
  engine             = "redis"
  node_type          = "cache.t4g.medium"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]
}

At-rest + in-transit encryption, Multi-AZ, automatic failover, and 7-day snapshots are all on by default.

2 Β· Customer-managed KMS key (CMK) for at-rest encryption
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-cmk"
  engine             = "redis"
  node_type          = "cache.r7g.large"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  at_rest_encryption_enabled = true           # default
  kms_key_arn                = module.kms.arn # auditable, independently-revocable CMK
}

Toggling at_rest_encryption_enabled is FORCE-NEW β€” choose encryption (and the CMK) at creation.

3 Β· Tags (merge with provider default_tags)
# Provider-level default_tags is the CALLER's concern (root module / pipeline):
provider "aws" {
  default_tags { tags = { ManagedBy = "terraform", Org = "" } }
}

module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-tagged"
  engine             = "redis"
  node_type          = "cache.t4g.medium"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  tags = {
    Environment = "prod"
    DataClass   = "PII"
    Org         = "-Data" # resource tag wins over default_tags on key conflict
  }
}
# module.elasticache.tags_all => { ManagedBy, Org=-Data, Environment, DataClass }
4 Β· Redis cluster mode enabled (sharded)
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-sharded"
  engine             = "redis"
  node_type          = "cache.r7g.large"
  subnet_ids         = module.vpc.private_subnet_ids # 3 AZs
  security_group_ids = [module.redis_sg.id]

  # Cluster mode enabled β€” num_node_groups forbids num_cache_clusters
  num_node_groups         = 3
  replicas_per_node_group = 2

  # Cluster mode requires a parameter group with cluster-enabled = yes
  parameter_group = {
    family     = "redis7"
    parameters = { "cluster-enabled" = "yes" }
  }
}

Three shards Γ— (1 primary + 2 replicas) = 9 nodes. Clients use the configuration endpoint for auto-discovery.

5 Β· RBAC users + user group (preferred over AUTH token)
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-rbac"
  engine             = "redis"
  node_type          = "cache.r7g.large"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  transit_encryption_enabled = true # required for RBAC

  users = {
    app-rw = {
      user_name     = "app-rw"
      access_string = "on ~* +@all"
      passwords     = [data.aws_secretsmanager_secret_version.app_rw.secret_string] # sensitive
    }
    app-ro = {
      user_name     = "app-ro"
      access_string = "on ~* +@read"
      passwords     = [data.aws_secretsmanager_secret_version.app_ro.secret_string]
    }
    default = {
      user_name            = "default"
      access_string        = "off ~* -@all" # locked-down default user
      no_password_required = true
    }
  }

  user_group = {
    user_group_id = "casey-redis-rbac-ug"
    engine        = "redis"
    # the module includes every `users` key automatically; add externally-managed ids here
  }
}

ElastiCache requires the default user to be a member of the user group β€” included above.

6 Β· Legacy AUTH token from Secrets Manager
data "aws_secretsmanager_secret_version" "redis_auth" {
  secret_id = "casey/redis/auth-token"
}

module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-authtoken"
  engine             = "redis"
  node_type          = "cache.t4g.medium"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  transit_encryption_enabled = true # required for auth_token
  auth_token                 = data.aws_secretsmanager_secret_version.redis_auth.secret_string
  auth_token_update_strategy = "ROTATE"
}

Prefer RBAC (example 5). Where an AUTH token is unavoidable, source it from Secrets Manager β€” never inline a literal.

7 Β· Valkey engine
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-valkey"
  engine             = "valkey"
  engine_version     = "8.0"
  node_type          = "cache.r7g.large"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]
}

Valkey is the Redis-compatible, lower-cost engine β€” RBAC, encryption, and HA all apply identically.

8 Β· Memcached cluster (cross-AZ)
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-memcached"
  engine             = "memcached"
  node_type          = "cache.t4g.medium"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.memcached_sg.id]

  num_cache_nodes = 3
  az_mode         = "cross-az"

  transit_encryption_enabled = true # Memcached 1.6.12+ supports in-transit; at-rest is unsupported
}

Memcached takes the aws_elasticache_cluster path. RBAC, replication, Multi-AZ, snapshots, and at-rest encryption do not apply β€” those inputs are ignored.

9 Β· Custom parameter group (module-created)
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-params"
  engine             = "redis"
  node_type          = "cache.r7g.large"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  parameter_group = {
    family      = "redis7"
    description = "Redis 7 tuned parameters"
    parameters = {
      "maxmemory-policy" = "allkeys-lru"
      "timeout"          = "300"
    }
  }
}

The module creates <name>-params and associates it (takes precedence over parameter_group_name).

10 Β· Log delivery to CloudWatch Logs
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-logs"
  engine             = "redis"
  node_type          = "cache.r7g.large"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  log_delivery_configuration = {
    slow = {
      destination      = module.redis_slow_log.name # terraform-aws-cloudwatch-log-group
      destination_type = "cloudwatch-logs"
      log_format       = "json"
      log_type         = "slow-log"
    }
    engine = {
      destination      = module.redis_engine_log.name
      destination_type = "cloudwatch-logs"
      log_format       = "json"
      log_type         = "engine-log"
    }
  }
}

Max 2 configs β€” one slow-log and one engine-log. The log destination is caller-owned and referenced by name.

11 Β· Custom backup + maintenance windows, 35-day retention
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-backups"
  engine             = "redis"
  node_type          = "cache.r7g.large"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  snapshot_retention_limit  = 35
  snapshot_window           = "04:00-05:00"         # UTC
  maintenance_window        = "sun:05:30-sun:06:30" # UTC
  final_snapshot_identifier = "casey-redis-backups-final"
}
12 Β· Data tiering (r6gd nodes)
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-tiered"
  engine             = "redis"
  node_type          = "cache.r6gd.xlarge" # r6gd required for data tiering
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  data_tiering_enabled = true
}

Data tiering offloads colder data to local NVMe SSD β€” only on r6gd node types.

13 Β· Restore from snapshot
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-restored"
  engine             = "redis"
  node_type          = "cache.r7g.large"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  snapshot_name = "casey-core-redis-2026-06-18" # FORCE-NEW
}
14 Β· Disposable dev cache (secure defaults relaxed)
module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-redis-dev"
  engine             = "redis"
  node_type          = "cache.t4g.small"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  # OPT-OUTS β€” dev only, never in prod / PII:
  num_cache_clusters         = 1 # single node
  automatic_failover_enabled = false
  multi_az_enabled           = false
  snapshot_retention_limit   = 0 # disables snapshots
  apply_immediately          = true

  tags = { Environment = "dev", DataClass = "synthetic" }
}

At-rest and in-transit encryption remain on β€” even disposable caches stay encrypted.

15 Β· 🏁 End-to-end composition (VPC β†’ SG β†’ KMS β†’ Secrets β†’ ElastiCache)
module "vpc" {
  source   = "git::https://github.com/microsoftexpert/terraform-aws-vpc?ref=v1.0.0"
  name     = "casey-data"
  vpc_cidr = "10.40.0.0/16"
  #... produces private_subnet_ids across 3 AZs
}

module "kms" {
  source      = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  description = "CMK for ElastiCache at-rest encryption"
  alias       = "alias/casey-redis"
}

module "redis_sg" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-security-group?ref=v1.0.0"
  name   = "casey-redis-sg"
  vpc_id = module.vpc.id

  ingress_rules = {
    redis = {
      from_port                    = 6379
      to_port                      = 6379
      ip_protocol                  = "tcp"
      referenced_security_group_id = module.app_sg.id # app tier only β€” never 0.0.0.0/0
      description                  = "Redis from application tier"
    }
  }
}

data "aws_secretsmanager_secret_version" "redis_app" {
  secret_id = "casey/redis/app-rw"
}

module "elasticache" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-elasticache?ref=v1.0.0"

  name               = "casey-core-redis"
  engine             = "redis"
  engine_version     = "7.1"
  node_type          = "cache.r7g.large"
  subnet_ids         = module.vpc.private_subnet_ids
  security_group_ids = [module.redis_sg.id]

  kms_key_arn                = module.kms.arn
  transit_encryption_enabled = true

  users = {
    app-rw  = { user_name = "app-rw", access_string = "on ~* +@all", passwords = [data.aws_secretsmanager_secret_version.redis_app.secret_string] }
    default = { user_name = "default", access_string = "off ~* -@all", no_password_required = true }
  }
  user_group = { user_group_id = "casey-core-redis-ug", engine = "redis" }

  snapshot_retention_limit  = 14
  final_snapshot_identifier = "casey-core-redis-final"

  tags = {
    Environment = "prod"
    DataClass   = "PII"
    Compliance  = "privacy-regulation"
  }
}

output "redis_primary_endpoint" { value = module.elasticache.primary_endpoint_address }
output "redis_reader_endpoint" { value = module.elasticache.reader_endpoint_address }
output "redis_arn" { value = module.elasticache.arn }

πŸ“₯ Inputs

Name Type Default Description
name string β€” (required) Replication group / cluster id. FORCE-NEW. 1–40 chars, lowercase
engine string "redis" redis / valkey β†’ replication group; memcached β†’ cluster. FORCE-NEW
node_type string β€” (required) Node instance class (e.g. cache.r7g.large). r6gd required for data tiering
subnet_ids list(string) β€” (required) Subnet group members (private, β‰₯ 2 AZs for Multi-AZ)
security_group_ids list(string) [] Security groups attached to the cache
description string null Replication group description (Redis/Valkey)
engine_version string null Engine version; null = latest at create
port number null Cache port; null = engine default (6379 / 11211)
num_cache_clusters number 2 Nodes (1 primary + replicas), cluster mode disabled
num_node_groups / replicas_per_node_group number null Shards / replicas-per-shard, cluster mode enabled
cluster_mode / durability string null Explicit cluster-mode / Valkey durability
data_tiering_enabled bool false Data tiering (r6gd only)
at_rest_encryption_enabled bool true At-rest encryption. FORCE-NEW (Redis/Valkey)
kms_key_arn string null CMK for at-rest encryption (else AWS-managed key)
transit_encryption_enabled bool true In-transit TLS; required for AUTH/RBAC
transit_encryption_mode string null preferred / required β€” no-downtime migration
auth_token / auth_token_update_strategy string null / ROTATE Legacy AUTH token (sensitive); prefer RBAC
automatic_failover_enabled bool true Auto-promote replica on failure (Redis/Valkey)
multi_az_enabled bool true Multi-AZ support (Redis/Valkey)
snapshot_retention_limit number 7 Snapshot retention days (0–35; 0 disables)
snapshot_window / maintenance_window string null UTC windows
snapshot_arns / snapshot_name list/string null Seed / restore from snapshot. snapshot_name FORCE-NEW
final_snapshot_identifier string null Final snapshot on destroy (Redis/Valkey)
parameter_group_name string null Existing parameter group to associate
parameter_group object null Module-creates a dedicated parameter group <name>-params
log_delivery_configuration map(object) {} slow-log / engine-log to CloudWatch / Firehose (max 2)
users map(object) {} RBAC users keyed by user id (Redis/Valkey)
user_group / user_group_ids object / list null RBAC user group (module-created or external)
num_cache_nodes / az_mode / preferred_availability_zones β€” 1 / null / null Memcached topology
outpost_mode / preferred_outpost_arn string null Memcached Outposts placement
apply_immediately / auto_minor_version_upgrade bool false / true Maintenance behavior
notification_topic_arn string null SNS topic for ElastiCache notifications
ip_discovery / network_type string null IP version / dual-stack
tags map(string) {} Tags merged onto all taggable resources
timeouts object {} create / update / delete timeouts

Full type schemas and per-field descriptions live in variables.tf.


🧾 Outputs

See the Emits table above. Primary outputs are id and arn; connectivity is exposed as primary_endpoint_address / reader_endpoint_address (cluster mode disabled) or configuration_endpoint_address (cluster mode enabled / Memcached); RBAC data is exposed as user_ids / user_arns / user_group_id; tags_all reflects the merged tag set.


🧠 Architecture Notes

  • ARN / ID formats.
  • Redis/Valkey arn: arn:aws:elasticache:<region>:<account>:replicationgroup:<name>; id = the replication group id (name).
  • Memcached arn: arn:aws:elasticache:<region>:<account>:cluster:<name>; id = the cluster id (name).
  • Subnet group arn: :subnetgroup:<name>-subnets; parameter group arn: :parametergroup:<name>-params; RBAC user arn: :user:<user_id>.
  • Two mutually-exclusive keystones. engine = memcached selects aws_elasticache_cluster.this; everything else selects aws_elasticache_replication_group.this. RBAC, replication, Multi-AZ, snapshots, and at-rest encryption are Redis/Valkey-only β€” those inputs are silently ignored on the Memcached path.
  • FORCE-NEW (immutable) fields. name (replication_group_id / cluster_id), engine, subnet_group_name membership, toggling at_rest_encryption_enabled, enabling/disabling cluster mode (cluster_enabled), snapshot_name, and β€” on engine versions < 7.0.5 β€” transit_encryption_enabled all destroy-and-recreate the cache (and its data). The module-created parameter group uses create_before_destroy so family changes don't deadlock.
  • Cluster mode disabled vs enabled. num_cache_clusters (disabled) and num_node_groups/replicas_per_node_group (enabled) are mutually exclusive β€” the module nulls num_cache_clusters whenever num_node_groups is set. Cluster mode enabled also needs a parameter group with cluster-enabled = yes.
  • tags ↔ tags_all ↔ default_tags. The module sets only resource-level tags (merged with per-user tags on RBAC users via merge(var.tags, each.value.tags)). The provider's default_tags is the caller's concern (never set inside a module). On a key collision the resource tag wins. tags_all (output) is the computed union AWS actually applied β€” use it for drift checks and audit.
  • Eventual consistency. Replication-group and node creation is asynchronous and can take many minutes; endpoints (primary_endpoint_address, reader_endpoint_address, configuration_endpoint_address) are stable DNS names that follow failover, so applications should resolve them at connect time rather than caching IPs.
  • Destroy ordering. Terraform tears down the cache β†’ user group β†’ users / parameter group / subnet group. A subnet group or parameter group cannot be deleted while a cache still references it; the dependency graph normally orders this, but a half-failed destroy can leave a group pinned by a lingering cache. With final_snapshot_identifier set, a final snapshot is taken before the Redis/Valkey group is deleted. There are no NAT/ENI destroy hazards beyond the service-managed ENIs the SLR cleans up.
  • No us-east-1 constraint. ElastiCache is a regional service β€” none of the us-east-1 global-service rules (CloudFront/WAF/ACM) apply. Rely on provider inheritance for the Region.

🧱 Design Principles

Secure by default; every weakening is an explicit, documented opt-out.

Posture Default How to opt out
At-rest encryption at_rest_encryption_enabled = true (AWS-managed key) false (Redis/Valkey; discouraged)
Customer-managed key available via kms_key_arn omit for the AWS-managed key
In-transit TLS transit_encryption_enabled = true false (strongly discouraged)
Public exposure private subnets only β€” no public endpoint n/a
Authentication RBAC user group or AUTH token (TLS on) open access (discouraged)
Automatic failover automatic_failover_enabled = true false (single node)
Multi-AZ multi_az_enabled = true false (single AZ)
Backups snapshot_retention_limit = 7 0 (disables snapshots; discouraged)
Final snapshot on destroy recommended via final_snapshot_identifier leave null (skips it)

Other principles: exactly four .tf files; one keystone named this per engine path; child collections (users, parameter group, user group) via for_each over map(object) (never count); deeply-typed object schemas with optional defaults; validation {} on every closed value set; no credential or region variables; secrets referenced not stored; primary outputs id + arn; tags_all surfaced.


πŸš€ Runbook

# Validate (no credentials needed)
terraform init -backend=false
terraform validate
terraform fmt -check

# Plan / apply (requires AWS credentials + Region)
# credentials via AWS_PROFILE / SSO / OIDC; Region via the provider block
terraform plan -out tfplan
terraform apply tfplan

plan/apply require a valid credential chain (profile / SSO / OIDC web-identity) and a configured Region. The module declares no provider {} block β€” supply it (and any assume_role) at the root. Replication-group creation can take several minutes; raise timeouts.create for large sharded topologies.


πŸ§ͺ Testing

  • terraform init -backend=false && terraform validate β€” schema and reference integrity.
  • terraform fmt -check β€” canonical formatting.
  • terraform plan against a sandbox account β€” confirm secure defaults render (at-rest + in-transit encryption, Multi-AZ, automatic failover, 7-day snapshots) and that the correct keystone (replication_group vs cluster) is selected by engine.
  • Post-apply smoke test: connect over TLS from an in-VPC host to the primary_endpoint_address (or configuration_endpoint_address) on 6379, authenticate with an RBAC user, and run PING / INFO replication.

πŸ’¬ Example Output

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

Outputs:

arn = "arn:aws:elasticache:us-east-2:123456789012:replicationgroup:casey-core-redis"
id = "casey-core-redis"
redis_primary_endpoint = "master.casey-core-redis.abc123.use2.cache.amazonaws.com"
redis_reader_endpoint = "replica.casey-core-redis.abc123.use2.cache.amazonaws.com"
user_group_id = "casey-core-redis-ug"

πŸ” Troubleshooting

Symptom Likely cause Fix
Tag drift on every plan default_tags overlaps a key the module also sets Drop the duplicate from one side; resource tags win β€” reconcile in the root module
AccessDenied on elasticache:CreateReplicationGroup Identity lacks the elasticache: actions Attach the Required IAM Permissions
InvalidParameterValue:... AWSServiceRoleForElastiCache SLR not yet created and iam:CreateServiceLinkedRole missing Grant iam:CreateServiceLinkedRole, or pre-create the SLR
automatic_failover_enabled rejected num_cache_clusters < 2 Set num_cache_clusters >= 2, or disable failover for single-node dev
Cluster-mode parameter group error num_node_groups set without cluster-enabled = yes Supply a parameter_group with parameters = { "cluster-enabled" = "yes" }
auth_token / RBAC rejected transit_encryption_enabled = false Enable in-transit encryption β€” it is required for AUTH/RBAC
User group create fails β€” missing default ElastiCache requires default user membership Add default to users or user_group.additional_user_ids
Toggling encryption shows full replacement at_rest_encryption_enabled / transit_encryption_enabled (<7.0.5) are FORCE-NEW Set them at creation; for in-transit on 7.0.5+ use transit_encryption_mode migration
Subnet/parameter group won't delete A lingering cache still references it (half-failed destroy) Remove the cache first; Terraform's graph normally orders this
CacheClusterNotFound / quota errors Region soft limit (300 nodes) hit Raise the quota via Service Quotas
Credential-chain errors on plan/apply No profile/SSO/OIDC resolved, or wrong Region Set AWS_PROFILE / assume the role; confirm the provider Region

πŸ”— Related Docs


🧑 "Infrastructure as Code should be standardized, consistent, and secure."