IaC AWS

Terraform Module: AWS DocumentDB — production MongoDB-compatible clusters with encryption, backups, and TLS

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

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 configlive/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 configlive/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

TerraformAWSDocumentDBModuleIaC
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading