Quick take — A reusable hashicorp/aws ~> 5.0 Terraform module for AWS DocumentDB: a multi-AZ aws_docdb_cluster with instances, a private subnet group, a hardened TLS parameter group, KMS encryption, and audit logging. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.
Quickstart (copy-paste)
Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):
provider "aws" {
region = "us-east-1"
}
module "documentdb" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-documentdb?ref=v1.0.0"
name_prefix = "..." # Prefix for resource names; cluster id becomes `<name_pr…
subnet_ids = ["...", "..."] # Private subnet IDs (>= 2, in different AZs) for the sub…
vpc_security_group_ids = ["...", "..."] # Security groups controlling 27017/TLS ingress.
}
Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.
What this module is
Amazon DocumentDB is AWS’s managed, MongoDB-compatible document database. It separates compute from storage: a aws_docdb_cluster owns a single auto-scaling, six-way-replicated storage volume (across three Availability Zones), and you attach one writer plus zero-or-more reader instances (aws_docdb_cluster_instance) that share that volume. That architecture means failover is fast (a reader is promoted, no data copy) and read scaling is just “add another instance” — but it also means there are a half-dozen resources that must be wired together correctly every time: the cluster, its instances, a DB subnet group pinning it to private subnets, a cluster parameter group, a KMS key reference, and the master credentials.
Hand-rolling that per environment is where drift and security gaps creep in: someone forgets storage_encrypted, ships tls=disabled on the parameter group, leaves deletion_protection off in prod, or hardcodes the master password into state in plaintext. This module wraps all of it behind a small set of variables so every DocumentDB cluster in the estate comes out encrypted, multi-AZ, TLS-enforced, backed up, and exporting audit/profiler logs to CloudWatch — by default, not by reviewer vigilance.
When to use it
- You run MongoDB workloads (catalogs, user profiles, content, IoT documents, event payloads) and want a managed engine instead of operating MongoDB on EC2.
- You need a repeatable, multi-AZ cluster definition across
dev,staging, andprodthat differs only in instance class and instance count. - You want encryption at rest (KMS), encryption in transit (TLS enforced via parameter group), automated backups with a defined retention, and a hard guarantee that the cluster lives in private subnets only.
- You want master credentials managed by AWS Secrets Manager (
manage_master_user_password) rather than passing a plaintext password through Terraform variables and state.
Reach for something else if you need MongoDB features DocumentDB does not implement (certain aggregation stages, $where, full-text/geospatial parity, multi-document ACID across shards beyond its limits) — validate compatibility first — or if your dataset is tiny and intermittent, where DocumentDB’s always-on instance cost is hard to justify versus a serverless document store.
Module structure
terraform-module-aws-documentdb/
├── versions.tf # provider + Terraform version pins
├── main.tf # subnet group, parameter group, cluster, instances
├── variables.tf # var-driven inputs with validation
└── outputs.tf # cluster id/arn, endpoints, port, sg, secret arn
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
main.tf
locals {
cluster_identifier = "${var.name_prefix}-docdb"
# When AWS manages the master password, do NOT pass master_password.
master_password = var.manage_master_user_password ? null : var.master_password
tags = merge(
var.tags,
{
Module = "terraform-module-aws-documentdb"
ManagedBy = "Terraform"
}
)
}
# Pins the cluster to the (private) subnets you hand it.
resource "aws_docdb_subnet_group" "this" {
name = "${local.cluster_identifier}-subnets"
description = "Subnet group for ${local.cluster_identifier}"
subnet_ids = var.subnet_ids
tags = local.tags
}
# Cluster parameter group: enforce TLS in transit and enable audit logging.
resource "aws_docdb_cluster_parameter_group" "this" {
name = "${local.cluster_identifier}-params"
family = var.parameter_group_family
description = "Hardened parameters for ${local.cluster_identifier}"
parameter {
name = "tls"
value = var.tls_enabled ? "enabled" : "disabled"
}
parameter {
name = "audit_logs"
value = var.enable_audit_logs ? "enabled" : "disabled"
}
# Apply method is "pending-reboot" for these static parameters.
lifecycle {
create_before_destroy = true
}
tags = local.tags
}
resource "aws_docdb_cluster" "this" {
cluster_identifier = local.cluster_identifier
engine = "docdb"
engine_version = var.engine_version
port = var.port
master_username = var.master_username
master_password = local.master_password
manage_master_user_password = var.manage_master_user_password ? true : null
db_subnet_group_name = aws_docdb_subnet_group.this.name
db_cluster_parameter_group_name = aws_docdb_cluster_parameter_group.this.name
vpc_security_group_ids = var.vpc_security_group_ids
# Encryption at rest. KMS key optional; defaults to the aws/rds AWS-managed key.
storage_encrypted = true
kms_key_id = var.kms_key_id
# Backups + maintenance.
backup_retention_period = var.backup_retention_period
preferred_backup_window = var.preferred_backup_window
preferred_maintenance_window = var.preferred_maintenance_window
# Ship audit/profiler logs to CloudWatch Logs.
enabled_cloudwatch_logs_exports = var.enabled_cloudwatch_logs_exports
# Safety rails.
deletion_protection = var.deletion_protection
skip_final_snapshot = var.skip_final_snapshot
final_snapshot_identifier = var.skip_final_snapshot ? null : "${local.cluster_identifier}-final"
apply_immediately = var.apply_immediately
tags = local.tags
lifecycle {
# master_password churn (e.g. rotation) should not force replacement.
ignore_changes = [master_password]
}
}
# Writer + reader instances that attach to the shared cluster volume.
resource "aws_docdb_cluster_instance" "this" {
count = var.instance_count
identifier = "${local.cluster_identifier}-${count.index}"
cluster_identifier = aws_docdb_cluster.this.id
instance_class = var.instance_class
engine = "docdb"
# Lower index numbers are preferred as the writer during failover.
promotion_tier = count.index
auto_minor_version_upgrade = var.auto_minor_version_upgrade
enable_performance_insights = var.enable_performance_insights
preferred_maintenance_window = var.preferred_maintenance_window
apply_immediately = var.apply_immediately
tags = local.tags
}
variables.tf
variable "name_prefix" {
description = "Prefix for all resource names (e.g. \"acme-prod-catalog\"). The cluster id becomes \"<name_prefix>-docdb\"."
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]{1,40}$", var.name_prefix))
error_message = "name_prefix must be lowercase alphanumeric/hyphen, start with a letter, and be 2-41 chars."
}
}
variable "subnet_ids" {
description = "Private subnet IDs (>= 2, in different AZs) for the DocumentDB subnet group."
type = list(string)
validation {
condition = length(var.subnet_ids) >= 2
error_message = "DocumentDB requires at least two subnets in two different Availability Zones."
}
}
variable "vpc_security_group_ids" {
description = "Security group IDs to attach to the cluster (control 27017/TLS ingress here)."
type = list(string)
validation {
condition = length(var.vpc_security_group_ids) > 0
error_message = "Provide at least one security group ID."
}
}
variable "engine_version" {
description = "DocumentDB engine version (e.g. \"5.0.0\"). Pin it; do not float."
type = string
default = "5.0.0"
}
variable "parameter_group_family" {
description = "Cluster parameter group family; must match the major engine version (e.g. \"docdb5.0\")."
type = string
default = "docdb5.0"
validation {
condition = can(regex("^docdb[0-9]+\\.[0-9]+$", var.parameter_group_family))
error_message = "parameter_group_family must look like \"docdb5.0\"."
}
}
variable "instance_class" {
description = "Instance class for cluster instances (e.g. db.r6g.large)."
type = string
default = "db.r6g.large"
validation {
condition = can(regex("^db\\.", var.instance_class))
error_message = "instance_class must start with \"db.\" (e.g. db.r6g.large)."
}
}
variable "instance_count" {
description = "Number of cluster instances. 1 = writer only; >=2 adds readers + enables HA failover."
type = number
default = 2
validation {
condition = var.instance_count >= 1 && var.instance_count <= 16
error_message = "instance_count must be between 1 and 16."
}
}
variable "port" {
description = "TCP port the cluster listens on."
type = number
default = 27017
}
variable "master_username" {
description = "Master username. Cannot be a reserved word (e.g. \"admin\")."
type = string
default = "docdbadmin"
}
variable "manage_master_user_password" {
description = "If true, AWS generates and stores the master password in Secrets Manager (recommended). If true, leave master_password null."
type = bool
default = true
}
variable "master_password" {
description = "Master password (8-100 chars). Only used when manage_master_user_password = false. Pass via a secret-backed variable, never hardcode."
type = string
default = null
sensitive = true
validation {
condition = var.master_password == null || length(coalesce(var.master_password, "")) >= 8
error_message = "master_password must be at least 8 characters when provided."
}
}
variable "kms_key_id" {
description = "KMS key ARN for storage encryption. Null uses the AWS-managed aws/rds key. Supply a CMK for compliance/key-rotation control."
type = string
default = null
}
variable "tls_enabled" {
description = "Enforce TLS in transit on the cluster parameter group."
type = bool
default = true
}
variable "enable_audit_logs" {
description = "Enable DocumentDB audit logging (DDL/auth events) via the parameter group."
type = bool
default = true
}
variable "enabled_cloudwatch_logs_exports" {
description = "Log types to export to CloudWatch Logs. Valid: audit, profiler."
type = list(string)
default = ["audit", "profiler"]
validation {
condition = alltrue([for l in var.enabled_cloudwatch_logs_exports : contains(["audit", "profiler"], l)])
error_message = "enabled_cloudwatch_logs_exports may only contain \"audit\" and/or \"profiler\"."
}
}
variable "backup_retention_period" {
description = "Days to retain automated backups (1-35)."
type = number
default = 7
validation {
condition = var.backup_retention_period >= 1 && var.backup_retention_period <= 35
error_message = "backup_retention_period must be between 1 and 35 days."
}
}
variable "preferred_backup_window" {
description = "Daily backup window in UTC (hh24:mi-hh24:mi)."
type = string
default = "03:00-04:00"
}
variable "preferred_maintenance_window" {
description = "Weekly maintenance window in UTC (ddd:hh24:mi-ddd:hh24:mi)."
type = string
default = "sun:04:30-sun:05:30"
}
variable "deletion_protection" {
description = "Block accidental cluster deletion. Keep true in production."
type = bool
default = true
}
variable "skip_final_snapshot" {
description = "Skip the final snapshot on destroy. Keep false in production."
type = bool
default = false
}
variable "auto_minor_version_upgrade" {
description = "Allow automatic minor engine upgrades during the maintenance window."
type = bool
default = true
}
variable "enable_performance_insights" {
description = "Enable Performance Insights on cluster instances."
type = bool
default = true
}
variable "apply_immediately" {
description = "Apply modifications immediately instead of during the maintenance window. Can cause downtime."
type = bool
default = false
}
variable "tags" {
description = "Tags applied to all resources."
type = map(string)
default = {}
}
outputs.tf
output "cluster_id" {
description = "DocumentDB cluster identifier."
value = aws_docdb_cluster.this.id
}
output "cluster_arn" {
description = "ARN of the DocumentDB cluster."
value = aws_docdb_cluster.this.arn
}
output "cluster_resource_id" {
description = "Stable cluster resource ID (use in IAM auth policy conditions)."
value = aws_docdb_cluster.this.cluster_resource_id
}
output "endpoint" {
description = "Cluster (writer) endpoint — point write traffic here."
value = aws_docdb_cluster.this.endpoint
}
output "reader_endpoint" {
description = "Reader endpoint — load-balances across reader instances."
value = aws_docdb_cluster.this.reader_endpoint
}
output "port" {
description = "Port the cluster listens on."
value = aws_docdb_cluster.this.port
}
output "master_username" {
description = "Master username."
value = aws_docdb_cluster.this.master_username
}
output "master_user_secret_arn" {
description = "Secrets Manager ARN holding the AWS-managed master password (null if manage_master_user_password = false)."
value = try(aws_docdb_cluster.this.master_user_secret[0].secret_arn, null)
}
output "subnet_group_name" {
description = "Name of the DocumentDB subnet group."
value = aws_docdb_subnet_group.this.name
}
output "parameter_group_name" {
description = "Name of the cluster parameter group."
value = aws_docdb_cluster_parameter_group.this.name
}
output "instance_endpoints" {
description = "Per-instance endpoints (writer + readers)."
value = aws_docdb_cluster_instance.this[*].endpoint
}
How to use it
module "documentdb" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-documentdb?ref=v1.0.0"
name_prefix = "acme-prod-catalog"
subnet_ids = module.network.private_subnet_ids
vpc_security_group_ids = [aws_security_group.docdb.id]
engine_version = "5.0.0"
parameter_group_family = "docdb5.0"
instance_class = "db.r6g.xlarge"
instance_count = 3 # 1 writer + 2 readers
# AWS-managed master credentials in Secrets Manager (default).
manage_master_user_password = true
master_username = "catalogadmin"
# Customer-managed key for compliance.
kms_key_id = aws_kms_key.docdb.arn
backup_retention_period = 14
deletion_protection = true
skip_final_snapshot = false
tags = {
Environment = "prod"
Team = "catalog-platform"
CostCenter = "CC-4471"
}
}
# Security group: allow the app tier to reach DocumentDB over TLS.
resource "aws_security_group" "docdb" {
name_prefix = "acme-prod-catalog-docdb-"
vpc_id = module.network.vpc_id
ingress {
description = "DocumentDB from app tier"
from_port = 27017
to_port = 27017
protocol = "tcp"
security_groups = [module.app.security_group_id]
}
lifecycle {
create_before_destroy = true
}
}
# Downstream: hand the writer endpoint + secret ARN to an ECS service so the
# app reads its connection string and rotated password at runtime.
resource "aws_ecs_task_definition" "api" {
family = "catalog-api"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "1024"
memory = "2048"
container_definitions = jsonencode([
{
name = "api"
image = "${aws_ecr_repository.api.repository_url}:latest"
environment = [
{ name = "DOCDB_ENDPOINT", value = module.documentdb.endpoint },
{ name = "DOCDB_READER", value = module.documentdb.reader_endpoint },
{ name = "DOCDB_PORT", value = tostring(module.documentdb.port) }
]
secrets = [
{
name = "DOCDB_CREDENTIALS"
valueFrom = module.documentdb.master_user_secret_arn
}
]
}
])
}
With Terragrunt
Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.
1. Root config — live/terragrunt.hcl (inherited by every module):
remote_state {
backend = "s3"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
# ...s3 state bucket/container + key per path...
}
}
2. Module config — live/prod/documentdb/terragrunt.hcl:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-documentdb?ref=v1.0.0"
}
inputs = {
name_prefix = "..."
subnet_ids = ["...", "..."]
vpc_security_group_ids = ["...", "..."]
}
3. Deploy one environment, or roll out all modules together:
cd live/prod/documentdb && terragrunt apply # this module
terragrunt run-all apply # every module under live/prod
Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
name_prefix |
string |
— | yes | Prefix for resource names; cluster id becomes <name_prefix>-docdb. |
subnet_ids |
list(string) |
— | yes | Private subnet IDs (>= 2, in different AZs) for the subnet group. |
vpc_security_group_ids |
list(string) |
— | yes | Security groups controlling 27017/TLS ingress. |
engine_version |
string |
"5.0.0" |
no | DocumentDB engine version. |
parameter_group_family |
string |
"docdb5.0" |
no | Parameter group family matching the major engine version. |
instance_class |
string |
"db.r6g.large" |
no | Instance class for cluster instances. |
instance_count |
number |
2 |
no | Instance count; 1 = writer only, >=2 adds readers + HA. |
port |
number |
27017 |
no | TCP listener port. |
master_username |
string |
"docdbadmin" |
no | Master username (no reserved words). |
manage_master_user_password |
bool |
true |
no | Let AWS manage the master password in Secrets Manager. |
master_password |
string |
null |
no | Master password; only when manage_master_user_password = false. |
kms_key_id |
string |
null |
no | KMS key ARN for at-rest encryption (null = aws/rds key). |
tls_enabled |
bool |
true |
no | Enforce TLS in transit on the parameter group. |
enable_audit_logs |
bool |
true |
no | Enable audit logging via the parameter group. |
enabled_cloudwatch_logs_exports |
list(string) |
["audit","profiler"] |
no | Log types exported to CloudWatch Logs. |
backup_retention_period |
number |
7 |
no | Automated backup retention in days (1-35). |
preferred_backup_window |
string |
"03:00-04:00" |
no | Daily backup window (UTC). |
preferred_maintenance_window |
string |
"sun:04:30-sun:05:30" |
no | Weekly maintenance window (UTC). |
deletion_protection |
bool |
true |
no | Block accidental deletion. |
skip_final_snapshot |
bool |
false |
no | Skip final snapshot on destroy. |
auto_minor_version_upgrade |
bool |
true |
no | Allow automatic minor engine upgrades. |
enable_performance_insights |
bool |
true |
no | Enable Performance Insights on instances. |
apply_immediately |
bool |
false |
no | Apply changes immediately vs. maintenance window. |
tags |
map(string) |
{} |
no | Tags applied to all resources. |
Outputs
| Name | Description |
|---|---|
cluster_id |
DocumentDB cluster identifier. |
cluster_arn |
ARN of the cluster. |
cluster_resource_id |
Stable resource ID for IAM policy conditions. |
endpoint |
Writer endpoint for write traffic. |
reader_endpoint |
Reader endpoint load-balanced across readers. |
port |
Port the cluster listens on. |
master_username |
Master username. |
master_user_secret_arn |
Secrets Manager ARN for the AWS-managed master password. |
subnet_group_name |
Name of the DocumentDB subnet group. |
parameter_group_name |
Name of the cluster parameter group. |
instance_endpoints |
Per-instance endpoints (writer + readers). |
Enterprise scenario
A retail platform team runs its product catalog and inventory documents on DocumentDB. They instantiate this module once per environment from a shared pipeline: dev gets a single db.t3.medium instance with a 1-day backup and deletion_protection = false, while prod runs three db.r6g.xlarge instances (one writer, two readers) across three AZs, a customer-managed KMS key for PCI scope, 14-day backups, and audit + profiler logs streamed to CloudWatch and on to their SIEM. The application’s ECS service consumes the module’s reader_endpoint for catalog reads and pulls the rotated master credentials straight from master_user_secret_arn, so no database password ever touches Terraform state or a CI variable.
Best practices
- Always encrypt and enforce TLS. Keep
storage_encrypted = true(the module hardcodes it) andtls_enabled = true; supply a customer-managedkms_key_idfor regulated workloads so you control key rotation and grants. DocumentDB only lets you set encryption at cluster creation — you cannot toggle it later, so get it right the first time. - Let AWS manage the master password. Keep
manage_master_user_password = trueand consumemaster_user_secret_arndownstream. This keeps the secret out of Terraform state and enables Secrets Manager rotation; theignore_changes = [master_password]lifecycle prevents post-rotation drift. - Size for HA and read scale. Use
instance_count >= 2in production so a failover promotes a standby reader instead of rebuilding; route read-heavy traffic toreader_endpointand reserve the writerendpointfor writes. Set lowerpromotion_tier(index) on the instances you want promoted first. - Protect against accidental loss. Keep
deletion_protection = trueandskip_final_snapshot = falsein prod; setbackup_retention_periodto your RPO (7-14 days is common) and confirm thepreferred_backup_windowsits in a low-traffic period. - Control cost with instance class and graviton. DocumentDB bills per instance-hour plus I/O and storage — prefer Graviton (
db.r6g) classes for ~20% better price/performance, scaledevdown todb.t3.medium, and remember every reader you add is a full-price instance, so add readers for throughput, not just redundancy. - Standardize naming and tagging. Drive everything off
name_prefix(e.g.acme-prod-catalog) so cluster, subnet group, and parameter group names line up, and passEnvironment/CostCenter/Teamthroughtagsfor cost allocation and audit.