If your team runs an application on AWS with a standby disaster recovery (DR) region, you can set up CodePipeline so every release deploys to both regions at once. The setup here uses ca-central-1 as the primary region and us-east-1 as a warm standby, meaning the second region already has its infrastructure running and is ready to take traffic. The common problem is that the standby only gets updated by hand, so it slowly falls behind the primary. When the primary region has an outage and traffic fails over, the standby serves an older version of the app, and its database schema may no longer match. The failover then turns into a second incident. The fix is to deploy the same build artifact (the packaged output of your build) to both regions on every release and check both afterwards. CodePipeline supports this with cross-region actions, which copy the artifact into each region. AWS Key Management Service (KMS) multi-region keys let both regions decrypt that artifact with the same key.
What’s happening
The cross-region pipeline pattern has three pieces that must align:
- Artifact replication. CodePipeline can replicate artifacts from the pipeline’s home region to additional regions, but only if you declare
ArtifactStores(plural) instead ofArtifactStore(singular) at the pipeline level. Each region gets its own S3 bucket and its own KMS key. - Cross-region action. A deploy action can specify
Regionin its action declaration. CodePipeline picks the artifact from the matching regional bucket, runs the action against the CodeDeploy application in that region, and reports status back to the home region’s pipeline run. - Same artifact, two regions. The artifact is built once in the home region and replicated. There is no second build. This is what makes “deployed to DR” a reliable claim — it is literally the same bytes that staging validated.
The order of operations on a failover then becomes:
- Route 53 health check on the primary fails → traffic shifts to DR.
- DR is already running the current release (because every release deployed to it). No emergency redeploy needed.
- The DR-region database is the read replica or active-active partner of primary — promote it.
- Communicate the incident, monitor recovery, plan the failback.
Without the every-release-deploys-to-DR practice, step 2 becomes “page someone to run a deploy” and the RTO doubles. With it, the failover is mechanical.
How to verify
The cross-region pipeline definition, abbreviated to the parts that matter:
AppPipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: app-pipeline
RoleArn: !GetAtt PipelineRole.Arn
ArtifactStores:
- Region: ca-central-1
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucketCanada
EncryptionKey:
Id: !GetAtt ArtifactKeyCanada.Arn
Type: KMS
- Region: us-east-1
ArtifactStore:
Type: S3
Location: !Ref ArtifactBucketUsEast
EncryptionKey:
Id: !GetAtt ArtifactKeyUsEast.Arn
Type: KMS
Stages:
# ... Source, Build, Test stages run in home region (ca-central-1)
- Name: DeployPrimary
Actions:
- Name: DeployPrimary
Region: ca-central-1
ActionTypeId: { Category: Deploy, Owner: AWS, Provider: CodeDeploy, Version: '1' }
Configuration:
ApplicationName: !Ref ProdAppCanada
DeploymentGroupName: !Ref ProdGroupCanada
InputArtifacts: [{ Name: BuildOutput }]
- Name: ValidatePrimary
Actions:
- Name: SyntheticsPrimary
Region: ca-central-1
ActionTypeId: { Category: Test, Owner: AWS, Provider: CodeBuild, Version: '1' }
Configuration: { ProjectName: !Ref PrimarySyntheticsProject }
InputArtifacts: [{ Name: BuildOutput }]
- Name: DeployStandby
Actions:
- Name: DeployStandby
Region: us-east-1
ActionTypeId: { Category: Deploy, Owner: AWS, Provider: CodeDeploy, Version: '1' }
Configuration:
ApplicationName: !Ref ProdAppUsEast
DeploymentGroupName: !Ref ProdGroupUsEast
InputArtifacts: [{ Name: BuildOutput }]
- Name: ValidateStandby
Actions:
- Name: SyntheticsStandby
Region: us-east-1
ActionTypeId: { Category: Test, Owner: AWS, Provider: CodeBuild, Version: '1' }
Configuration: { ProjectName: !Ref StandbySyntheticsProject }
InputArtifacts: [{ Name: BuildOutput }]
The ArtifactStores plural is mandatory the moment you have a cross-region action. Each entry must be a region in which the pipeline has at least one action. The S3 bucket in each region must be in the same region as the entry — CodePipeline will not cross-region-copy out of an us-east-1 bucket to a ca-central-1 action.
The KMS keys deserve careful thought. The simplest pattern is one regional CMK per region, with the pipeline’s service role granted kms:Decrypt on both. The more elegant pattern is a multi-region KMS key — a single logical key with replicas in both regions, so the artifact encrypted in ca-central-1 decrypts in us-east-1 without re-encryption. Create the multi-region primary:
aws kms create-key \
--region ca-central-1 \
--multi-region \
--description "CodePipeline artifact key (multi-region primary)"
Then replicate to us-east-1:
aws kms replicate-key \
--key-id mrk-abc123 \
--replica-region us-east-1
Both replicas share the same key material and the same key ID prefix (mrk-), so the artifact’s KMS metadata travels unchanged.
Verify the cross-region action is actually using the right regional bucket:
aws codepipeline get-pipeline-state \
--name app-pipeline \
--query 'stageStates[?stageName==`DeployStandby`].actionStates[0].latestExecution'
The execution should show summary referencing the us-east-1 deployment ID. If it references ca-central-1, the Region: field on the action is missing or wrong.
The fix
The cross-region pipeline practice has six rules that hold across every multi-region engagement:
Every release deploys to every region. The whole reason cross-region works is that DR is never stale. A pipeline that conditionally deploys to DR (“only on weekends” or “only on tagged releases”) guarantees the DR region drifts. The drift is the incident.
Validate the standby region the same way you validate primary. The ValidateStandby stage runs synthetics against us-east-1 endpoints. If the standby synthetics fail, the pipeline fails — even though primary already succeeded. This is the gate that catches “the deploy looked fine but DR is broken.” Without it, you discover the breakage during a failover, which is the worst possible time.
Use multi-region KMS keys, not per-region keys. Two separate keys work, but multi-region keys eliminate the re-encrypt step and make the IAM policies symmetric. The cost is one extra aws kms replicate-key call at setup.
The CodeDeploy applications in each region must be independent. A single CodeDeploy application cannot span regions. You need ProdAppCanada and ProdAppUsEast as separate resources, with separate deployment groups, separate Auto Scaling groups, separate target groups. The pipeline glues them together, not the deploy service.
Cross-region IAM role assumption. The pipeline’s service role in the home region must trust the deploy roles in both regions. If you use cross-account deploy (DR account separate from primary), this gets more elaborate — the home pipeline role assumes a DR-account role, which has the CodeDeploy permissions in us-east-1. Get this trust block right at setup; debugging it after the pipeline has already executed once is significantly harder.
Failover is a separate runbook. The pipeline keeps DR fresh; it does not perform failover. Failover is Route 53 health-check flips plus database promotion plus monitoring alerts. Both pieces have to exist — a pipeline without a failover runbook leaves DR ready but unused, and a failover runbook without a pipeline practices the failover against stale code.
The cross-region pipeline is the engineering work that makes the “we have DR” claim true. Every other piece — the standby infrastructure, the runbooks, the failover test — depends on it.
Stack Harbor builds and operates production CodePipeline workflows as part of Managed AWS onboarding and the ongoing Code Deploy Pipeline operating layer. If your release path is still SSH + a Slack message, start with an infrastructure consult.