Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS DocumentDB Terraform Module

A secure-by-default Amazon DocumentDB (MongoDB-compatible) cluster — encrypted at rest, TLS-required in transit, deletion-protected, and audit-logged — with its instances, DB subnet group, and cluster parameter group provisioned from a single composite call. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • 🗄️ Provisions an Amazon DocumentDB cluster (aws_docdb_cluster) — AWS's managed, MongoDB-compatible document database — as the keystone resource.
  • 👥 Creates writer + reader instances as a for_each map keyed by a stable, caller-supplied name, so each member is independently addressable and resizable without churning its siblings.
  • 🌐 Owns the DB subnet group (spanning ≥ 2 AZs) and the cluster parameter group (with tls = enabled baked in) so a single call yields a complete, private topology.
  • 🔒 Secure by default: storage encryption on, TLS required, deletion protection on, final snapshot taken, 7-day backups, and audit logs exported to CloudWatch.
  • 🔑 Master credentials are managed in AWS Secrets Manager by default (manage_master_user_password), so no password is stored in Terraform state.
  • 🧩 Consumes networking, security, and KMS by reference — it never creates a VPC, security group, or KMS key itself.

💡 Why it matters: DocumentDB clusters routinely hold PII under privacy-regulation. An unencrypted or publicly-reachable document store 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"]
 DOC["terraform-aws-documentdb"]
 APP["Application tier<br/>(ECS / EKS / EC2)"]

 VPC -->|subnet_ids| DOC
 SG -->|vpc_security_group_ids| DOC
 KMS -->|kms_key_arn| DOC
 DOC -->|master_user_secret| SM
 DOC -->|endpoint / reader_endpoint| APP

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

DocumentDB sits at the data 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 holds the managed master password.


🧬 What this module builds

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

 subgraph mod["terraform-aws-documentdb"]
 SG["aws_docdb_subnet_group.this"]
 PG["aws_docdb_cluster_parameter_group.this<br/>(tls = enabled)"]
 C["aws_docdb_cluster.this<br/>keystone — encrypted, deletion-protected"]
 I["aws_docdb_cluster_instance.this<br/>for_each — writer + readers"]
 SEC["master_user_secret<br/>(Secrets Manager, managed)"]
 end

 SUBNETS --> SG
 SG --> C
 PG --> C
 SGS --> C
 CMK --> C
 C --> I
 C -. manages.-> SEC

 style C fill:#FF9900,color:#fff
Loading
Resource Role
aws_docdb_cluster.this Keystone — the cluster control plane, encryption, backups, deletion protection
aws_docdb_cluster_instance.this Writer + reader instances (for_each over var.instances)
aws_docdb_subnet_group.this Placement across caller-supplied subnets (≥ 2 AZs)
aws_docdb_cluster_parameter_group.this Cluster parameters, with tls = enabled by default

✅ 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 Region model note below.


🔑 Required IAM Permissions

DocumentDB shares the RDS control plane, so it is managed through the rds: IAM action namespace — there is no docdb: namespace. The Terraform identity needs (least-privilege):

Action Required for Notes
rds:CreateDBCluster, rds:DeleteDBCluster, rds:ModifyDBCluster, rds:DescribeDBClusters Cluster lifecycle Keystone resource
rds:CreateDBInstance, rds:DeleteDBInstance, rds:ModifyDBInstance, rds:DescribeDBInstances Cluster instances One per instances map entry
rds:CreateDBSubnetGroup, rds:DeleteDBSubnetGroup, rds:DescribeDBSubnetGroups DB subnet group Module-owned
rds:CreateDBClusterParameterGroup, rds:ModifyDBClusterParameterGroup, rds:DeleteDBClusterParameterGroup, rds:DescribeDBClusterParameterGroups Cluster parameter group Carries the tls parameter
rds:AddTagsToResource, rds:RemoveTagsFromResource, rds:ListTagsForResource Tagging All taggable resources
rds:CreateDBClusterSnapshot, rds:DescribeDBClusterSnapshots Final / manual snapshots Final snapshot on destroy
secretsmanager:CreateSecret, secretsmanager:TagResource, secretsmanager:DescribeSecret Managed master password When manage_master_user_password = true (default)
kms:DescribeKey, kms:CreateGrant, kms:RetireGrant CMK storage encryption Only when kms_key_arn (CMK) is supplied
iam:CreateServiceLinkedRole First-time AWSServiceRoleForRDS creation One-time per account; harmless if it already exists

⚠️ Scope the resource ARNs in your policy to arn:aws:rds:<region>:<account>:cluster:* / :db:* / :subgrp:* / :cluster-pg:* patterns rather than "*" where your governance allows.


📋 AWS Prerequisites

  • Service-linked role: AWSServiceRoleForRDS (DocumentDB shares the RDS control plane) is auto-created on first use; iam:CreateServiceLinkedRole covers the one-time creation.
  • Networking: the supplied subnet_ids must span at least two Availability Zones in the target VPC. DocumentDB internally assigns up to three AZs — if you set availability_zones to fewer than 3, the next plan shows a difference that forces recreation, so either supply 3 AZs or leave it null.
  • Customer-managed KMS key (optional): to use a CMK for storage encryption, its key policy must allow the RDS/DocumentDB service principal (rds.amazonaws.com) the standard kms:Encrypt/Decrypt/GenerateDataKey*/CreateGrant set. Switching the key later forces a new cluster.
  • TLS in transit: the tls cluster parameter is enabled by default. Clients must present the Amazon DocumentDB CA bundle (global-bundle.pem). A FIPS 140-3 TLS option also exists at the parameter level for compliance-sensitive workloads.
  • Engine / instance availability: the chosen engine_version (and its parameter_group_family, e.g. docdb5.0) and each instance_class must be offered in the target Region — verify with aws docdb describe-db-engine-versions / describe-orderable-db-instance-options.
  • Region model: the module relies on provider inheritance — there is no region variable. The caller's provider block (or alias) sets the Region. DocumentDB is not a us-east-1 global-service constraint.
  • Service quotas: default soft limits apply per Region (e.g. clusters and instances per Region, instances per cluster). All are raisable via a Service Quotas request. See Amazon DocumentDB quotas and limits.

📁 Module Structure

terraform-aws-documentdb/
├── 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, cluster (this), instances (for_each)
├── outputs.tf # id + arn, endpoints, secret, instance maps, 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 "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-core-docdb"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]

  master_username = "caseyadmin"
  # manage_master_user_password defaults to true → password lives in Secrets Manager

  instances = {
    writer  = { instance_class = "db.r6g.large", promotion_tier = 0 }
    reader1 = { instance_class = "db.r6g.large", promotion_tier = 1 }
  }

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

Everything not shown inherits the secure baseline: storage encryption on, TLS required, deletion protection on, final snapshot on destroy, 7-day backups, audit logs exported.


🔌 Cross-Module Contract

Consumes

Input Type Source module
subnet_ids list(string) terraform-aws-vpc (private subnets, ≥ 2 AZs)
vpc_security_group_ids list(string) terraform-aws-security-group
kms_key_arn string (KMS key ARN, optional) terraform-aws-kms
master_password string (sensitive, optional) terraform-aws-secrets-manager (only if self-managing)

Emits

Output Description Consumed by
id Cluster identifier (resource id) references
arn Cluster ARN — cross-resource reference type IAM policies, AWS Backup, monitoring
cluster_identifier Cluster identifier CLI / console
cluster_resource_id Immutable, Region-unique resource id IAM policy conditions, CloudTrail
endpoint Cluster (writer) endpoint application write config
reader_endpoint Load-balanced reader endpoint read traffic
port Cluster port (default 27017) application config
hosted_zone_id Route 53 hosted zone of the endpoint alias records
cluster_members Instance identifiers in the cluster monitoring
master_user_secret Managed master-password secret block (ARN, KMS key, status) terraform-aws-secrets-manager consumers / apps
master_username Master username (sensitive) app bootstrap
instance_ids / instance_arns / instance_endpoints Per-instance maps keyed by instances key monitoring, DNS
writer_instance_keys Map keys currently acting as writer failover-aware tooling
subnet_group_name / subnet_group_arn DB subnet group references
parameter_group_name / parameter_group_arn Cluster parameter group references
tags_all All tags incl. provider default_tags governance / audit

📚 Example Library

1 · Minimal cluster (managed secret, secure defaults)
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-min"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]
  master_username        = "caseyadmin"

  instances = {
    writer = { instance_class = "db.r6g.large" }
  }
}
2 · Customer-managed KMS key (CMK) for storage encryption
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-cmk"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]
  master_username        = "caseyadmin"

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

  instances = {
    writer  = { instance_class = "db.r6g.xlarge", promotion_tier = 0 }
    reader1 = { instance_class = "db.r6g.xlarge", promotion_tier = 1 }
  }
}

Switching kms_key_arn later forces a new cluster — choose 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 "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-tagged"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]
  master_username        = "caseyadmin"

  instances = { writer = { instance_class = "db.r6g.large" } }

  tags = {
    Environment = "prod"
    DataClass   = "PII"
    Org         = "-Data" # resource tag wins over default_tags on key conflict
  }
}
# module.documentdb.tags_all => { ManagedBy, Org=-Data, Environment, DataClass }
4 · High-availability multi-AZ topology (writer + 2 readers)
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-ha"
  subnet_ids             = module.vpc.private_subnet_ids # 3 AZs
  vpc_security_group_ids = [module.docdb_sg.id]
  master_username        = "caseyadmin"

  instances = {
    writer  = { instance_class = "db.r6g.2xlarge", promotion_tier = 0 }
    reader1 = { instance_class = "db.r6g.2xlarge", promotion_tier = 1 }
    reader2 = { instance_class = "db.r6g.xlarge", promotion_tier = 2 }
  }
}

Lower promotion_tier is promoted first on failover. Manage instances by map key — resizing reader2 never touches writer or reader1.

5 · Self-managed master password from Secrets Manager
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-selfsecret"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]

  master_username             = "caseyadmin"
  manage_master_user_password = false
  master_password             = module.docdb_secret.secret_string # sensitive

  instances = { writer = { instance_class = "db.r6g.large" } }
}

Discouraged — a value here lands in plaintext in state. Prefer the managed-secret default (omit master_password).

6 · Custom backup window + 35-day retention
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-backups"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]
  master_username        = "caseyadmin"

  backup_retention_period      = 35
  preferred_backup_window      = "04:00-05:00"         # UTC
  preferred_maintenance_window = "sun:05:30-sun:06:30" # UTC

  instances = { writer = { instance_class = "db.r6g.large" } }
}
7 · Audit + profiler log exports to CloudWatch
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-logs"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]
  master_username        = "caseyadmin"

  enabled_cloudwatch_logs_exports = ["audit", "profiler"]

  instances = { writer = { instance_class = "db.r6g.large" } }
}

Enabling profiler also requires the matching profiler parameters in the cluster parameter group (see example 8).

8 · Custom cluster parameters (keep TLS, add profiler)
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-params"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]
  master_username        = "caseyadmin"

  parameter_group_family = "docdb5.0"
  cluster_parameters = {
    tls                   = { value = "enabled" } # keep the secure default
    profiler              = { value = "enabled" }
    profiler_threshold_ms = { value = "100" }
    audit_logs            = { value = "all" }
    ttl_monitor           = { value = "enabled" }
  }

  enabled_cloudwatch_logs_exports = ["audit", "profiler"]

  instances = { writer = { instance_class = "db.r6g.large" } }
}
9 · I/O-Optimized storage
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-iopt"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]
  master_username        = "caseyadmin"

  storage_type = "iopt1" # predictable price for I/O-heavy workloads

  instances = {
    writer  = { instance_class = "db.r6g.2xlarge" }
    reader1 = { instance_class = "db.r6g.2xlarge" }
  }
}
10 · Performance Insights on instances (with CMK)
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-pi"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]
  master_username        = "caseyadmin"
  kms_key_arn            = module.kms.arn

  instances = {
    writer = {
      instance_class                  = "db.r6g.xlarge"
      enable_performance_insights     = true
      performance_insights_kms_key_id = module.kms.arn
    }
    reader1 = {
      instance_class              = "db.r6g.xlarge"
      enable_performance_insights = true
    }
  }
}
11 · Restore from snapshot
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-restored"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]

  snapshot_identifier = "casey-core-docdb-2026-06-18" # FORCE-NEW; master_username/password ignored

  instances = { writer = { instance_class = "db.r6g.large" } }
}
12 · Point-in-time restore (PITR)
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-pitr"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]

  restore_to_point_in_time = {
    source_cluster_identifier  = "casey-core-docdb"
    restore_type               = "copy-on-write"
    use_latest_restorable_time = true
  }

  instances = { writer = { instance_class = "db.r6g.large" } }
}
13 · Disposable dev cluster (secure defaults relaxed)
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-dev"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]
  master_username        = "devadmin"

  # OPT-OUTS — dev only, never in prod / PII:
  deletion_protection = false
  skip_final_snapshot = true
  apply_immediately   = true

  instances = { writer = { instance_class = "db.t3.medium" } }

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

Storage encryption and TLS remain on — even disposable clusters stay encrypted.

14 · DocumentDB global cluster (secondary region member)
module "documentdb" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-documentdb?ref=v1.0.0"

  cluster_identifier     = "casey-docdb-dr"
  subnet_ids             = module.vpc_dr.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg_dr.id]

  global_cluster_identifier = aws_docdb_global_cluster.casey.id # join a global cluster
  # master_username omitted on a global secondary

  instances = { writer = { instance_class = "db.r6g.xlarge" } }
}
15 · 🏁 End-to-end composition (VPC → SG → KMS → DocumentDB → Secrets)
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 DocumentDB storage encryption"
  alias       = "alias/casey-docdb"
}

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

  ingress_rules = {
    mongo = {
      from_port                    = 27017
      to_port                      = 27017
      ip_protocol                  = "tcp"
      referenced_security_group_id = module.app_sg.id # app tier only
      description                  = "DocumentDB from application tier"
    }
  }
}

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

  cluster_identifier     = "casey-core-docdb"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.docdb_sg.id]

  master_username = "caseyadmin" # managed-secret path (default)
  kms_key_arn     = module.kms.arn

  backup_retention_period         = 14
  enabled_cloudwatch_logs_exports = ["audit"]

  instances = {
    writer  = { instance_class = "db.r6g.xlarge", promotion_tier = 0 }
    reader1 = { instance_class = "db.r6g.xlarge", promotion_tier = 1 }
  }

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

# The application reads the managed password straight from Secrets Manager:
output "docdb_secret_arn" {
  value = module.documentdb.master_user_secret[0].secret_arn
}
output "docdb_writer_endpoint" { value = module.documentdb.endpoint }
output "docdb_reader_endpoint" { value = module.documentdb.reader_endpoint }

📥 Inputs

Name Type Default Description
cluster_identifier string — (required) Cluster id. FORCE-NEW. lowercase, 1–63 chars
subnet_ids list(string) — (required) DB subnet group members, ≥ 2 AZs
instances map(object) — (required) Writer + reader instances keyed by stable name
vpc_security_group_ids list(string) [] Security groups to attach
engine string "docdb" Only docdb valid. FORCE-NEW
engine_version string null Engine version; null = AWS default
port number 27017 Connection port
availability_zones list(string) null Explicit AZs (use 3 or leave null)
master_username string null Master user. FORCE-NEW
manage_master_user_password bool true Manage password in Secrets Manager (secure default)
master_password string (sensitive) null Self-managed password (discouraged)
storage_encrypted bool true Encryption at rest. FORCE-NEW
kms_key_arn string null CMK for storage encryption. FORCE-NEW
storage_type string null standard or iopt1
deletion_protection bool true Block destroy
skip_final_snapshot bool false Skip final snapshot on destroy
final_snapshot_identifier string null Name of final snapshot
snapshot_identifier string null Restore from snapshot. FORCE-NEW
backup_retention_period number 7 Backup retention days (1–35)
preferred_backup_window string null UTC backup window
preferred_maintenance_window string null UTC maintenance window
enabled_cloudwatch_logs_exports list(string) ["audit"] audit and/or profiler
apply_immediately bool false Apply changes now vs. maintenance window
allow_major_version_upgrade bool false Permit major upgrades
global_cluster_identifier string null Join a global cluster
restore_to_point_in_time object null PITR config. FORCE-NEW
subnet_group_name / subnet_group_description string derived DB subnet group naming
parameter_group_family string "docdb5.0" Parameter group family. FORCE-NEW
parameter_group_name / parameter_group_description string derived Parameter group naming
cluster_parameters map(object) { tls = enabled } Cluster parameters (TLS on by default)
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; instance-level data is exposed as maps keyed by your instances keys; master_user_secret carries the managed-secret ARN; tags_all reflects the merged tag set.


🧠 Architecture Notes

  • ARN / ID formats.
  • Cluster arn: arn:aws:rds:<region>:<account>:cluster:<cluster_identifier>
  • Instance arn: arn:aws:rds:<region>:<account>:db:<instance_identifier>
  • cluster_resource_id is a stable, Region-unique id (e.g. cluster-ABC123...) — use it (not the name) in IAM policy conditions and CloudTrail correlation, since it survives rename.
  • rds: namespace. DocumentDB rides the RDS control plane — every API call, ARN service segment, and IAM action uses rds, not docdb. Write least-privilege policies accordingly.
  • FORCE-NEW (immutable) fields. cluster_identifier, engine, master_username, db_subnet_group_name (membership), parameter_group_family, storage_encrypted, switching kms_key_arn, snapshot_identifier, and restore_to_point_in_time all destroy-and-recreate the cluster. Per-instance availability_zone forces replacement of that instance.
  • tagstags_alldefault_tags. The module sets only resource-level 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. Cluster and instance creation is asynchronous; the writer is promoted from cluster members by DocumentDB, so writer_instance_keys may settle a moment after apply. Endpoints (endpoint, reader_endpoint) are stable DNS names that follow failover.
  • Three-AZ behavior. DocumentDB auto-distributes across up to 3 AZs. Setting availability_zones to fewer than 3 produces a perpetual diff that forces recreation — leave it null or supply 3.
  • Destroy ordering. Terraform tears down instances → cluster → subnet/parameter groups. With deletion_protection = true (default) the cluster destroy is blocked until you set it false; with skip_final_snapshot = false (default) a final snapshot is taken first. The DB subnet group cannot be deleted while the cluster still references it — Terraform's dependency graph handles the order, but a half-failed destroy can leave a subnet group pinned by a lingering cluster.
  • No us-east-1 constraint. DocumentDB is a regional service — none of the us-east-1 global-service rules (CloudFront/WAF/ACM) apply.

🧱 Design Principles

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

Posture Default How to opt out
Storage encryption at rest storage_encrypted = true (AWS-managed key) storage_encrypted = false (discouraged)
Customer-managed key available via kms_key_arn omit for the AWS-managed key
In-transit TLS tls cluster parameter enabled set cluster_parameters.tls = "disabled" (strongly discouraged)
Public exposure private subnets only — no public option n/a
Deletion protection deletion_protection = true deletion_protection = false
Final snapshot skip_final_snapshot = false skip_final_snapshot = true (discouraged)
Backups backup_retention_period = 7 lower to a minimum of 1
Audit logging enabled_cloudwatch_logs_exports = ["audit"] set [] (discouraged)
Managed master password manage_master_user_password = true (Secrets Manager) supply master_password (plaintext-in-state, discouraged)

Other principles: exactly four .tf files; single keystone aws_docdb_cluster.this; child instances 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; 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.


🧪 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 (encryption, TLS parameter, deletion protection, audit export) and that instances produce one writer + N readers.
  • Post-apply smoke test: connect with the DocumentDB CA bundle over TLS and verify a serverStatus/isMaster against the writer endpoint.

💬 Example Output

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

Outputs:

arn = "arn:aws:rds:us-east-2:123456789012:cluster:casey-core-docdb"
docdb_reader_endpoint = "casey-core-docdb.cluster-ro-abc123.us-east-2.docdb.amazonaws.com"
docdb_secret_arn = "arn:aws:secretsmanager:us-east-2:123456789012:secret:rds!cluster-...-AbC123"
docdb_writer_endpoint = "casey-core-docdb.cluster-abc123.us-east-2.docdb.amazonaws.com"
id = "casey-core-docdb"
writer_instance_keys = ["writer"]

🔍 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 rds:CreateDBCluster Identity lacks the rds: actions (DocumentDB uses rds, not docdb) Attach the Required IAM Permissions
Cannot delete cluster... deletion protection deletion_protection = true (default) Set deletion_protection = false, apply, then destroy
Destroy hangs on final snapshot skip_final_snapshot = false (default) needs a name Set final_snapshot_identifier, or skip_final_snapshot = true for disposable clusters
Perpetual diff on availability_zones Fewer than 3 AZs specified Supply 3 AZs or leave null (DocumentDB auto-assigns)
Clients can't connect / TLS handshake fails tls enabled but client missing CA bundle Use the Amazon DocumentDB CA bundle (global-bundle.pem)
InvalidParameterCombination switching kms_key_arn KMS key is FORCE-NEW; can't change in place Recreate the cluster (or restore from snapshot under a new key)
Subnet group won't delete A lingering cluster still references it (half-failed destroy) Remove the cluster first; Terraform's graph normally orders this
DBClusterQuotaExceeded Region soft limit on clusters/instances 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."