Skip to content

microsoftexpert/tf-mod-aws-dms

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🟧 AWS Database Migration Service (DMS) Terraform Module

Secure-by-default database migration compute, endpoints, and replication for AWS DMS β€” classic instance + task, DMS Serverless replication_config, generic + S3 endpoints, subnet group, event subscriptions, and imported certificates, all wired for encryption-at-rest, TLS-in-transit, and Secrets-Manager-backed credentials. Built for the AWS provider v6.x.

Terraform >= 1.12 aws 6.x module v1.0.0 type=composite resources=8


🧩 Overview

  • πŸ” Creates the DMS replication instance (aws_dms_replication_instance.this) β€” the keystone, classic instance-based replication compute β€” always encrypted at rest, private by default.
  • ⚑ Creates DMS Serverless replication (aws_dms_replication_config) β€” the modern, no-standing-instance migration path β€” side by side with the classic path.
  • πŸ”Œ Creates source/target endpoints for ~20 database and streaming engines (aws_dms_endpoint) with polymorphic engine-specific settings (MySQL, PostgreSQL, Oracle, Kafka, Kinesis, MongoDB, Redshift, OpenSearch/Elasticsearch, Redis).
  • πŸͺ£ Creates a dedicated S3 endpoint (aws_dms_s3_endpoint) β€” the modern replacement for extra_connection_attributes-based S3 endpoints.
  • πŸ—ΊοΈ Creates the replication subnet group (aws_dms_replication_subnet_group) the instance/serverless compute runs in, from private subnets you supply.
  • 🧡 Creates classic replication tasks (aws_dms_replication_task) running on the module's own instance.
  • πŸ”” Creates opt-in event subscriptions (aws_dms_event_subscription) publishing lifecycle events to an SNS topic.
  • πŸ“œ Imports SSL certificates (aws_dms_certificate, PEM or Oracle Wallet) and wires them into endpoints by key.
  • πŸ”’ Defaults every endpoint's ssl_mode to "require", every replication instance to private (publicly_accessible = false), and prefers Secrets-Manager-backed endpoint credentials over inline passwords.

πŸ’‘ Why it matters: DMS moves live, often PII-bearing, database rows between systems β€” a single migration module that defaults to encrypted, private, TLS-required, Secrets-Manager-backed replication removes an entire class of "we migrated the data over an unencrypted, publicly-reachable pipe" incidents before a caller ever writes a line of HCL.


❀️ 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["tf-mod-aws-vpc"] --> SG["tf-mod-aws-security-group"]
 VPC --> DMS["tf-mod-aws-dms"]
 SG --> DMS
 KMS["tf-mod-aws-kms"] --> DMS
 SECRETS["tf-mod-aws-secrets-manager"] --> DMS
 IAMROLE["tf-mod-aws-iam-role"] --> DMS
 S3["tf-mod-aws-s3-bucket"] --> DMS
 SNS["tf-mod-aws-sns"] --> DMS
 RDS["tf-mod-aws-rds"] -.source data.-> DMS
 AURORA["tf-mod-aws-rds-aurora"] -.source data.-> DMS
 DMS --> CWALARM["tf-mod-aws-cloudwatch-alarm (Phase 4)"]
 DMS --> IAMPOLICY["tf-mod-aws-iam-policy"]

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

🧬 What this module builds

flowchart TB
 subgraph DMSMOD["tf-mod-aws-dms"]
 RSG["aws_dms_replication_subnet_group.this (supporting, optional)"]
 RI["aws_dms_replication_instance.this (KEYSTONE)"]
 CERT["aws_dms_certificate.this (for_each certificates)"]
 EP["aws_dms_endpoint.this (for_each endpoints)"]
 S3EP["aws_dms_s3_endpoint.this (for_each s3_endpoints)"]
 RC["aws_dms_replication_config.this (for_each replication_configs, Serverless)"]
 RT["aws_dms_replication_task.this (for_each replication_tasks, classic)"]
 EVT["aws_dms_event_subscription.this (for_each event_subscriptions)"]
 end

 RSG --> RI
 RSG -->|subnet group id| RC
 CERT -->|certificate arn| EP
 CERT -->|certificate arn| S3EP
 EP -->|endpoint arn| RC
 EP -->|endpoint arn| RT
 S3EP -->|endpoint arn| RC
 S3EP -->|endpoint arn| RT
 RI -->|instance arn| RT
 RI -->|source ids| EVT
 RT -->|source ids| EVT

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

βœ… Provider / Versions

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

No provider {} block is declared inside this module β€” it inherits the caller's configured aws provider (credentials, region, default_tags, assume_role).


πŸ”‘ Required IAM Permissions

Least-privilege actions the Terraform executing principal needs (distinct from the DMS service roles DMS itself assumes at runtime β€” see AWS Prerequisites below):

Action Required for Notes
dms:CreateReplicationInstance, dms:DescribeReplicationInstances, dms:ModifyReplicationInstance, dms:DeleteReplicationInstance Replication instance lifecycle β€”
dms:CreateReplicationSubnetGroup, dms:DescribeReplicationSubnetGroups, dms:ModifyReplicationSubnetGroup, dms:DeleteReplicationSubnetGroup Subnet group lifecycle Requires dms-vpc-role to already exist (see Prerequisites)
dms:CreateEndpoint, dms:DescribeEndpoints, dms:ModifyEndpoint, dms:DeleteEndpoint, dms:TestConnection Endpoint + S3 endpoint lifecycle β€”
dms:CreateReplicationConfig, dms:DescribeReplicationConfigs, dms:ModifyReplicationConfig, dms:DeleteReplicationConfig Serverless replication config lifecycle β€”
dms:CreateReplicationTask, dms:DescribeReplicationTasks, dms:ModifyReplicationTask, dms:DeleteReplicationTask, dms:StartReplicationTask, dms:StopReplicationTask Classic replication task lifecycle β€”
dms:CreateEventSubscription, dms:DescribeEventSubscriptions, dms:ModifyEventSubscription, dms:DeleteEventSubscription Event subscription lifecycle β€”
dms:ImportCertificate, dms:DescribeCertificates, dms:DeleteCertificate Certificate import/lifecycle Content is stored in state plaintext β€” protect the backend
dms:AddTagsToResource, dms:ListTagsForResource, dms:RemoveTagsFromResource Tag management on every DMS resource β€”
iam:PassRole Passing dms-vpc-role, dms-cloudwatch-logs-role, endpoint service_access_role_arn/secrets_manager_access_role_arn to the DMS service No service-linked role is auto-created by this module β€” the three conventionally-named roles must pre-exist
kms:DescribeKey, kms:CreateGrant Customer-managed CMK on the instance, serverless compute, or S3 endpoint Only when a CMK ARN is supplied

πŸ“‹ AWS Prerequisites

  • Three conventionally-named IAM roles must exist before a replication instance/subnet group can be created, and are not created by this module:
  • dms-vpc-role β€” trusts dms.amazonaws.com, attaches AmazonDMSVPCManagementRole. Required for aws_dms_replication_instance / aws_dms_replication_subnet_group.
  • dms-cloudwatch-logs-role β€” trusts dms.amazonaws.com, attaches AmazonDMSCloudWatchLogsRole. Required for CloudWatch logging of replication tasks.
  • dms-access-for-endpoint β€” trusts dms.amazonaws.com, attaches AmazonDMSRedshiftS3Role. Required only for Redshift-target endpoints.

Create them via tf-mod-aws-iam-role before running this module (the AWS Console auto-creates them on first console use, but a Terraform-only pipeline in a locked-down account must create them explicitly).

  • No region constraint β€” unlike CloudFront/WAFv2/ACM, DMS has no us-east-1 global-resource coupling; standard provider inheritance applies.
  • Service quotas: replication instances per account/Region (default 20, soft), allocated storage per instance (5-6144 GiB), DMS Serverless DCU ceiling (384 DCUs per replication config). Request an increase before a large migration wave.
  • Engine-side CDC prerequisites (not enforced by this module β€” verify against the source engine's own docs): binary logging for MySQL CDC, wal_level = logical + a replication slot for PostgreSQL CDC, supplemental logging for Oracle CDC, CDC enabled at the database level for SQL Server.

πŸ“ Module Structure

tf-mod-aws-dms/
β”œβ”€β”€ providers.tf
β”œβ”€β”€ variables.tf
β”œβ”€β”€ main.tf
β”œβ”€β”€ outputs.tf
β”œβ”€β”€ README.md
└── SCOPE.md

βš™οΈ Quick Start

module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "core-migration"
  replication_instance_class = "dms.t3.medium"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.dms_sg.id]
  kms_key_arn            = module.kms.arn

  endpoints = {
    source_pg = {
      endpoint_type                   = "source"
      engine_name                     = "postgres"
      secrets_manager_arn             = module.pg_secret.arn
      secrets_manager_access_role_arn = module.dms_secrets_role.arn
      ssl_mode                        = "verify-full"
    }
  }

  s3_endpoints = {
    target_lake = {
      endpoint_type           = "target"
      bucket_name             = module.migration_bucket.id
      service_access_role_arn = module.dms_s3_role.arn
    }
  }

  replication_configs = {
    pg_to_lake = {
      replication_type    = "full-load-and-cdc"
      source_endpoint_key = "source_pg"
      target_endpoint_key = "target_lake"
      table_mappings = jsonencode({
        rules = [{
          "rule-type"      = "selection"
          "rule-id"        = "1"
          "rule-name"      = "1"
          "rule-action"    = "include"
          "object-locator" = { "schema-name" = "%", "table-name" = "%" }
        }]
      })
      start_replication = true
      compute_config = {
        max_capacity_units = 16
      }
    }
  }

  tags = {
    Environment = "prod"
    Application = "core-migration"
  }
}

πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
subnet_ids list(string) tf-mod-aws-vpc (private subnets, >= 2 AZs)
vpc_security_group_ids list(string) tf-mod-aws-security-group
kms_key_arn string (ARN) tf-mod-aws-kms
endpoints[*].secrets_manager_arn string (ARN) tf-mod-aws-secrets-manager
endpoints[*].secrets_manager_access_role_arn string (ARN) tf-mod-aws-iam-role
s3_endpoints[*].bucket_name string tf-mod-aws-s3-bucket
s3_endpoints[*].service_access_role_arn string (ARN) tf-mod-aws-iam-role
event_subscriptions[*].sns_topic_arn string (ARN) tf-mod-aws-sns

Emits

Output Description Consumed by
id Replication instance id Reference by callers needing the instance identifier
arn Replication instance ARN tf-mod-aws-iam-policy, tf-mod-aws-cloudwatch-log-group
endpoints Map: key -> { id, arn, engine_name } In-module replication_configs/replication_tasks, tf-mod-aws-iam-policy
s3_endpoints Map: key -> { id, arn, engine_display_name, external_id, status } Same as above
replication_configs Map: key -> { id, arn } tf-mod-aws-cloudwatch-alarm (Phase 4)
replication_tasks Map: key -> { id, arn, status } tf-mod-aws-cloudwatch-alarm, event subscriptions
event_subscriptions Map: key -> { id, arn } Governance/audit
certificates Map: key -> { id, arn } Endpoint certificate_arn wiring
tags_all Merged tags on the replication instance Governance/cost reporting

πŸ“š Example Library

1 Β· Minimal classic (instance-based) migration
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "poc-migration"
  replication_instance_class = "dms.t3.micro"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.dms_sg.id]

  endpoints = {
    source_mysql = {
      endpoint_type = "source"
      engine_name   = "mysql"
      server_name   = "onprem-mysql.internal"
      port          = 3306
      database_name = "app"
      username      = "dms_reader"
      password      = var.mysql_password # discouraged β€” see example 5
    }
    target_mysql = {
      endpoint_type                   = "target"
      engine_name                     = "mysql"
      secrets_manager_arn             = module.target_secret.arn
      secrets_manager_access_role_arn = module.dms_secrets_role.arn
    }
  }

  replication_tasks = {
    full_load = {
      migration_type      = "full-load"
      source_endpoint_key = "source_mysql"
      target_endpoint_key = "target_mysql"
      table_mappings = jsonencode({
        rules = [{
          "rule-type"      = "selection", "rule-id" = "1", "rule-name" = "1"
          "rule-action"    = "include"
          "object-locator" = { "schema-name" = "app", "table-name" = "%" }
        }]
      })
    }
  }
}
2 Β· DMS Serverless replication (recommended for new migrations)
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  # Serverless still needs a subnet group but no standing replication_instance_class
  # sizing β€” the module always declares replication_instance_id/class because the
  # keystone resource is required; size it minimally when only using Serverless.
  replication_instance_id    = "serverless-anchor"
  replication_instance_class = "dms.t3.micro"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.dms_sg.id]
  kms_key_arn            = module.kms.arn

  endpoints = {
    source_pg = {
      endpoint_type                   = "source"
      engine_name                     = "postgres"
      secrets_manager_arn             = module.pg_secret.arn
      secrets_manager_access_role_arn = module.dms_secrets_role.arn
    }
  }

  s3_endpoints = {
    target_lake = {
      endpoint_type           = "target"
      bucket_name             = module.migration_bucket.id
      service_access_role_arn = module.dms_s3_role.arn
      data_format             = "parquet"
    }
  }

  replication_configs = {
    pg_to_lake = {
      replication_type    = "full-load-and-cdc"
      source_endpoint_key = "source_pg"
      target_endpoint_key = "target_lake"
      table_mappings      = file("${path.module}/table-mappings.json")
      start_replication   = true
      compute_config = {
        max_capacity_units = 32
        min_capacity_units = 2
        multi_az           = true
      }
    }
  }
}
3 Β· MySQL-to-S3 data lake migration (Parquet, Glue Catalog)
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "mysql-to-lake"
  replication_instance_class = "dms.r5.large"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.dms_sg.id]
  kms_key_arn            = module.kms.arn

  endpoints = {
    source_mysql = {
      endpoint_type                   = "source"
      engine_name                     = "mysql"
      secrets_manager_arn             = module.mysql_secret.arn
      secrets_manager_access_role_arn = module.dms_secrets_role.arn
      mysql_settings = {
        events_poll_interval = 5
      }
    }
  }

  s3_endpoints = {
    target_lake = {
      endpoint_type                     = "target"
      bucket_name                       = module.migration_bucket.id
      service_access_role_arn           = module.dms_s3_role.arn
      data_format                       = "parquet"
      parquet_version                   = "parquet-2-0"
      encryption_mode                   = "SSE_KMS"
      server_side_encryption_kms_key_id = module.kms.arn
      glue_catalog_generation           = true
      date_partition_enabled            = true
      date_partition_sequence           = "YYYYMMDD"
    }
  }

  replication_configs = {
    mysql_to_lake = {
      replication_type    = "full-load-and-cdc"
      source_endpoint_key = "source_mysql"
      target_endpoint_key = "target_lake"
      table_mappings = jsonencode({
        rules = [{
          "rule-type"      = "selection", "rule-id" = "1", "rule-name" = "1"
          "rule-action"    = "include"
          "object-locator" = { "schema-name" = "app", "table-name" = "%" }
        }]
      })
      compute_config = { max_capacity_units = 64 }
    }
  }
}
4 Β· Secrets-Manager-backed endpoint credentials (recommended)
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "secure-migration"
  replication_instance_class = "dms.t3.medium"
  subnet_ids                 = module.vpc.private_subnet_ids
  vpc_security_group_ids     = [module.dms_sg.id]

  endpoints = {
    source_oracle = {
      endpoint_type                   = "source"
      engine_name                     = "oracle"
      secrets_manager_arn             = module.oracle_secret.arn    # tf-mod-aws-secrets-manager
      secrets_manager_access_role_arn = module.dms_secrets_role.arn # tf-mod-aws-iam-role
      ssl_mode                        = "verify-full"
    }
  }
}

Wire module.oracle_secret from tf-mod-aws-secrets-manager holding a JSON document with username, password, host, port, dbname/oracleSID β€” DMS reads it via secrets_manager_access_role_arn, never via the Terraform executing principal.

5 Β· Inline password (discouraged, explicit opt-in)
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "legacy-migration"
  replication_instance_class = "dms.t3.medium"
  subnet_ids                 = module.vpc.private_subnet_ids
  vpc_security_group_ids     = [module.dms_sg.id]

  endpoints = {
    source_sqlserver = {
      endpoint_type = "source"
      engine_name   = "sqlserver"
      server_name   = "onprem-sqlserver.internal"
      port          = 1433
      database_name = "app"
      username      = "dms_reader"
      password      = var.sqlserver_password # sensitive input; still plaintext in state
      ssl_mode      = "require"
    }
  }
}

⚠️ password is marked sensitive = true at the root-module input level as a compensating control, but the provider stores it in Terraform state in plaintext regardless (a documented, unavoidable behavior of aws_dms_endpoint). Use an encrypted backend and least-privilege state access, and prefer example 4 wherever the target account allows it.

6 Β· SSL-required PostgreSQL CDC endpoint with imported CA certificate
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "pg-cdc-migration"
  replication_instance_class = "dms.r5.large"
  subnet_ids                 = module.vpc.private_subnet_ids
  vpc_security_group_ids     = [module.dms_sg.id]

  certificates = {
    onprem_ca = {
      certificate_pem = file("${path.module}/certs/onprem-ca.pem")
    }
  }

  endpoints = {
    source_pg = {
      endpoint_type                   = "source"
      engine_name                     = "postgres"
      secrets_manager_arn             = module.pg_secret.arn
      secrets_manager_access_role_arn = module.dms_secrets_role.arn
      ssl_mode                        = "verify-full"
      certificate_key                 = "onprem_ca"
      postgres_settings = {
        plugin_name      = "pglogical"
        heartbeat_enable = true
        capture_ddls     = true
      }
    }
  }
}
7 Β· Kafka target endpoint with SASL-SSL
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "cdc-to-kafka"
  replication_instance_class = "dms.r5.large"
  subnet_ids                 = module.vpc.private_subnet_ids
  vpc_security_group_ids     = [module.dms_sg.id]

  endpoints = {
    target_kafka = {
      endpoint_type = "target"
      engine_name   = "kafka"
      kafka_settings = {
        broker            = "b-1.mymsk.kafka.us-east-1.amazonaws.com:9096"
        topic             = "cdc-events"
        security_protocol = "sasl-ssl"
        sasl_mechanism    = "scram-sha-512"
        sasl_username     = var.msk_username
        sasl_password     = var.msk_password
      }
    }
  }
}
8 Β· Event subscription on task failure (SNS)
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "core-migration"
  replication_instance_class = "dms.t3.medium"
  subnet_ids                 = module.vpc.private_subnet_ids
  vpc_security_group_ids     = [module.dms_sg.id]

  event_subscriptions = {
    task_failures = {
      sns_topic_arn    = module.dms_alerts.arn # tf-mod-aws-sns
      source_type      = "replication-task"
      event_categories = ["failure"]
    }
    instance_lifecycle = {
      sns_topic_arn    = module.dms_alerts.arn
      source_type      = "replication-instance"
      event_categories = ["creation", "failure", "deletion"]
    }
  }
}
9 Β· Customer-managed KMS CMK across instance, S3 endpoint, and Serverless compute
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "cmk-migration"
  replication_instance_class = "dms.r5.large"
  subnet_ids                 = module.vpc.private_subnet_ids
  vpc_security_group_ids     = [module.dms_sg.id]
  kms_key_arn                = module.kms.arn # tf-mod-aws-kms

  s3_endpoints = {
    target_lake = {
      endpoint_type                     = "target"
      bucket_name                       = module.migration_bucket.id
      service_access_role_arn           = module.dms_s3_role.arn
      encryption_mode                   = "SSE_KMS"
      server_side_encryption_kms_key_id = module.kms.arn
    }
  }

  replication_configs = {
    pg_to_lake = {
      replication_type    = "full-load-and-cdc"
      source_endpoint_key = "source_pg"
      target_endpoint_key = "target_lake"
      table_mappings      = file("${path.module}/table-mappings.json")
      compute_config = {
        max_capacity_units = 32
        kms_key_id         = module.kms.arn # falls back to var.kms_key_arn if omitted
      }
    }
  }
}
10 Β· Secure-by-default opt-out β€” public replication instance (documented exception)
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "internet-facing-migration"
  replication_instance_class = "dms.t3.medium"

  # DOCUMENTED EXCEPTION: source database is only reachable over the public
  # internet (no VPN/Direct Connect available). Requires PUBLIC subnets and a
  # security group locked to the source database's known egress IP range.
  publicly_accessible    = true
  subnet_ids             = module.vpc.public_subnet_ids
  vpc_security_group_ids = [module.dms_public_sg.id]
}
11 Β· `for_each` pattern β€” one module call, multiple isolated migrations
locals {
  migrations = {
    billing = {
      instance_class = "dms.t3.medium"
      source_secret  = module.billing_secret.arn
    }
    claims = {
      instance_class = "dms.r5.large"
      source_secret  = module.claims_secret.arn
    }
  }
}

module "dms" {
  for_each = local.migrations
  source   = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "${each.key}-migration"
  replication_instance_class = each.value.instance_class
  subnet_ids                 = module.vpc.private_subnet_ids
  vpc_security_group_ids     = [module.dms_sg.id]

  endpoints = {
    source = {
      endpoint_type                   = "source"
      engine_name                     = "postgres"
      secrets_manager_arn             = each.value.source_secret
      secrets_manager_access_role_arn = module.dms_secrets_role.arn
    }
  }

  tags = { MigrationDomain = each.key }
}
12 Β· Import an existing replication instance and endpoint
import {
  to = module.dms.aws_dms_replication_instance.this
  id = "legacy-migration-instance"
}

import {
  to = module.dms.aws_dms_endpoint.this["source_pg"]
  id = "legacy-source-pg-endpoint"
}

Match replication_instance_id, replication_instance_class, and the endpoints["source_pg"] object to the imported resource's live configuration before running terraform plan to avoid a disruptive diff.

13 Β· Kerberos-authenticated classic migration (requires the instance-based path)
module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "kerberos-migration"
  replication_instance_class = "dms.r5.large"
  subnet_ids                 = module.vpc.private_subnet_ids
  vpc_security_group_ids     = [module.dms_sg.id]

  kerberos_authentication_settings = {
    key_cache_secret_iam_arn = module.dms_kerberos_role.arn
    key_cache_secret_id      = module.kerberos_secret.arn
    krb5_file_contents       = file("${path.module}/krb5.conf")
  }

  endpoints = {
    source_sqlserver = {
      endpoint_type = "source"
      engine_name   = "sqlserver"
      server_name   = "corp-sqlserver.internal"
      port          = 1433
      database_name = "app"
    }
  }

  replication_tasks = {
    full_load_and_cdc = {
      migration_type      = "full-load-and-cdc"
      source_endpoint_key = "source_sqlserver"
      target_endpoint_key = "target_pg"
      table_mappings      = file("${path.module}/table-mappings.json")
    }
  }
}
14 Β· Tags merge with provider default_tags
# Root/provider block (outside this module):
# provider "aws" {
# default_tags {
# tags = { CostCenter = "-INFRA", ManagedBy = "terraform" }
# }
# }

module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "tagged-migration"
  replication_instance_class = "dms.t3.medium"
  subnet_ids                 = module.vpc.private_subnet_ids
  vpc_security_group_ids     = [module.dms_sg.id]

  tags = {
    Environment = "prod"
    Application = "claims-migration"
    # A key collision with default_tags (e.g. CostCenter) here would win β€”
    # resource-level tags always beat provider default_tags.
  }
}

output "dms_tags_all" {
  value = module.dms.tags_all # merged result: CostCenter, ManagedBy, Environment, Application
}
15 Β· End-to-end composition (VPC β†’ SG β†’ KMS β†’ Secrets Manager β†’ RDS source β†’ S3 target β†’ DMS)
module "vpc" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-vpc?ref=v1.0.0"
  #...
}

module "dms_sg" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-security-group?ref=v1.0.0"
  vpc_id = module.vpc.id
  # egress-only rules to the RDS source and S3 (via VPC endpoint), no ingress
}

module "kms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-kms?ref=v1.0.0"
  #...
}

module "dms_secrets_role" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-iam-role?ref=v1.0.0"
  # trusts dms.amazonaws.com; allows secretsmanager:GetSecretValue on the secret below
}

module "pg_secret" {
  source      = "git::https://github.com/microsoftexpert/tf-mod-aws-secrets-manager?ref=v1.0.0"
  kms_key_arn = module.kms.arn
  # secret_string holds { username, password, host, port, dbname }
}

module "source_rds" {
  source      = "git::https://github.com/microsoftexpert/tf-mod-aws-rds?ref=v1.0.0"
  identifier  = "core-pg"
  engine      = "postgres"
  kms_key_arn = module.kms.arn
  subnet_ids  = module.vpc.private_subnet_ids
  #...
}

module "migration_bucket" {
  source      = "git::https://github.com/microsoftexpert/tf-mod-aws-s3-bucket?ref=v1.0.0"
  bucket      = "core-migration-lake"
  kms_key_arn = module.kms.arn
}

module "dms_s3_role" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-iam-role?ref=v1.0.0"
  # trusts dms.amazonaws.com; allows s3:PutObject/GetObject/ListBucket on migration_bucket.arn
}

module "dms_alerts" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-sns?ref=v1.0.0"
  # resource policy allows dms.amazonaws.com to Publish
}

module "dms" {
  source = "git::https://github.com/microsoftexpert/tf-mod-aws-dms?ref=v1.0.0"

  replication_instance_id    = "core-pg-to-lake"
  replication_instance_class = "dms.r5.large"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.dms_sg.id]
  kms_key_arn            = module.kms.arn

  endpoints = {
    source_pg = {
      endpoint_type                   = "source"
      engine_name                     = "postgres"
      server_name                     = module.source_rds.address
      port                            = module.source_rds.port
      database_name                   = "app"
      secrets_manager_arn             = module.pg_secret.arn
      secrets_manager_access_role_arn = module.dms_secrets_role.arn
      ssl_mode                        = "verify-full"
      postgres_settings = {
        plugin_name = "pglogical"
      }
    }
  }

  s3_endpoints = {
    target_lake = {
      endpoint_type                     = "target"
      bucket_name                       = module.migration_bucket.id
      service_access_role_arn           = module.dms_s3_role.arn
      data_format                       = "parquet"
      encryption_mode                   = "SSE_KMS"
      server_side_encryption_kms_key_id = module.kms.arn
      glue_catalog_generation           = true
    }
  }

  replication_configs = {
    core_pg_to_lake = {
      replication_type    = "full-load-and-cdc"
      source_endpoint_key = "source_pg"
      target_endpoint_key = "target_lake"
      table_mappings = jsonencode({
        rules = [{
          "rule-type"      = "selection", "rule-id" = "1", "rule-name" = "1"
          "rule-action"    = "include"
          "object-locator" = { "schema-name" = "app", "table-name" = "%" }
        }]
      })
      start_replication = true
      compute_config = {
        max_capacity_units = 32
        min_capacity_units = 2
        multi_az           = true
      }
    }
  }

  event_subscriptions = {
    failures = {
      sns_topic_arn    = module.dms_alerts.arn
      source_type      = "replication-task"
      event_categories = ["failure"]
    }
  }

  tags = {
    Environment = "prod"
    Application = "core-pg-to-lake"
  }
}

πŸ“₯ Inputs

Core / Identity

  • replication_instance_id (required) β€” replication instance identifier
  • replication_instance_class (required) β€” e.g. "dms.t3.medium"

Storage & Engine

  • allocated_storage (default 50), engine_version, kms_key_arn

Networking

  • subnet_ids, create_replication_subnet_group (default true), replication_subnet_group_id, replication_subnet_group_description, vpc_security_group_ids, availability_zone, multi_az, publicly_accessible (default false), network_type, dns_name_servers

Maintenance

  • preferred_maintenance_window, auto_minor_version_upgrade (default true), allow_major_version_upgrade (default false), apply_immediately (default false)

Authentication

  • kerberos_authentication_settings (classic path only)

Endpoints

  • endpoints (map β€” every engine except S3), s3_endpoints (map β€” dedicated S3 endpoint), certificates (map β€” imported PEM/Wallet)

Migration

  • replication_configs (map β€” DMS Serverless), replication_tasks (map β€” classic)

Notifications

  • event_subscriptions (map)

Governance

  • tags (default {}), timeouts (replication instance only)

🧾 Outputs

  • id, arn β€” replication instance identifier and ARN (primary outputs)
  • replication_instance_id, replication_instance_private_ips, replication_instance_public_ips
  • replication_subnet_group_id, replication_subnet_group_vpc_id (conditional β€” try(..., null), null when an existing group is reused)
  • certificates β€” map of { id, arn }
  • endpoints β€” map of { id, arn, engine_name }
  • s3_endpoints β€” map of { id, arn, engine_display_name, external_id, status }
  • replication_configs β€” map of { id, arn }
  • replication_tasks β€” map of { id, arn, status }
  • event_subscriptions β€” map of { id, arn }
  • tags_all β€” merged tags on the replication instance

No output ever emits a secret value (endpoint password, certificate content). Those live only in the resource arguments/state, per the provider's documented plaintext-in-state limitation for aws_dms_endpoint/aws_dms_certificate.


🧠 Architecture Notes

  • ARN formats: replication instance β€” arn:aws:dms:<region>:<account>:rep:<uuid>; endpoint β€” arn:aws:dms:<region>:<account>:endpoint:<uuid>; replication config β€” arn:aws:dms:<region>:<account>:replication-config:<id>; replication task β€” arn:aws:dms:<region>:<account>:task:<uuid>.
  • Immutable/force-new fields: replication_instance_class / availability_zone / replication_subnet_group_id changes can force replacement of the replication instance depending on the change β€” always review terraform plan. kms_key_arn cannot be changed in place on an existing instance. S3 endpoint ignore_header_rows is FORCE-NEW.
  • tags ↔ tags_all ↔ default_tags: identical to every other tf-mod-aws-* module β€” resource tags win over provider default_tags on key conflict; tags_all on the replication instance reflects the merged result. aws_dms_replication_subnet_group has no arn attribute (only id/vpc_id/tags_all).
  • Eventual consistency / running-replication behavior: most in-place changes to a running aws_dms_replication_config or aws_dms_replication_task first stop the replication; use start_replication/start_replication_task to resume after the change lands.
  • Destroy ordering: endpoints/certificates referenced by a running replication_config/replication_task cannot be deleted until the task/config is deleted or stopped first. Terraform's implicit dependency graph handles this automatically as long as you reference endpoints and certificates through this module's own keys (source_endpoint_key, certificate_key) rather than hard-coded external ARNs.
  • No us-east-1 global-resource requirement β€” DMS is fully regional, unlike CloudFront/WAFv2/ACM.

🧱 Design Principles

  • Replication instance encryption cannot be disabled β€” there is no storage_encrypted toggle to defeat (unlike RDS); kms_key_arn = null simply falls back to the AWS-managed aws/dms key. Supply a CMK for auditable, revocable key control on PII/privacy-regulation workloads.
  • publicly_accessible defaults to false. Opt out with publicly_accessible = true only for a documented internet-facing exception (example 10) β€” this also requires public subnets and a permissive security group.
  • auto_minor_version_upgrade defaults to true (overrides the AWS API default of false) to keep the replication engine patched. Opt out with auto_minor_version_upgrade = false to pin an exact minor version.
  • Endpoint ssl_mode defaults to "require" (overrides the AWS API default of "none"). Opt out to "none" only for engines/environments that cannot terminate TLS β€” a documented exception; prefer "verify-full" wherever the engine and CA chain support it.
  • Secrets-Manager-backed endpoint credentials are the recommended path (example 4) over inline username/password (example 5, explicit opt-in). Inline password inputs are still stored in Terraform state in plaintext β€” a documented, unavoidable provider behavior β€” so an encrypted backend and least-privilege state access are the compensating control.
  • Event subscriptions are opt-in β€” no default subscription is created; callers wire an SNS topic explicitly (example 8).
  • Both migration paths are first-class β€” replication_configs (Serverless, recommended default) and replication_tasks (classic, instance-based) coexist so a single module call can run either or both.

πŸš€ Runbook

cd tf-mod-aws-dms
terraform init -backend=false
terraform validate
terraform fmt -check

⚠️ terraform plan/terraform apply require valid AWS credentials (AWS_PROFILE / SSO / OIDC role) and a region set in the caller's provider block. Pin the module source with ?ref=v1.0.0 β€” never a branch.

terraform plan
terraform apply
terraform output

πŸ§ͺ Testing

  • terraform init -backend=false β€” provider plugin resolution only, no state backend required.
  • terraform validate β€” schema/type validation for all 8 resources, including the polymorphic aws_dms_endpoint *_settings dynamic blocks.
  • terraform fmt -check β€” canonical HCL formatting.
  • tflint (AWS ruleset) + checkov β€” run via the shared validate_all_modules.ps1 harness.
  • Live testing (plan/apply) should target a non-production AWS account with a least-privilege IAM role and a throwaway source database β€” never validate against a production PII-bearing source.

πŸ’¬ Example Output

$ terraform apply

module.dms.aws_dms_replication_subnet_group.this["this"]: Creating...
module.dms.aws_dms_replication_subnet_group.this["this"]: Creation complete after 3s
module.dms.aws_dms_certificate.this["onprem_ca"]: Creating...
module.dms.aws_dms_certificate.this["onprem_ca"]: Creation complete after 2s
module.dms.aws_dms_replication_instance.this: Creating...
module.dms.aws_dms_replication_instance.this: Still creating... [10m elapsed]
module.dms.aws_dms_replication_instance.this: Creation complete after 14m32s
module.dms.aws_dms_endpoint.this["source_pg"]: Creating...
module.dms.aws_dms_endpoint.this["source_pg"]: Creation complete after 4s
module.dms.aws_dms_s3_endpoint.this["target_lake"]: Creating...
module.dms.aws_dms_s3_endpoint.this["target_lake"]: Creation complete after 3s
module.dms.aws_dms_replication_config.this["pg_to_lake"]: Creating...
module.dms.aws_dms_replication_config.this["pg_to_lake"]: Creation complete after 6s

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

Outputs:

arn = "arn:aws:dms:us-east-1:123456789012:rep:ABCD1234EFGH5678"
id = "core-pg-to-lake"
endpoints = {
 "source_pg" = {
 "arn" = "arn:aws:dms:us-east-1:123456789012:endpoint:WXYZ9876"
 "engine_name" = "postgres"
 "id" = "source_pg"
 }
}

πŸ” Troubleshooting

  • AccessDeniedException creating the replication instance/subnet group β€” dms-vpc-role does not exist yet in the account. Create it via tf-mod-aws-iam-role first (see AWS Prerequisites); this is an ordering issue, not a Terraform bug.
  • Tag drift on every plan β€” a default_tags key collides with a key in var.tags; resource tags win, but if the value differs between the two you'll see perpetual drift. Keep colliding keys' values identical, or don't duplicate the key in both places.
  • Credential-chain failures on plan/apply β€” the AWS provider found no usable credentials (AWS_PROFILE/SSO session expired, OIDC role not assumed). This module never accepts credential variables; fix the caller's provider configuration or CI OIDC federation, not this module.
  • terraform validate passes but apply fails with AccessDenied on a specific dms:* action β€” the executing IAM role is missing an action from SCOPE.md's Required IAM permissions list. Add the specific action; do not broaden to dms:*.
  • A modified replication_config/replication_task appears to "hang" β€” most in-place changes to a running config/task stop it first; check status (via the replication_tasks/replication_configs outputs or the console) and set start_replication/start_replication_task = true to resume.
  • Destroy fails with a dependency error on an endpoint or certificate β€” a replication_config/replication_task is still referencing it. Ensure Terraform's dependency graph sees the reference (via source_endpoint_key/certificate_key, not a hard-coded ARN) so destroy ordering is automatic; otherwise destroy the config/task first.
  • aws_dms_endpoint apply fails with a settings-block error β€” a *_settings object was populated for the wrong engine_name (e.g. oracle_settings on a mysql endpoint). Terraform's type system accepts any combination; the DMS API rejects the mismatch at apply time. Only set the settings block matching engine_name.
  • CDC never starts producing changes β€” an engine-side prerequisite is missing (binary logging, logical replication slot, supplemental logging, etc. β€” see AWS Prerequisites). This module cannot detect or enforce these; verify on the source database directly.

πŸ”— Related Docs

  • AWS DMS User Guide β€” replication instance sizing and best practices
  • AWS DMS User Guide β€” Security (IAM roles required: dms-vpc-role, dms-cloudwatch-logs-role, dms-access-for-endpoint)
  • AWS DMS User Guide β€” Sources for data migration (per-engine settings and CDC prerequisites)
  • AWS DMS User Guide β€” Targets for data migration (per-engine settings)
  • AWS DMS User Guide β€” Table mapping selection and transformation rules
  • Terraform Registry β€” hashicorp/aws provider: aws_dms_replication_instance, aws_dms_endpoint, aws_dms_s3_endpoint, aws_dms_replication_config, aws_dms_replication_task, aws_dms_replication_subnet_group, aws_dms_event_subscription, aws_dms_certificate

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