A secure-by-default Amazon DAX cluster — TLS in transit, encryption at rest, VPC-only placement, with its subnet group and an optional dedicated parameter group — delivering an in-memory read/write cache in front of one or more DynamoDB tables from a single composite call. Built for the AWS provider v6.x.
- ⚡ Provisions an Amazon DAX cluster (
aws_dax_cluster) — an in-memory, microsecond-latency cache that sits in front of DynamoDB tables via the DAX client SDK. - 🌐 Owns the cluster's subnet group (always created — DAX has no meaningful default placement) and an optional dedicated parameter group for TTL/query-cache tuning.
- 🔒 Secure by default: TLS in-transit (
cluster_endpoint_encryption_type = "TLS") and encryption at rest (server_side_encryption.enabled = true) are both on — opt-out, never opt-in. - 🧩 Consumes networking, security, and the DynamoDB-access IAM role by reference — it never creates a VPC, security group, IAM role, or DynamoDB table itself.
⚠️ Unlike RDS/DynamoDB/ElastiCache, DAX's own at-rest encryption has no customer-managed-KMS option in the provider schema — see Architecture Notes.
💡 Why it matters: DAX routinely caches hot DynamoDB reads that can include PII under privacy-regulation. A cache with TLS off or sitting in the wrong subnets defeats the whole point of encrypting the table behind it — so this module ships locked down and makes you opt out explicitly.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
flowchart LR
VPC["tf-mod-aws-vpc"]
SG["tf-mod-aws-security-group"]
IAM["tf-mod-aws-iam-role"]
DDB["tf-mod-aws-dynamodb"]
KMS["tf-mod-aws-kms"]
DAX["tf-mod-aws-dax"]
APP["Application tier<br/>ECS / EKS / EC2"]
VPC -->|subnet_ids| DAX
SG -->|security_group_ids| DAX
IAM -->|iam_role_arn| DAX
KMS -.->|CMK on the table, not DAX| DDB
DAX -->|assumes role, proxies reads/writes| DDB
DAX -->|configuration_endpoint| APP
style DAX fill:#FF9900,color:#fff
DAX sits at the caching tier in front of DynamoDB. It is downstream of the networking, security-group, and IAM-role foundations, and it proxies traffic through to a DynamoDB table (which may itself be encrypted with a customer-managed KMS key — that key never touches DAX directly). The application tier talks to DAX's configuration endpoint via the DAX client SDK, which falls back to DynamoDB directly on cache misses.
flowchart TD
subgraph caller["Caller-supplied (by reference)"]
SUBNETS["subnet_ids<br/>(tf-mod-aws-vpc)"]
SGS["security_group_ids<br/>(tf-mod-aws-security-group)"]
ROLE["iam_role_arn<br/>(tf-mod-aws-iam-role)"]
end
subgraph mod["tf-mod-aws-dax"]
SUB["aws_dax_subnet_group.this"]
PG["aws_dax_parameter_group.this<br/>(optional)"]
CL["aws_dax_cluster.this<br/>keystone"]
end
SUBNETS --> SUB
SUB --> CL
SGS --> CL
ROLE --> CL
PG --> CL
style CL fill:#FF9900,color:#fff
| Resource | Role |
|---|---|
aws_dax_cluster.this |
Keystone — the DAX cluster: nodes, encryption, maintenance, notifications |
aws_dax_subnet_group.this |
Placement across caller-supplied subnets (always created) |
aws_dax_parameter_group.this |
TTL / query-cache parameters (created only when parameter_group is set) |
ℹ️ Both Mermaid diagrams above were validated with the Mermaid Chart MCP tool (
validate_and_render_mermaid_diagram) during authoring — both returnedvalid: true.
| 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).
| Action | Required for | Notes |
|---|---|---|
dax:CreateCluster, dax:UpdateCluster, dax:DeleteCluster, dax:DescribeClusters |
Cluster lifecycle | Keystone |
dax:CreateSubnetGroup, dax:DeleteSubnetGroup, dax:DescribeSubnetGroups |
Subnet group | Module-owned, always created |
dax:CreateParameterGroup, dax:UpdateParameterGroup, dax:DeleteParameterGroup, dax:DescribeParameterGroups, dax:DescribeParameters |
Parameter group | Only when parameter_group is set |
dax:TagResource, dax:UntagResource, dax:ListTags |
Tagging | Cluster only — subnet/parameter groups are not taggable |
iam:PassRole (scoped to iam_role_arn) |
DAX assumes this role at runtime to call DynamoDB | Scope to the specific role ARN, never iam:PassRole on * |
iam:CreateServiceLinkedRole |
First-time AWSServiceRoleForDAX creation |
One-time per account; harmless if it already exists |
⚠️ Scope the resource ARNs in your policy toarn:aws:dax:<region>:<account>:cache/*/:subnet-group/*/:parameter-group/*patterns rather than"*"where your governance allows.
- Service-linked role:
AWSServiceRoleForDAXis auto-created on first DAX use in an account/Region (iam:CreateServiceLinkedRole) — it lets the service manage ENIs on the caller's behalf inside the VPC. iam_role_arntrust + permissions policy: the IAM role DAX assumes must trustdax.amazonaws.comand carry least-privilege DynamoDB actions (GetItem,BatchGetItem,Query,Scan,PutItem,UpdateItem,DeleteItem,BatchWriteItem,ConditionCheckItem,DescribeTable) scoped to the target table(s). This module does not create that role — wire it fromtf-mod-aws-iam-role.- VPC-only, no public endpoint concept: DAX clusters always live inside a VPC via the subnet group; there is no
publicly_accessibleflag to set because DAX has no public-endpoint mode at all. Security-group ingress (default port 8111) is what actually governs reachability. - Region constraints: none — DAX is a regional service with no us-east-1 global-service coupling.
- Node / Region availability: DAX node types and the service itself are not available in every Region — confirm availability before targeting a new Region.
- Service quotas: default soft limits apply per account/Region for cluster count and nodes-per-cluster; request an increase via Service Quotas if the topology needs more.
tf-mod-aws-dax/
├── 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, optional parameter group, DAX cluster (this)
├── outputs.tf # id + arn, endpoints, subnet/parameter group ids, tags_all
├── SCOPE.md # boundary, IAM, prerequisites, gotchas, secure defaults
└── README.md # this file
The smallest working call — networking, security, and the DynamoDB-access role wired from upstream modules:
module "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-orders-cache"
iam_role_arn = module.dax_role.arn
node_type = "dax.r5.large"
replication_factor = 3
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg.id]
tags = {
Environment = "prod"
DataClass = "PII"
CostCenter = "platform-data"
}
}Everything not shown inherits the secure baseline: TLS in-transit encryption and at-rest encryption on.
| Input | Type | Source module |
|---|---|---|
subnet_ids |
list(string) |
tf-mod-aws-vpc (private subnets) |
security_group_ids |
list(string) |
tf-mod-aws-security-group |
iam_role_arn |
string (IAM role ARN) |
tf-mod-aws-iam-role |
notification_topic_arn |
string (SNS ARN, optional) |
app-integration module |
| This module output | Feeds into |
|---|---|
id |
References to the cluster by its DAX identifier |
arn |
tf-mod-aws-iam-role / tf-mod-aws-iam-policy (scoping dax:* actions), monitoring |
name |
Tagging, monitoring, and DNS modules that key on resource name |
configuration_endpoint |
Application tier — the DAX client SDK connects here for cluster discovery |
cluster_address |
Application config that needs the bare DNS name |
port |
tf-mod-aws-security-group ingress rules for the DAX port |
nodes |
Monitoring / per-node routing |
cluster_endpoint_encryption_type |
Security review / compliance evidence that TLS is actually applied |
subnet_group_id |
Reference / import |
subnet_group_vpc_id |
Cross-checking tf-mod-aws-vpc wiring |
parameter_group_id |
Reference / import |
tags_all |
Governance / audit |
1 · Minimal cluster (secure defaults)
module "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-orders-cache"
iam_role_arn = module.dax_role.arn
node_type = "dax.r5.large"
replication_factor = 3
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg.id]
}TLS in-transit and at-rest encryption are both on by default.
2 · 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 "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-orders-cache"
iam_role_arn = module.dax_role.arn
node_type = "dax.r5.large"
replication_factor = 3
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg.id]
tags = {
Environment = "prod"
DataClass = "PII"
Org = "-Data" # resource tag wins over default_tags on key conflict
}
}
# module.dax.tags_all => { ManagedBy, Org=-Data, Environment, DataClass }3 · Customer-managed KMS — applied to the underlying table, not DAX
# DAX's own server_side_encryption has no kms_key_arn argument in the AWS
# provider schema — its at-rest encryption is always backed by an AWS-owned
# key. For an auditable, revocable CMK, encrypt the DynamoDB table DAX
# accelerates instead:
module "kms" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-kms?ref=v1.0.0"
description = "CMK for the orders DynamoDB table"
alias = "alias/casey-orders-table"
}
module "orders_table" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dynamodb?ref=v1.0.0"
name = "casey-orders"
hash_key = "pk"
attributes = [{ name = "pk", type = "S" }]
server_side_encryption = {
enabled = true
kms_key_arn = module.kms.arn
}
}
module "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-orders-cache"
iam_role_arn = module.dax_role.arn # policy scoped to module.orders_table.arn
node_type = "dax.r5.large"
replication_factor = 3
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg.id]
}DAX's own
server_side_encryption.enabled = true(default) still applies to the cache itself — the CMK secures the table it accelerates.
4 · Secure-by-default opt-out — TLS disabled (documented exception only)
module "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-legacy-cache"
iam_role_arn = module.dax_role.arn
node_type = "dax.t3.small"
replication_factor = 1
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg.id]
# OPT-OUT — only for a legacy client that cannot negotiate TLS; document the
# exception and prefer upgrading the client instead:
cluster_endpoint_encryption_type = "NONE"
tags = { Environment = "dev", DataClass = "synthetic" }
}This value cannot be changed on an existing cluster — choose it at creation time.
5 · High-availability cluster (3 nodes, no single point of failure)
module "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-orders-cache-ha"
iam_role_arn = module.dax_role.arn
node_type = "dax.r5.xlarge"
replication_factor = 3
availability_zones = ["us-east-2a", "us-east-2b", "us-east-2c"]
subnet_ids = module.vpc.private_subnet_ids # spans the same 3 AZs
security_group_ids = [module.dax_sg.id]
}6 · Dedicated parameter group (TTL tuning)
module "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-orders-cache-tuned"
iam_role_arn = module.dax_role.arn
node_type = "dax.r5.large"
replication_factor = 3
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg.id]
parameter_group = {
description = "Longer TTLs for a read-heavy catalog cache"
parameters = {
"query-ttl-millis" = "300000"
"record-ttl-millis" = "300000"
}
}
}The module creates <cluster_name>-params and associates it (takes precedence over parameter_group_name).
7 · Existing (externally-managed) parameter group
module "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-catalog-cache"
iam_role_arn = module.dax_role.arn
node_type = "dax.r5.large"
replication_factor = 3
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg.id]
parameter_group_name = "casey-shared-dax-params" # managed outside this module call
}8 · Notifications to SNS
module "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-orders-cache"
iam_role_arn = module.dax_role.arn
node_type = "dax.r5.large"
replication_factor = 3
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg.id]
notification_topic_arn = module.dax_notifications.arn # tf-mod-aws-sns
maintenance_window = "sun:05:00-sun:09:00"
}9 · Custom timeouts (large / slow-to-provision topology)
module "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-bulk-cache"
iam_role_arn = module.dax_role.arn
node_type = "dax.r5.4xlarge"
replication_factor = 5
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg.id]
timeouts = {
create = "60m"
update = "60m"
delete = "120m"
}
}Provider defaults are already generous (create/update 45m, delete 90m) — raise further only for very large clusters.
10 · for_each — a cache per microservice, at scale
locals {
caches = {
orders = { node_type = "dax.r5.large", replication_factor = 3 }
catalog = { node_type = "dax.r5.xlarge", replication_factor = 3 }
sessions = { node_type = "dax.t3.small", replication_factor = 1 }
}
}
module "dax" {
for_each = local.caches
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-${each.key}-cache"
iam_role_arn = module.dax_role[each.key].arn
node_type = each.value.node_type
replication_factor = each.value.replication_factor
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg[each.key].id]
tags = { Service = each.key, Environment = "prod" }
}11 · Import an existing cluster
import {
to = module.dax.aws_dax_cluster.this
id = "casey-orders-cache"
}Run terraform plan after adding the import block to reconcile any drift between the imported cluster's real configuration and the module call's arguments before applying.
12 · Least-privilege security group (application tier only)
module "dax_sg" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-security-group?ref=v1.0.0"
name = "casey-dax-sg"
vpc_id = module.vpc.id
ingress_rules = {
dax = {
from_port = 8111
to_port = 8111
ip_protocol = "tcp"
referenced_security_group_id = module.app_sg.id # app tier only — never 0.0.0.0/0
description = "DAX from application tier"
}
}
}13 · 🏁 End-to-end composition (VPC → SG → IAM role → DynamoDB → DAX)
module "vpc" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
name = "casey-data"
vpc_cidr = "10.40.0.0/16"
#... produces private_subnet_ids across 3 AZs
}
module "orders_table" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dynamodb?ref=v1.0.0"
name = "casey-orders"
hash_key = "pk"
attributes = [{ name = "pk", type = "S" }]
}
module "dax_role" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-iam-role?ref=v1.0.0"
name = "casey-orders-dax-role"
assume_role_policy = {
statements = [{
effect = "Allow"
principals = { type = "Service", identifiers = ["dax.amazonaws.com"] }
actions = ["sts:AssumeRole"]
}]
}
inline_policies = {
dynamodb-access = {
statements = [{
effect = "Allow"
actions = [
"dynamodb:GetItem", "dynamodb:BatchGetItem", "dynamodb:Query", "dynamodb:Scan",
"dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem",
"dynamodb:BatchWriteItem", "dynamodb:ConditionCheckItem", "dynamodb:DescribeTable",
]
resources = [module.orders_table.arn]
}]
}
}
}
module "dax_sg" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-security-group?ref=v1.0.0"
name = "casey-orders-dax-sg"
vpc_id = module.vpc.id
ingress_rules = {
dax = {
from_port = 8111
to_port = 8111
ip_protocol = "tcp"
referenced_security_group_id = module.app_sg.id
description = "DAX from application tier"
}
}
}
module "dax" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-dax?ref=v1.0.0"
cluster_name = "casey-orders-cache"
iam_role_arn = module.dax_role.arn
node_type = "dax.r5.large"
replication_factor = 3
subnet_ids = module.vpc.private_subnet_ids
security_group_ids = [module.dax_sg.id]
tags = {
Environment = "prod"
DataClass = "PII"
Compliance = "privacy-regulation"
}
}
output "dax_configuration_endpoint" { value = module.dax.configuration_endpoint }
output "dax_arn" { value = module.dax.arn }- Core:
cluster_name(FORCE-NEW),iam_role_arn,node_type,replication_factor - Networking:
subnet_ids(required, always creates the subnet group),subnet_group_description,security_group_ids,availability_zones - Encryption:
cluster_endpoint_encryption_type(defaultTLS),server_side_encryption(default{ enabled = true }, AWS-owned key only) - Configuration:
description,maintenance_window,notification_topic_arn - Parameter group:
parameter_group_name(external),parameter_group(module-created, takes precedence) - Tags / Timeouts:
tags(cluster only),timeouts(create/update/delete)
Full type schemas and per-field descriptions live in
variables.tf.
id— the DAX cluster id (same ascluster_name)arn— the DAX cluster ARN (cross-resource reference type)name— the cluster nametags_all— merged tags including providerdefault_tags(cluster only — subnet/parameter groups are not taggable)configuration_endpoint—{address, port}for the DAX client SDKcluster_address— bare DNS name without the portport— configuration-endpoint port (default 8111)nodes— per-node objects (id,address,port,availability_zone)cluster_endpoint_encryption_type— the in-transit posture actually applied (NONE/TLS)subnet_group_id— the subnet group's id (its name)subnet_group_vpc_id— the VPC id the subnet group resolved toparameter_group_id— conditionally present (try(..., null)): the module-created group's id, the caller-suppliedparameter_group_name, ornullfor the engine default
No output in this module is sensitive = true — DAX exposes no credential-bearing attributes.
- ARN / ID formats.
arn:aws:dax:<region>:<account>:cache/<cluster_name>;idis the (lowercased)cluster_name. Subnet group and parameter groupidare simply theirname— neither exposes anarnattribute in the AWS provider. - FORCE-NEW (immutable) fields.
cluster_nameis the resource's import id and cannot be renamed in place.availability_zonesis effectively immutable — nodes are placed at creation time; treat changes as replacement and confirm withterraform plan, since the provider schema does not explicitly annotate this argument as ForceNew. - No customer-managed KMS option for DAX itself.
server_side_encryptionis a bareenabledboolean backed only by an AWS-owned key — there is nowhere in the schema to plumb a CMK for the cluster. Apply a CMK to the underlying DynamoDB table instead (tf-mod-aws-kms+tf-mod-aws-dynamodb); see Example 3. tags↔tags_all↔default_tags. Onlyaws_dax_clusteris taggable — the subnet group and parameter group have notagsargument at all. The provider'sdefault_tagsis 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.- Eventual consistency / long operations. Cluster creation, node replacement, and teardown are slow relative to most AWS resources — the provider's own default timeouts are
create/update45m anddelete90m. The module only renders atimeouts {}block when the caller supplies one; otherwise these defaults apply silently. - Destroy ordering. The cluster must be destroyed before its subnet group or parameter group can be removed; Terraform's dependency graph orders this automatically. The module-created parameter group uses
create_before_destroyso name/description changes don't deadlock. parameter_group_nameis in-place updatable (not FORCE-NEW) — DAX applies a new parameter group without recreating the cluster, though individual parameter changes may require a node reboot depending on that parameter's apply type, which the Terraform resource does not surface.- No us-east-1 constraint. DAX is a regional service — none of the us-east-1 global-service rules (CloudFront/WAF/ACM) apply.
Secure by default; every weakening is an explicit, documented opt-out.
| Posture | Default | How to opt out |
|---|---|---|
| In-transit encryption (TLS) | cluster_endpoint_encryption_type = "TLS" |
"NONE" (discouraged; cannot be changed after creation) |
| At-rest encryption | server_side_encryption.enabled = true |
false (discouraged; AWS-owned key either way — no CMK option exists) |
| Network placement | VPC-only via caller-supplied private subnet_ids; module always creates the subnet group |
n/a — DAX has no public-endpoint concept to block |
| Ingress | security_group_ids caller-supplied; module never defaults to 0.0.0.0/0 |
n/a |
| High availability | caller chooses replication_factor (module does not force a minimum) |
set replication_factor = 1 for a single node (no read replicas) |
Other principles: exactly four .tf files; one keystone (aws_dax_cluster.this) plus a subnet group (always created) and an optional for_each-rendered parameter group (never count); deeply-typed object schemas with optional defaults; validation {} on every closed value set; no credential or region variables; primary outputs id + arn; tags_all surfaced.
# 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
⚠️ Pin the module source with?ref=v1.0.0— never a branch.plan/applyrequire a valid credential chain (profile / SSO / OIDC web-identity) and a configured Region. The module declares noprovider {}block — supply it (and anyassume_role) at the root. Cluster creation can take many minutes; raisetimeouts.createfor large multi-node topologies.
terraform init -backend=false && terraform validate— schema and reference integrity.terraform fmt -check— canonical formatting.terraform planagainst a sandbox account — confirm secure defaults render (cluster_endpoint_encryption_type = "TLS",server_side_encryption.enabled = true) and that the subnet group is always created.- Post-apply smoke test: from an in-VPC host in an allowed security group, connect with the DAX client SDK to
configuration_endpoint, issue aGetItem, and confirm a cache hit/miss cycle against the target DynamoDB table.
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
arn = "arn:aws:dax:us-east-2:123456789012:cache/casey-orders-cache"
id = "casey-orders-cache"
configuration_endpoint = "casey-orders-cache.abc123.dax-clusters.use2.cache.amazonaws.com:8111"
cluster_endpoint_encryption_type = "TLS"
| 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 dax:CreateCluster |
Identity lacks the dax: actions |
Attach the Required IAM Permissions |
AccessDenied / cluster stuck creating, DAX can't reach DynamoDB |
iam_role_arn's trust or permissions policy is missing or scoped wrong |
Confirm the role trusts dax.amazonaws.com and grants the DynamoDB actions against the target table's ARN |
InvalidParameterValue:... AWSServiceRoleForDAX |
SLR not yet created and iam:CreateServiceLinkedRole missing |
Grant iam:CreateServiceLinkedRole, or pre-create the SLR |
| Cluster shows full replacement on plan | cluster_name or availability_zones changed (immutable) |
Revert the change, or accept the replacement (data loss — the cache holds no durable state, but connections/endpoints change) |
| Application can't reach the configuration endpoint | Security group doesn't allow port 8111 from the app tier, or wrong subnets | Add an ingress rule scoped to the app security group; verify subnet_ids are reachable from the app tier |
| Client fails TLS handshake | cluster_endpoint_encryption_type = "NONE" but client expects TLS (or vice versa) |
Align the client SDK's TLS setting with the cluster's cluster_endpoint_encryption_type |
| Parameter change has no effect | Some DAX parameters require a node reboot to apply | Check the parameter's apply type in the AWS console/API; a rolling maintenance-window reboot may be required |
| Credential-chain errors on plan/apply | No profile/SSO/OIDC resolved, or wrong Region | Set AWS_PROFILE / assume the role; confirm the provider Region |
| Destroy hangs on subnet/parameter group | A lingering cluster still references it (half-failed destroy) | Remove the cluster first; Terraform's graph normally orders this |
- Amazon DynamoDB Accelerator (DAX) Developer Guide
- DAX cluster ARN and concepts reference
- Customizing DAX cluster settings (AZ preference, parameter groups, maintenance windows)
- Terraform
aws_dax_clusterresource reference - Terraform
aws_dax_subnet_groupresource reference - Terraform
aws_dax_parameter_groupresource reference - Module
SCOPE.md— boundary, IAM, prerequisites, gotchas - Upstream:
tf-mod-aws-vpc,tf-mod-aws-security-group,tf-mod-aws-iam-role,tf-mod-aws-dynamodb,tf-mod-aws-kms
🧡 "Infrastructure as Code should be standardized, consistent, and secure."