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["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
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
| 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 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.
tf-mod-aws-dms/
βββ providers.tf
βββ variables.tf
βββ main.tf
βββ outputs.tf
βββ README.md
βββ SCOPE.md
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"
}
}| 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 |
| 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 |
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"
}
}
}
β οΈ 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/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"
}
}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 othertf-mod-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 tf-mod-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 viatf-mod-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."