Skip to content
← Writing
Technical guideAWS CDK11 min

AWS DataSync DMS Step Functions CDK: Building a Multi-Environment Migration Pipeline That Does Not Fail Silently

Coordinating DataSync, DMS, Glue, and OpenSearch under one Step Functions control plane sounds clean until one service is not ready and the whole pipeline silently produces incomplete data. This post dissects a CDK TypeScript stack that wires all four together and shows exactly where it breaks in production.

By Rahul Ladumor

The migration ran for 45 minutes, reported success, and wrote incomplete data to production. The DMS replication instance had been stopped three days earlier to save money, and nobody restarted it. The pipeline did not know, because it never checked. That failure is the reason I now build these stacks pre-flight first. aws datasync dms step functions cdk done right is not about the orchestration being clever; it is about the orchestrator knowing whether its downstream services are actually ready before it moves a byte.


Who This Is For

awsAWS Cloud · migration pipelineRequestResponseTelemetryINGESTTRANSFORMLOAD12345Source storeon-prem / DBDataSyncfile transferAmazon S3stagingStep FunctionsorchestrateAWS GlueETL transformTargetS3 / RedshiftDMSDB migration pathCloudWatchstate + alarmsDynamoDBrun stateIdempotent, resumable migration: pre-flight checks gate every stage; re-runs skip completed work.

If you are comfortable with CDK TypeScript and have run at least one of DataSync, DMS, or Step Functions in production, this is for you. Not a "what is DMS" explainer. This assumes you know what a replication instance is, you have seen a Step Functions state machine, and your question is how to wire all of this together in one CDK stack without it falling apart at 2 AM on migration night.

You are probably building a platform-team tool for environment promotion (dev to staging to prod), or you inherited a pile of cron jobs and ad-hoc scripts that mostly work and need to replace them with something auditable. Either way, the value here is not the high-level idea (five services, one state machine). It is the operational knowledge that only accumulates through production incidents: where the naive version fails silently, and what to build so it fails loudly and early instead.


Problem Framing

Moving data between AWS environments sounds like solved territory. S3 has replication, Aurora has DMS, OpenSearch has, well, that is where it gets complicated. The real issue is not any individual service. It is the orchestration layer.

Teams say "we have a script for that." What they actually have is a cron job that kicks off a DataSync task, a separate Lambda watching for DMS replication to finish, a third thing rebuilding the OpenSearch index, and a Slack notification that may or may not fire when something goes wrong. This works on migration run #1. Run #2, someone changed a security group. Run #3, the DMS instance was stopped to save money and nobody restarted it. Run #4, Glue reprocessed the full dataset because nobody configured bookmarks, and now there are duplicate records in Aurora.

The fix is not more scripts. It is a single orchestrator with real failure semantics. Step Functions gives you that, and it also gives you a new problem: it has no idea whether DataSync, DMS, Glue, or OpenSearch are actually ready before it starts. It fires the first state and hopes. More on that in the failure section.

The pipeline here handles three migration targets - S3 buckets (bulk file data), Aurora (relational), and OpenSearch indexes - across environments, on a schedule or on demand, with failure notifications that actually reach a human.


Annotated Implementation

The core breaks into four services doing the work, one orchestrator running the show, and one pre-flight layer the naive version is missing entirely.

DataSync handles S3-to-S3. The stack provisions a DataSync task with source and destination location resources pointing at your environment-specific buckets. DataSync beats S3 Cross-Region Replication here because it gives task-level control: throttle bandwidth, filter by prefix, and get a task-level completion event Step Functions can wait on. The cost is $0.0125/GB (DataSync pricing, checked June 2026), which for a 500 GB monthly workload is $6.25 in DataSync charges on top of S3 request costs. That sounds harmless until you sync the same 500 GB five times because someone triggered migration runs manually to check if it worked, and those five runs billed real money while fixing nothing.

DMS handles Aurora migration: one replication instance (EC2-backed), source and target endpoints, a replication task. The persistent replication instance is the expensive part, roughly $70 to $150/month depending on instance class (estimate, varies by class and region), running 24/7 whether or not a migration is happening. There are ways around that with a start/stop Lambda hook the stack provisions alongside DMS. For Aurora cost trade-offs more broadly, the Aurora DMS replication cost breakdown post goes deeper.

Glue does ETL transformation in the middle: scrubbing PII before it lands in dev, transforming schema versions, filtering records. Glue jobs sit between DataSync completing and DMS starting, which introduces 2 to 10 minutes of cold-start latency per run. Fine for nightly cadences. A problem if you thought this pipeline would give you near-real-time environment sync. It will not.

Step Functions is the control plane. Here is the partial CDK snippet showing the core orchestration chain, intentionally incomplete, the pattern not the full deployable stack:

// Illustrative only - not the full stack
const datasynctaskState = new tasks.CallAwsService(this, 'StartDataSyncTask', {
 service: 'datasync',
 action: 'startTaskExecution',
 parameters: {
 TaskArn: dataSyncTask.taskArn,
 },
 iamResources: [dataSyncTask.taskArn],
}).addCatch(migrationFailureHandler, {
 errors: ['States.ALL'],
 resultPath: '$.errorInfo',
});

const dmsTaskState = new tasks.CallAwsService(this, 'StartDMSReplication', {
 service: 'databasemigration',
 action: 'startReplicationTask',
 parameters: {
 ReplicationTaskArn: replicationTask.ref,
 StartReplicationTaskType: 'start-replication',
 },
 iamResources: ['*'],
}).addCatch(migrationFailureHandler, {
 errors: ['States.ALL'],
 resultPath: '$.errorInfo',
});

// Chain: DataSync -> Glue -> DMS -> OpenSearch rebuild
const definition = datasynctaskState
 .next(glueJobState)
 .next(dmsTaskState)
 .next(openSearchRebuildState);

The addCatch on every state is the part most people skip the first time. Without it, Step Functions swallows service errors and the state machine ends in a Succeeded state while your data is in an unknown condition. For a deeper look at AWS Step Functions error handling patterns, that post covers the general patterns.

EventBridge wires the trigger layer. Two modes: scheduled (nightly, a cron expression in the CDK rule) and event-based (S3 ObjectCreated events on prefixes that indicate a source refresh). The EventBridge rule patterns for data pipeline triggers post has the rule structures if you need them.


Decision Matrix

Before you commit to this stack, the honest decision matrix.

Scenario Recommendation
S3 sync only, same region Skip DataSync, use S3 Batch Operations
< 50 GB migrations, no relational data Lambda-based sync is cheaper and simpler
Need CDC (change data capture) from Aurora DMS CDC mode - this stack supports it
Near-real-time sync required (< 5 min lag) This stack is wrong. Look at DMS CDC + direct triggers
Heterogeneous sources (S3 + Aurora + OpenSearch) This stack is the right call
Budget under $100/month total Stop DMS instance between runs
Compliance audit requirement Step Functions execution history is your audit log
Team has no CDK experience Terraform version might be easier to maintain

The two questions that decide it: do you have at least three distinct migration targets (S3, relational, search), and is your migration cadence daily or less frequent? Both yes, this stack makes sense. Either no, you are overbuilding.


Failure and Edge Cases

This is the section to read, not skim.

The pre-flight problem. The biggest gap in the naive version is that Step Functions starts the state machine with no idea whether its downstream services are ready. DMS replication instance stopped? The state machine starts, calls StartReplicationTask, gets an InvalidResourceStateFault, hits the Catch block, routes to the failure handler. Best case you get paged. Worst case, if the Catch is not wired correctly, it silently marks the execution failed and nothing moves.

The fix, and this is the most important architectural decision in the whole stack, is a pre-flight Lambda as the first state. Before a single byte moves, it checks: is the DMS replication instance available? Is the DataSync task not already running? Is the Glue cluster warm, or the cold-start time within SLA? Is OpenSearch cluster health green? If any fail, the state machine fails fast, before the 45-minute timer starts, before partial data lands anywhere. You trade 30 seconds of pre-flight for not having to manually clean up a 45-minute partial migration.

DataSync partial failures. DataSync task-level status can be SUCCESS while individual file transfers failed on specific S3 prefixes due to permission issues. Step Functions sees the task succeed. Your data is incomplete. The only way to catch this is parsing the DataSync task execution report, which is written to an S3 bucket you have to configure explicitly. Add a Lambda that reads that report as a verification state after DataSync completes.

Glue bookmark misconfiguration. Without Glue job bookmarks, every run reprocesses the full dataset. For a 500 GB workload with DMS running concurrently, that means duplicate records in Aurora. Bookmarks are off by default, and turning them on requires understanding your job's input data structure. This is pipeline-specific, not a one-liner.

VPC networking breaks silently. DMS replication instances and DataSync agents live inside your VPC. Any route table change, security group tightening, or NAT Gateway swap can break connectivity without triggering an alarm until the state machine actually tries to run. There is no built-in pre-check, which is another argument for the pre-flight Lambda: a connectivity probe from inside the VPC before execution starts. See VPC networking for DMS replication instances for the patterns that keep this from happening.

The OpenSearch full reindex problem. Every run triggers a full reindex, not incremental, which is fine for typical environment-promotion cadences. But if the source OpenSearch cluster is unavailable mid-migration - a rolling restart, a bad deployment - the reindex hangs, Step Functions waits, and the whole pipeline stalls. There is no partial-write protection on the target index.


Operational Guidance

A few things I would tell the on-call engineer inheriting this stack.

First, your audit trail is the Step Functions execution history. Every state transition, every input and output, every Catch trigger is logged. Before you look anywhere else during an incident, pull the execution ARN and read the event history in order. It tells you exactly which state failed and what the error was.

Second, the DMS replication instance is your most expensive always-on resource, and it is where I have seen teams throw the most money away. If migrations run nightly, there is no reason it runs 24/7. The start/stop Lambda hooks can take DMS from about $150/month to roughly $15 to $20/month if migrations complete in 2 to 3 hours nightly. That is around $130/month back per instance (the $150 you were paying minus the ~$20 you still pay), not a premature optimization.

Third, wire SQS DLQ processing before you go live. SNS failure fan-out to SQS is only useful if something reads the DLQ. An unprocessed DLQ is a quiet graveyard for failure events. Add a Lambda-based DLQ processor that escalates to PagerDuty after N messages accumulate without resolution.

Fourth, test your pre-flight Lambda against every failure mode before the first production migration. Stop the DMS instance, run the state machine, confirm it fails in state 1, not state 3. Revoke an IAM permission, run again, confirm the pre-flight catches it. Most teams skip this because "it works in staging," and staging has a permanently running DMS instance.


Closing Recommendation

Where I land after working through all of it: the architecture is sound. Step Functions as a unified control plane across DataSync, DMS, Glue, and OpenSearch is the right call for heterogeneous migration workloads - one execution history, one place to check audit state, one failure model. For CDK TypeScript multi-service pipeline patterns more broadly, the CDK TypeScript stack patterns for multi-service pipelines post has context.

But the naive version, Step Functions coordinating four services with no pre-flight, no DataSync report validation, no Glue bookmark config, is just a more elegant way to fail silently. The gap is not in the orchestration design. It is in the assumption that orchestration awareness equals service readiness. It does not.

Build the pre-flight Lambda first. Before you wire DataSync, before you configure DMS, write the Lambda that checks whether every downstream service is in a runnable state, and make it the first state in every environment's machine. It is what stands between you and the migration that runs for 45 minutes and writes incomplete data because someone stopped the DMS instance three days ago and forgot to restart it.

Next step

Have the same problem on your stack?

Send the architecture, AWS bill concern, deploy pain, or GenAI reliability issue. I'll find the first real bottleneck and propose a small, reversible fix.

Related reading

#AWS CDK#DataSync#DMS#Step Functions#Glue#Migration
Rahul Ladumor

Rahul Ladumor

Principal Cloud & AI Platform Architect. AWS Professional certified, 4x AWS Community Builder. I work with teams that have real users, real AWS bills, and real production pressure.

About Rahul →