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.
- 🔁 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 forextra_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_modeto"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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
flowchart LR
VPC["terraform-aws-vpc"] --> SG["terraform-aws-security-group"]
VPC --> DMS["terraform-aws-dms"]
SG --> DMS
KMS["terraform-aws-kms"] --> DMS
SECRETS["terraform-aws-secrets-manager"] --> DMS
IAMROLE["terraform-aws-iam-role"] --> DMS
S3["terraform-aws-s3-bucket"] --> DMS
SNS["terraform-aws-sns"] --> DMS
RDS["terraform-aws-rds"] -.source data.-> DMS
AURORA["terraform-aws-rds-aurora"] -.source data.-> DMS
DMS --> CWALARM["terraform-aws-cloudwatch-alarm (Phase 4)"]
DMS --> IAMPOLICY["terraform-aws-iam-policy"]
style DMS fill:#FF9900,color:#fff
flowchart TB
subgraph DMSMOD["terraform-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
| 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).
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 |
- 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— trustsdms.amazonaws.com, attachesAmazonDMSVPCManagementRole. Required foraws_dms_replication_instance/aws_dms_replication_subnet_group.dms-cloudwatch-logs-role— trustsdms.amazonaws.com, attachesAmazonDMSCloudWatchLogsRole. Required for CloudWatch logging of replication tasks.dms-access-for-endpoint— trustsdms.amazonaws.com, attachesAmazonDMSRedshiftS3Role. Required only for Redshift-target endpoints.
Create them via terraform-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.
terraform-aws-dms/
├── providers.tf
├── variables.tf
├── main.tf
├── outputs.tf
├── README.md
└── SCOPE.md
module "dms" {
source = "git::https://github.com/microsoftexpert/terraform-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"
}
}| 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 (ARN) |
terraform-aws-kms |
endpoints[*].secrets_manager_arn |
string (ARN) |
terraform-aws-secrets-manager |
endpoints[*].secrets_manager_access_role_arn |
string (ARN) |
terraform-aws-iam-role |
s3_endpoints[*].bucket_name |
string |
terraform-aws-s3-bucket |
s3_endpoints[*].service_access_role_arn |
string (ARN) |
terraform-aws-iam-role |
event_subscriptions[*].sns_topic_arn |
string (ARN) |
terraform-aws-sns |
| Output | Description | Consumed by |
|---|---|---|
id |
Replication instance id | Reference by callers needing the instance identifier |
arn |
Replication instance ARN | terraform-aws-iam-policy, terraform-aws-cloudwatch-log-group |
endpoints |
Map: key -> { id, arn, engine_name } |
In-module replication_configs/replication_tasks, terraform-aws-iam-policy |
s3_endpoints |
Map: key -> { id, arn, engine_display_name, external_id, status } |
Same as above |
replication_configs |
Map: key -> { id, arn } |
terraform-aws-cloudwatch-alarm (Phase 4) |
replication_tasks |
Map: key -> { id, arn, status } |
terraform-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 |
1 · Minimal classic (instance-based) migration
module "dms" {
source = "git::https://github.com/microsoftexpert/terraform-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/terraform-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/terraform-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/terraform-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 # terraform-aws-secrets-manager
secrets_manager_access_role_arn = module.dms_secrets_role.arn # terraform-aws-iam-role
ssl_mode = "verify-full"
}
}
}Wire module.oracle_secret from terraform-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/terraform-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"
}
}
}
⚠️ passwordis markedsensitive = trueat the root-module input level as a compensating control, but the provider stores it in Terraform state in plaintext regardless (a documented, unavoidable behavior ofaws_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/terraform-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/terraform-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/terraform-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 # terraform-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/terraform-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 # terraform-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/terraform-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/terraform-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/terraform-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/terraform-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/terraform-aws-vpc?ref=v1.0.0"
#...
}
module "dms_sg" {
source = "git::https://github.com/microsoftexpert/terraform-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/terraform-aws-kms?ref=v1.0.0"
#...
}
module "dms_secrets_role" {
source = "git::https://github.com/microsoftexpert/terraform-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/terraform-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/terraform-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/terraform-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/terraform-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/terraform-aws-sns?ref=v1.0.0"
# resource policy allows dms.amazonaws.com to Publish
}
module "dms" {
source = "git::https://github.com/microsoftexpert/terraform-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"
}
}Core / Identity
replication_instance_id(required) — replication instance identifierreplication_instance_class(required) — e.g."dms.t3.medium"
Storage & Engine
allocated_storage(default50),engine_version,kms_key_arn
Networking
subnet_ids,create_replication_subnet_group(defaulttrue),replication_subnet_group_id,replication_subnet_group_description,vpc_security_group_ids,availability_zone,multi_az,publicly_accessible(defaultfalse),network_type,dns_name_servers
Maintenance
preferred_maintenance_window,auto_minor_version_upgrade(defaulttrue),allow_major_version_upgrade(defaultfalse),apply_immediately(defaultfalse)
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)
id,arn— replication instance identifier and ARN (primary outputs)replication_instance_id,replication_instance_private_ips,replication_instance_public_ipsreplication_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.
- 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_idchanges can force replacement of the replication instance depending on the change — always reviewterraform plan.kms_key_arncannot be changed in place on an existing instance. S3 endpointignore_header_rowsis FORCE-NEW. tags↔tags_all↔default_tags: identical to every otherterraform-aws-*module — resourcetagswin over providerdefault_tagson key conflict;tags_allon the replication instance reflects the merged result.aws_dms_replication_subnet_grouphas noarnattribute (onlyid/vpc_id/tags_all).- Eventual consistency / running-replication behavior: most in-place
changes to a running
aws_dms_replication_configoraws_dms_replication_taskfirst stop the replication; usestart_replication/start_replication_taskto resume after the change lands. - Destroy ordering: endpoints/certificates referenced by a running
replication_config/replication_taskcannot 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.
- Replication instance encryption cannot be disabled — there is no
storage_encryptedtoggle to defeat (unlike RDS);kms_key_arn = nullsimply falls back to the AWS-managedaws/dmskey. Supply a CMK for auditable, revocable key control on PII/privacy-regulation workloads. publicly_accessibledefaults tofalse. Opt out withpublicly_accessible = trueonly for a documented internet-facing exception (example 10) — this also requires public subnets and a permissive security group.auto_minor_version_upgradedefaults totrue(overrides the AWS API default offalse) to keep the replication engine patched. Opt out withauto_minor_version_upgrade = falseto pin an exact minor version.- Endpoint
ssl_modedefaults 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). Inlinepasswordinputs 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) andreplication_tasks(classic, instance-based) coexist so a single module call can run either or both.
cd terraform-aws-dms
terraform init -backend=false
terraform validate
terraform fmt -check
⚠️ terraform plan/terraform applyrequire 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 outputterraform init -backend=false— provider plugin resolution only, no state backend required.terraform validate— schema/type validation for all 8 resources, including the polymorphicaws_dms_endpoint*_settingsdynamic blocks.terraform fmt -check— canonical HCL formatting.tflint(AWS ruleset) +checkov— run via the sharedvalidate_all_modules.ps1harness.- 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.
$ 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"
}
}
AccessDeniedExceptioncreating the replication instance/subnet group —dms-vpc-roledoes not exist yet in the account. Create it viaterraform-aws-iam-rolefirst (see AWS Prerequisites); this is an ordering issue, not a Terraform bug.- Tag drift on every plan — a
default_tagskey collides with a key invar.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 validatepasses butapplyfails withAccessDeniedon a specificdms:*action — the executing IAM role is missing an action from SCOPE.md's Required IAM permissions list. Add the specific action; do not broaden todms:*.- A modified
replication_config/replication_taskappears to "hang" — most in-place changes to a running config/task stop it first; checkstatus(via thereplication_tasks/replication_configsoutputs or the console) and setstart_replication/start_replication_task = trueto resume. - Destroy fails with a dependency error on an endpoint or certificate — a
replication_config/replication_taskis still referencing it. Ensure Terraform's dependency graph sees the reference (viasource_endpoint_key/certificate_key, not a hard-coded ARN) so destroy ordering is automatic; otherwise destroy the config/task first. aws_dms_endpointapply fails with a settings-block error — a*_settingsobject was populated for the wrongengine_name(e.g.oracle_settingson amysqlendpoint). Terraform's type system accepts any combination; the DMS API rejects the mismatch at apply time. Only set the settings block matchingengine_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.
- 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/awsprovider: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."