> ## Documentation Index
> Fetch the complete documentation index at: https://astronomer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Per-deployment migration reference

This page is the reference companion to [Configure per-deployment migration](/docs/astro-private-cloud/v-2-x/configure-per-deployment-migration) and [Manage and observe per-deployment migration](/docs/astro-private-cloud/v-2-x/manage-per-deployment-migration). It documents the configuration settings, cluster failover states, mission and flight states, migration errors and skips, the GraphQL surface, and the permissions for per-deployment migration on Astro Private Cloud (APC) 2.1 and later.

## Configuration settings

There is no per-deployment migration feature flag, no separate mutation to enable, and no per-cluster opt-in beyond `failoverEnabled`. The settings that govern it are the data plane failover settings plus the new region model.

| Setting                                              | Default                                                                                     | Set to                                                     | Effect                                                                                                                                                                                                                                                                                                                                                |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `global.dataPlaneFailover.enabled`                   | `false`                                                                                     | `true` on every participating control plane and data plane | Master switch for data plane failover and per-deployment migration. Requires split mode — not supported in unified mode. On a data plane, enables the data plane execution components (Pilot and Flightdeck) and the data plane internal API's `StartFlight` remote procedure call. On a control plane, enables Navigator and the APC API dispatcher. |
| `global.dataPlaneFailover.externalSecretManagerName` | Unset                                                                                       | The name of your `ClusterSecretStore`                      | Tells the data plane internal API which External Secrets Operator (ESO) `ClusterSecretStore` to use when pushing and pulling Deployment secrets. Required on data planes.                                                                                                                                                                             |
| `external-secrets.enabled`                           | `false`                                                                                     | `true` on every data plane                                 | Installs the ESO custom resource definitions that data plane failover depends on.                                                                                                                                                                                                                                                                     |
| `Region.name`                                        | —                                                                                           | Unique, human-meaningful                                   | Region identity. Compared by ID, not by name — the name is for you. Must be unique.                                                                                                                                                                                                                                                                   |
| `Region.cloudProvider`                               | —                                                                                           | `aws`, `gcp`, `azure`, or `local`                          | Every region has exactly one cloud provider. A cluster can only be assigned to a region whose provider matches the cluster's own, so region equality implies provider equality.                                                                                                                                                                       |
| `Cluster.regionId`                                   | Backfilled to `unset`                                                                       | The region the cluster physically lives in                 | The value the migration gate compares. Not enforced non-null, but migration is impossible while it points at `unset`.                                                                                                                                                                                                                                 |
| `Cluster.cloudProvider`                              | Inherited from the assigned region at registration; from data plane metadata when no region | Editable on the cluster                                    | Editable on the cluster edit form, and no longer overwritten by the data plane metadata reconcile. When you assign or change a region on an existing cluster, it is checked case-insensitively against `region.cloudProvider`, and skipped if the cluster's provider is empty.                                                                        |

## Cluster failover states

The cluster detail page shows a **Failover** field, which replaces the older binary "Failover Enabled." Reading it is how you tell whether a cluster can participate in migration:

| Status          | Meaning                                                                                                                                                       | Can it be a migration destination? |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| Not Capable     | The data plane doesn't have `dataPlaneFailover.enabled` set.                                                                                                  | No                                 |
| Pending Upgrade | Failover-capable, but Deployments still need the one-time failover upgrade, and no upgrade missions are running.                                              | No                                 |
| Upgrading (N/M) | At least one upgrade mission is in flight. N of M Deployments upgraded.                                                                                       | No                                 |
| Enabled         | All eligible Deployments are upgraded and the cluster passes the eligibility check.                                                                           | Yes                                |
| Issues Detected | The cluster has upgraded Deployments that are individually ineligible, for example a Deployment type change or ESO config drift. Hover for the affected list. | No                                 |

The underlying API fields are `Cluster.failoverEnabled` (the control-plane-effective state) and `Cluster.failoverUpgradeStatus` (`not_started`, `in_progress`, `complete`, or `null`).

<Note>
  `Cluster.failoverEnabled` starts from what the data plane reports, but the control plane force-downgrades it to `false` whenever an eligibility check fails. It reflects reality, not intent. If you enabled the Helm value and the field is still `false`, something in the eligibility check is failing, and the **Failover** status tells you which.
</Note>

## Failover-readiness shield states

The Deployments list shows a failover-readiness shield next to each Deployment name, on failover-capable clusters only. It uses one outline icon, distinguished by color, plus an exclamation badge for **Attention**. The API field behind it is `Deployment.failoverReadiness`.

| State           | Color     | `failoverReadiness` value | Meaning                                                               |
| --------------- | --------- | ------------------------- | --------------------------------------------------------------------- |
| Not applicable  | No shield | `not_capable`             | Failover-incapable cluster.                                           |
| Pending upgrade | Grey      | `pending_upgrade`         | Failover-capable but not yet upgraded for failover.                   |
| In progress     | Amber     | `in_progress`             | An upgrade mission is in flight.                                      |
| Ready           | Green     | `ready`                   | Upgraded and currently failover-eligible.                             |
| Attention       | Red       | `attention`               | The upgrade failed, or the Deployment is upgraded but now ineligible. |

<Warning>
  The shield tracks the one-time failover-upgrade lifecycle, not migrations. A Deployment that is mid-migration keeps a green **Ready** shield the entire time. Use the **Mission Status** column, or `Deployment.activeMission`, to see an in-flight migration.
</Warning>

## Mission states

Selecting N Deployments creates N independent missions, one per Deployment. Each moves through the following states:

| State             | Meaning                                                                                                                                                           |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CREATED`         | Deployment claimed and the mission row exists. Flights may not exist yet.                                                                                         |
| `PLANNING`        | Navigator has claimed the mission for planning.                                                                                                                   |
| `PLANNED`         | The required control plane flights are created and initialized.                                                                                                   |
| `IN_PROGRESS`     | At least one flight has been dispatched to a data plane.                                                                                                          |
| `CLEANUP_PENDING` | The Hyperjump flight succeeded, but source-side cleanup is still pending or failed. A per-deployment migration never reaches this state — see the following note. |
| `COMPLETED`       | Fully complete: cutover done and cleanup done.                                                                                                                    |
| `FAILED`          | The mission failed. See [Recover a failed migration](/docs/astro-private-cloud/v-2-x/manage-per-deployment-migration#recover-a-failed-migration).                      |

<Note>
  `CLEANUP_PENDING` doesn't occur in per-deployment migration. Migrations always run in controlled mode, so the Scavenger flight (source cleanup) must succeed before the Hyperjump flight is dispatched. Once the Hyperjump succeeds, the source is already cleaned and the mission goes straight to `COMPLETED`. If the Scavenger fails, the Hyperjump never runs and the mission is `FAILED`. This state arises only in a forced cluster failover, where the Hyperjump can cut over while source-side cleanup is still deferred or has failed — and even there it isn't an error.
</Note>

## Flight states

Each flight within a mission has its own state:

| State                | Meaning                                                                                                    |
| -------------------- | ---------------------------------------------------------------------------------------------------------- |
| `READY_TO_DISPATCH`  | Queued, waiting for the dispatcher.                                                                        |
| `HOLDING`            | Held — a dependency isn't satisfied yet, for example the Hyperjump flight waiting on the Scavenger flight. |
| `DISPATCHING`        | The dispatcher holds a claim and is attempting `StartFlight`.                                              |
| `DISPATCHED`         | The data plane internal API acknowledged, and the data plane has durably recorded the flight.              |
| `AWAITING_DP_HEALTH` | The data plane is unhealthy or unreachable. Dispatch is paused and resumes automatically when it recovers. |
| `FAILED_DISPATCH`    | Dispatch failed after the configured retries.                                                              |
| `SUCCEEDED`          | The flight completed successfully.                                                                         |
| `FAILED`             | The flight failed.                                                                                         |
| `CANCELED`           | The flight was canceled.                                                                                   |

`AWAITING_DP_HEALTH` is a wait, not a failure: the circuit breaker parked the flight because the data plane was unreachable. Fix the data plane and it resumes on its own.

## Migration errors and skips

`migrateDeployments` has two failure modes. Whole-mutation errors are about the destination and reject the entire call, so nothing moves. Per-deployment skips pass over one Deployment and let the rest proceed.

### Whole-mutation errors

These are evaluated in order, and the first match wins. Because cordon and in-flight-failover are checked before authorization, an unauthorized caller may see a health or cordon error first.

| Error                              | Cause                                                                          | Fix                                                                                                                                            |
| ---------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `DESTINATION_NOT_FOUND`            | The destination cluster ID doesn't exist, or the cluster is deleted.           | Check the ID.                                                                                                                                  |
| `DESTINATION_NOT_FAILOVER_ENABLED` | The destination's `failoverEnabled` is `false`.                                | Check the **Failover** field on the cluster detail page and resolve what is holding it back, usually Deployments pending the failover upgrade. |
| `DESTINATION_CLUSTER_UNHEALTHY`    | The destination's data plane health isn't healthy.                             | Restore data plane health, then retry.                                                                                                         |
| `DESTINATION_CLUSTER_CORDONED`     | The destination cluster is in maintenance mode.                                | Uncordon it, or pick another destination.                                                                                                      |
| `DESTINATION_FAILOVER_IN_PROGRESS` | A cluster failover is already targeting the destination.                       | Wait for it to finish and clean up.                                                                                                            |
| `DESTINATION_NOT_AUTHORIZED`       | You aren't a System Admin and lack `cluster.config.update` on the destination. | See [Permissions](#permissions).                                                                                                               |

### Per-deployment skips

When a Deployment is skipped, the rest of the batch still migrates. Skips are safe to retry: re-issuing `migrateDeployments` with just the skipped IDs is a normal workflow, because a skipped Deployment was never claimed and nothing is left in a partial state.

| Reason                               | Cause                                                                                    | Fix                                                                                                                                                                                                                                          |
| ------------------------------------ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEPLOYMENT_NOT_FOUND`               | The Deployment ID doesn't exist or the Deployment is deleted.                            | Check the ID.                                                                                                                                                                                                                                |
| `MISSING_CLUSTER_ID`                 | The Deployment's source cluster is missing or deleted.                                   | Data integrity issue — contact [Astronomer support](https://support.astronomer.io).                                                                                                                                                          |
| `SOURCE_CLUSTER_NOT_AUTHORIZED`      | You lack `cluster.config.update` on this Deployment's source cluster.                    | Migration is a cluster-admin operation on both clusters. See [Permissions](#permissions).                                                                                                                                                    |
| `CLUSTER_REGION_NOT_SET`             | The source or destination cluster is still on the `unset` sentinel region.               | Assign a real region. See [Configure per-deployment migration](/docs/astro-private-cloud/v-2-x/configure-per-deployment-migration#step-3-assign-a-region-to-every-failover-enabled-cluster). This is the most common skip after upgrading to 2.1. |
| `CROSS_REGION_MIGRATION_NOT_ALLOWED` | The source and destination are in different regions.                                     | You can't override this. Pick an in-region destination, or use cluster failover.                                                                                                                                                             |
| `SAME_CLUSTER`                       | The destination is the Deployment's current cluster.                                     | Nothing to do — it's already there.                                                                                                                                                                                                          |
| `CURRENT_MISSION_CONFLICT`           | The Deployment already has an active mission.                                            | Wait for it to reach a terminal state, then retry. The conflicting mission ID isn't returned in the skip payload — query `Deployment.activeMission` to find it.                                                                              |
| `CLUSTER_FAILOVER_IN_PROGRESS`       | A cluster failover is active on the Deployment's source cluster.                         | Wait for the failover to finish. Cluster failover and Deployment-scoped missions are mutually exclusive per Deployment.                                                                                                                      |
| `DEPLOYMENT_CORDONED`                | The Deployment is cordoned.                                                              | Uncordon it first.                                                                                                                                                                                                                           |
| `CLUSTER_CORDONED`                   | The Deployment's source cluster is in maintenance mode.                                  | Uncordon the cluster first.                                                                                                                                                                                                                  |
| `NOT_FAILOVER_READY`                 | The Deployment predates the failover prerequisites and hasn't been retrofitted.          | Run the failover upgrade on it, then retry.                                                                                                                                                                                                  |
| `UNSUPPORTED_DEPLOY_TYPE`            | The Deployment's latest deploy revision isn't `image`, for example git-sync or Dag-only. | This can't be migrated. Convert to image-based deploys.                                                                                                                                                                                      |
| `INTERNAL_ERROR` / `DB_ERROR`        | Transient failure during the claim transaction.                                          | Retry. If it persists, check the control plane logs.                                                                                                                                                                                         |

### Cluster failover skipping a migrating Deployment

The exclusion runs both ways. If a cluster failover starts while one of your migration missions is in flight, the failover silently skips that Deployment rather than aborting, and records it in `FailoverRequest.skippedDeployments`. Each entry carries the Deployment ID, release name, mission ID, and reason. The skipped list is surfaced on the cluster detail page at reconcile finalization. Those Deployments weren't failed over — once the migration mission completes, retry them explicitly.

## GraphQL surface

```graphql theme={null}
# Trigger
migrateDeployments(deploymentIds: [Id!]!, destinationClusterId: Id!): MigrateDeploymentsPayload!

type MigrateDeploymentsPayload {
  missions: [MigrationResult]!    # successfully claimed
  skipped:  [SkippedDeployment]!  # failed a per-deployment check
}
type MigrationResult   { deploymentId: Id, missionId: Id }
type SkippedDeployment { deploymentId: Id, reason: MigrationSkipReason }

# Observe
missionProgress(missionIds: [Id!]!): [MissionProgress]

type MissionProgress {
  id: Id
  kind: MissionKind                 # MOVE for migration and cluster failover; UPGRADE for retrofit
  state: MissionState
  originClusterId: Id
  destinationClusterId: Id          # null for upgrade missions
  failoverRequestId: Id             # null for migration and upgrade missions
  flights: [MissionFlightProgress!]
}
type MissionFlightProgress { id: Id, role: FlightRole, state: CpFlightState }

# Candidate destinations
failoverTargetClusters(sourceClusterId: Uuid!): [Cluster]

# Regions
regions: [Region]!
createRegion(name: String!, cloudProvider: String!): CreateRegionPayload!
updateRegion(regionId: Id!, name: String, cloudProvider: String): UpdateRegionPayload!
deleteRegion(regionId: Id!): DeleteRegionPayload!
```

## Relevant fields

| Field                                | Type              | Meaning                                                                                      |
| ------------------------------------ | ----------------- | -------------------------------------------------------------------------------------------- |
| `Deployment.isBeingFailedOver`       | `Boolean!`        | True while any mission is active. Clients should disable destructive actions.                |
| `Deployment.activeMission`           | `MissionProgress` | The current mission with its flights, or `null`.                                             |
| `Deployment.failoverReadiness`       | `String`          | Shield state: `not_capable`, `pending_upgrade`, `in_progress`, `ready`, or `attention`.      |
| `Deployment.isCordoned`              | `Boolean!`        | Cordoned Deployments are skipped by migration.                                               |
| `Deployment.secretsSynced`           | `Boolean`         | Whether the ESO push-secret sync succeeded. A failure surfaces as an **Attention** shield.   |
| `Cluster.failoverEnabled`            | `Boolean`         | Control-plane-effective: can this cluster be a migration destination?                        |
| `Cluster.failoverUpgradeStatus`      | `String`          | `not_started`, `in_progress`, or `complete`; `null` when the cluster isn't failover-capable. |
| `Cluster.isCordoned`                 | `Boolean`         | Cordoned clusters are rejected as destinations and block their Deployments as sources.       |
| `FailoverRequest.skippedDeployments` | `JSON`            | Deployments a cluster failover skipped because they were already in a mission.               |

<Note>
  The data plane health signal the migration gate reads is an internal control plane column and isn't exposed on the `Cluster` GraphQL type. To confirm it from the API, use `failoverTargetClusters`, which already filters on it.
</Note>

## Enumerations

```text theme={null}
MissionKind    MOVE | UPGRADE
MissionState   CREATED | PLANNING | PLANNED | IN_PROGRESS | CLEANUP_PENDING | COMPLETED | FAILED
FlightRole     HYPERJUMP | SCAVENGER | RETROFIT
CpFlightState  READY_TO_DISPATCH | AWAITING_DP_HEALTH | FAILED_DISPATCH | HOLDING |
               DISPATCHING | DISPATCHED | SUCCEEDED | FAILED | CANCELED
FailoverMode   CONTROLLED | FORCED        (migration is always CONTROLLED)

MigrationSkipReason
  DEPLOYMENT_NOT_FOUND | MISSING_CLUSTER_ID | SOURCE_CLUSTER_NOT_AUTHORIZED |
  CLUSTER_REGION_NOT_SET | CROSS_REGION_MIGRATION_NOT_ALLOWED | SAME_CLUSTER |
  CLUSTER_FAILOVER_IN_PROGRESS | CLUSTER_CORDONED | DEPLOYMENT_CORDONED |
  CURRENT_MISSION_CONFLICT | NOT_FAILOVER_READY | UNSUPPORTED_DEPLOY_TYPE |
  INTERNAL_ERROR | DB_ERROR
```

## Permissions

| Operation                                        | Required permission                                                                     |
| ------------------------------------------------ | --------------------------------------------------------------------------------------- |
| `regions` (list)                                 | `cluster.config.get` or `system.clusters.get`                                           |
| `createRegion` / `updateRegion` / `deleteRegion` | `cluster.config.update` or `system.clusters.update`                                     |
| `updateCluster` (assign region)                  | A cluster-admin role on that cluster, or System Admin                                   |
| `failoverTargetClusters`                         | `cluster.config.get` or `system.clusters.get`                                           |
| `migrateDeployments`                             | System Admin, or `cluster.config.update` on both the source and the destination cluster |
| `missionProgress`                                | `system.clusters.get`, or a cluster-admin role on the cluster                           |

<Warning>
  Migration is a cluster-admin operation. To migrate Deployments you must be a System Admin, or hold `cluster.config.update` on both the source and the destination cluster. Deployment-level permissions alone aren't enough. If you lack a cluster-admin role on the destination, the whole call is rejected with `DESTINATION_NOT_AUTHORIZED`. If you have it on the destination but not on a given Deployment's source cluster, that Deployment is skipped with `SOURCE_CLUSTER_NOT_AUTHORIZED` while the rest proceed.
</Warning>

## Troubleshooting index

| Symptom                                                    | Look at                                                                                                                                                                              |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Every Deployment was skipped with `CLUSTER_REGION_NOT_SET` | [Configure per-deployment migration](/docs/astro-private-cloud/v-2-x/configure-per-deployment-migration#step-3-assign-a-region-to-every-failover-enabled-cluster) — assign a real region. |
| The destination cluster isn't in the picker                | `Cluster.failoverEnabled` and the **Failover** field on cluster detail. You can't query data plane health directly — use `failoverTargetClusters`, which filters on it.              |
| The picker offered a cluster that then failed              | `failoverTargetClusters` doesn't filter by region or cordon. Confirm both manually.                                                                                                  |
| You can't deploy code to a Deployment                      | It probably has an active mission — check `Deployment.activeMission`.                                                                                                                |
| The shield is green but the Deployment is migrating        | Expected. The shield tracks the upgrade lifecycle; use the **Mission Status** column.                                                                                                |
| A mission is stuck in `CLEANUP_PENDING`                    | Not stuck — cutover succeeded, only cleanup remains. This applies to cluster failover only; a per-deployment migration never enters this state.                                      |
| A flight is `AWAITING_DP_HEALTH`                           | A wait, not a failure. Restore data plane health and it resumes automatically.                                                                                                       |
| A mission `FAILED` and the Deployment is gone              | See [Recover a failed migration](/docs/astro-private-cloud/v-2-x/manage-per-deployment-migration#recover-a-failed-migration). Retry forward — never hand-restore the source.              |
| A cluster failover didn't move one of your Deployments     | Check `FailoverRequest.skippedDeployments` on the cluster detail page.                                                                                                               |

## Related documentation

* [Configure per-deployment migration](/docs/astro-private-cloud/v-2-x/configure-per-deployment-migration)
* [Manage and observe per-deployment migration](/docs/astro-private-cloud/v-2-x/manage-per-deployment-migration)
* [Data plane failover](/docs/astro-private-cloud/v-2-x/data-plane-failover)
* [Trigger a data plane failover](/docs/astro-private-cloud/v-2-x/trigger-data-plane-failover)
