> ## 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.

# Migrate from unified mode to a split control plane and data plane

Astro Private Cloud (APC) supports three provisioning modes, set by `global.plane.mode` in the APC Helm chart:

* **unified** — the control plane and data plane run in the same cluster and namespace. This is the legacy default for existing APC installations.
* **control** — control plane components only, managing one or more remote data planes.
* **data** — data plane components only, registered with and managed by a remote control plane.

A split control plane and data plane (CP/DP) is a prerequisite for capabilities such as [data plane failover](/docs/astro-private-cloud/v-2-x/data-plane-failover), multi-cluster management, and dedicated Airflow clusters. This document describes how to migrate an existing unified-mode installation to split mode within the same cluster, with no Airflow downtime and no data loss.

The migration is a two-phase process:

1. **Control plane migration** — Put the cluster into maintenance mode, then run a Helm upgrade that switches `global.plane.mode` from `unified` to `control`. The existing cluster becomes a standalone control plane. Airflow Deployments keep running throughout, but the platform accepts no updates from the API or UI during the transition.
2. **Data plane provisioning and re-association** — Provision a new data plane in the same cluster, verify its endpoints, and bind the new data plane back to the control plane.

Deployment domains don't change during the migration. The split data plane reuses the existing `global.baseDomain` and ingress controller, so Airflow Deployment URLs stay identical to the prior unified installation.

## Migration steps at a glance

1. Upgrade the cluster to APC 2.1, the minimum version eligible for the CP/DP split.
2. Enable maintenance mode on the cluster to block Deployment provisioning calls during the migration.
3. Upgrade the platform from unified to control mode.
4. Provision a new data plane in the same namespace.
5. Re-bind the control plane to the data plane's internal gRPC and metadata endpoints.
6. Disable maintenance mode and resume Airflow Deployment operations.

## Prerequisites

* An APC platform running in unified mode, upgraded to APC 2.1 or later. APC 2.1 is the minimum version eligible for the CP/DP split.
* The Cluster Admin or System Admin role. Only these roles can put a cluster into maintenance mode.
* Access to the platform Helm release and `kubectl` access to the cluster.
* A backup of the platform metadata database and your current Helm values, taken before you begin.
* A record of your existing unified-mode Helm values. You reuse most of them and change only the keys shown in this document.

<Note>
  If your unified installation uses in-cluster Elasticsearch for logging, you must enable shared Elasticsearch during the migration (shown in Phase 1) so that Airflow task and component logs survive the mode change.
</Note>

## Phase 1: Migrate the control plane

```mermaid theme={null}
sequenceDiagram
    actor User as UI / API
    participant API as APC API
    participant Cluster as Unified cluster
    participant AF as Airflow deployments

    Note over User,AF: 1. Enable maintenance mode
    User->>API: cordonCluster mutation
    API->>Cluster: set maintenance mode on
    Cluster-->>API: cordoned
    User--xCluster: deployment mutations blocked

    Note over User,AF: 2. Helm upgrade — unified to control
    User->>Cluster: helm upgrade (plane.mode: control)
    Cluster->>Cluster: data plane components removed
    Cluster-->>API: control plane ready

    Note over User,AF: 3. Airflow workloads keep running
    AF->>AF: Dag runs, triggerer, and schedulers continue
```

<Steps>
  <Step title="Enable maintenance mode">
    Put the cluster into maintenance mode so that Deployment mutations are blocked during the cutover. This also cordons existing Airflow Deployments so that the Helm upgrade doesn't re-roll them.

    ```graphql theme={null}
    mutation {
      cordonCluster(clusterId: "<cluster-id>", reason: "maintenance") {
        id
        isCordoned
        cordonedAt
      }
    }
    ```

    While maintenance mode is active, the platform blocks REST API calls, GraphQL mutations, and Astro CLI operations that change Airflow Deployment state on the cluster, and returns `cluster is under maintenance`. Existing Airflow Deployments keep running: Dag runs, task execution, the triggerer, and schedulers are unaffected.
  </Step>

  <Step title="Update the Helm values for control mode">
    Change the following keys in your existing platform values. Keep all other unified-mode values unchanged.

    ```yaml theme={null}
    global:
      baseDomain: <base-domain>  # unchanged from your unified install
      plane:
        mode: "control"          # switched from "unified"
      # Enable only if your unified install uses in-cluster Elasticsearch logging.
      # Keeps Elasticsearch running in control mode as a shared log store.
      sharedElasticsearch:
        enabled: true
    astronomer:
      houston:
        upgradeDeployments:
          enabled: false         # disables the upgrade deployments job
    ```

    <Accordion title="(Optional) Bring Your Own ingress controller or OpenShift">
      If you run your own ingress controller or Red Hat OpenShift, enable the auth sidecar and set your ingress annotations instead. Adjust the annotations to match your ingress controller class.

      ```yaml theme={null}
      global:
        baseDomain: <base-domain>  # unchanged from your unified install
        plane:
          mode: "control"          # switched from "unified"
        # Enable only if your unified install uses in-cluster Elasticsearch logging.
        # Keeps Elasticsearch running in control mode as a shared log store.
        sharedElasticsearch:
          enabled: true
        authSidecar:
          enabled: true
        extraAnnotations:
          # set according to your ingress controller class configuration
          openshift-default.kubernetes.io/ingress.class: openshift-default
          route.openshift.io/termination: edge
      astronomer:
        houston:
          upgradeDeployments:
            enabled: false         # disables the upgrade deployments job
      ```
    </Accordion>

    <Warning>
      Applying these values runs a Helm upgrade, which upgrades every Airflow Deployment on an uncordoned cluster. Confirm the cluster is in maintenance mode (the previous step) before you apply, and set `--set astronomer.houston.upgradeDeployments.enabled=false`.
    </Warning>

    <Warning>
      If your unified install uses in-cluster Elasticsearch, you must set `global.sharedElasticsearch.enabled: true`. Without this flag, Elasticsearch is torn down when the cluster switches to control mode and historical logs become unavailable.
    </Warning>
  </Step>

  <Step title="Run the Helm upgrade">
    Apply the updated values with a Helm upgrade and wait for the release to reach a deployed state. The data plane components are removed and the cluster is reconfigured as a standalone control plane.

    After the upgrade, Airflow Deployments are no longer explorable in the UI. This is expected: the data plane internal API isn't available in control-only mode, so calls to it return an error until you provision and bind the new data plane in Phase 2. Existing Airflow workloads continue to run.
  </Step>
</Steps>

## Phase 2: Provision the data plane and re-associate

```mermaid theme={null}
sequenceDiagram
    actor User as UI / API
    participant API as APC API
    participant CP as Control plane
    participant DP as New data plane

    Note over User,DP: 1. Install the data plane (plane.mode: data)
    User->>DP: helm install (same baseDomain, nginx disabled)
    DP-->>User: data plane ready

    Note over User,DP: 2. Re-bind endpoints (updateCluster)
    User->>API: updateCluster mutation
    API->>CP: update metadata and internal API endpoints
    CP-->>DP: bind data plane endpoints

    Note over User,DP: 3. Disable maintenance mode
    User->>API: uncordonCluster mutation
    API-->>User: normal operations restored
```

<Steps>
  <Step title="Provision the data plane">
    Install a new data plane release in the same namespace. For a same-cluster data plane:

    * Leave `global.plane.domainPrefix` empty so that the data plane reuses the existing `global.baseDomain`.
    * Set `global.nginx.enabled: false` so that the data plane reuses the existing ingress controller instead of installing its own.
    * Set the ingress class annotation so that traffic routes to the existing controller.

    ```yaml theme={null}
    global:
      baseDomain: <base-domain>                   # same as the control plane
      plane:
        mode: "data"
        domainPrefix: ""                          # reuse the existing baseDomain
      sharedElasticsearch:
        enabled: true                             # match the control plane setting
      extraAnnotations:
        kubernetes.io/ingress.class: "<control-plane-release-name>-nginx"
      nginx:
        enabled: false                            # reuse the control plane's ingress controller
      postgresql:
        enabled: false
      tlsSecret: astronomer-tls
      ssl:
        enabled: true
        mode: "require"
    ```

    <Accordion title="(Optional) Bring Your Own ingress controller or OpenShift">
      If you run your own ingress controller or Red Hat OpenShift, enable the auth sidecar and set your ingress annotations instead. Adjust the annotations to match your ingress controller class.

      ```yaml theme={null}
      global:
        baseDomain: <base-domain>                   # same as the control plane
        plane:
          mode: "data"
          domainPrefix: ""                          # reuse the existing baseDomain
        sharedElasticsearch:
          enabled: true                             # match the control plane setting
        authSidecar:
          enabled: true
        extraAnnotations:
          # set according to your ingress controller class configuration
          openshift-default.kubernetes.io/ingress.class: openshift-default
          route.openshift.io/termination: edge
      ```
    </Accordion>

    <Accordion title="(Optional) In-cluster hosted registry with a StatefulSet">
      If your data plane uses an in-cluster registry hosted with a StatefulSet, include the following block in the data plane values so it reuses the control plane's registry.

      ```yaml theme={null}
      astronomer:
        registry:
          enabled: true
          fullnameOverride: <control-plane-release-name>-registry
      ```
    </Accordion>

    <Warning>
      Don't set `global.plane.domainPrefix`. Setting it generates a new set of Deployment base domains, which causes widespread downtime, forces a Deployment upgrade, and changes user-facing URLs.
    </Warning>

    <Warning>
      The data plane and control plane share one ingress controller in split mode. If you use the default ingress controller, set the `global.extraAnnotations` ingress class to the control plane's ingress class (`<control-plane-release-name>-nginx`) so that traffic routes to the existing controller.
    </Warning>

    Use a different Helm release name from the original unified release. Wait for the data plane Pods to become ready.
  </Step>

  <Step title="Re-bind the control plane to the data plane">
    Point the control plane at the new data plane's internal API (gRPC) and metadata endpoints with the `updateCluster` mutation.

    ```graphql theme={null}
    mutation {
      updateCluster(
        id: "<cluster-id>"
        commanderEndpoint: "commander.<global-baseDomain>:443"
        metadataEndpoint: "http://<dataplane-release-name>-commander.<namespace>.svc.cluster.local.:8880"
      ) {
        id
      }
    }
    ```

    Cluster metadata reconciles automatically after `updateCluster`, so the split-mode configuration is refreshed without a separate manual sync.

    <Info>
      The metadata and gRPC endpoint values appear in the post-install output of the data plane Helm release. Copy those values into the `updateCluster` mutation.
    </Info>
  </Step>

  <Step title="Disable maintenance mode">
    Take the cluster out of maintenance mode to restore normal operations.

    ```graphql theme={null}
    mutation {
      uncordonCluster(clusterId: "<cluster-id>") {
        id
        isCordoned
        cordonedAt
      }
    }
    ```
  </Step>
</Steps>

## Verify the migration

Confirm the following after the cutover:

* The control plane and data plane Pods are running and healthy.
* Each Airflow Deployment is running and reachable at its existing domain. The same-cluster data plane reuses `baseDomain`, so URLs are unchanged.
* Airflow task logs and component logs are continuous across the migration with no gap. This confirms the shared Elasticsearch configuration.
* Metrics history is intact.
* The cluster is out of maintenance mode and Deployment mutations succeed again. Create or update a test Deployment to confirm.

## Roll back

You can roll back a CP/DP split setup to a unified installation with the following steps.

<Steps>
  <Step title="Enable maintenance mode">
    Put the cluster into maintenance mode so that Deployment mutations are blocked during the cutover. This also cordons existing Airflow Deployments so that the Helm upgrade doesn't re-roll them.

    ```graphql theme={null}
    mutation {
      cordonCluster(clusterId: "<cluster-id>", reason: "maintenance") {
        id
        isCordoned
        cordonedAt
      }
    }
    ```
  </Step>

  <Step title="Uninstall the split data plane Helm release">
    ```bash theme={null}
    helm uninstall <dataplane-release-name> --debug
    ```
  </Step>

  <Step title="Upgrade the control plane back to unified">
    Change the following keys in your control plane platform values to switch from split back to unified mode.

    ```yaml theme={null}
    global:
      baseDomain: <base-domain>  # unchanged from your split install
      plane:
        mode: "unified"          # switched from "control"
    astronomer:
      houston:
        upgradeDeployments:
          enabled: false         # disables the upgrade deployments job
    ```

    Apply the values with a Helm upgrade:

    ```bash theme={null}
    helm upgrade <control-plane-release-name> -f config.yaml --no-hooks
    ```

    <Info>
      Running with `--no-hooks` skips the blocking jobs for the missing components. After the Helm upgrade completes, all components return to normal.
    </Info>
  </Step>

  <Step title="Re-bind the metadata and gRPC endpoints">
    Point the control plane at the data plane's internal API (gRPC) and metadata endpoints with the `updateCluster` mutation.

    ```graphql theme={null}
    mutation {
      updateCluster(
        id: "<cluster-id>"
        commanderEndpoint: "<control-plane-release-name>-commander.<namespace>.svc.cluster.local:50051"
        metadataEndpoint: "http://<control-plane-release-name>-commander.<namespace>.svc.cluster.local.:8880"
      ) {
        id
      }
    }
    ```
  </Step>

  <Step title="Disable maintenance mode">
    Take the cluster out of maintenance mode to restore normal operations.

    ```graphql theme={null}
    mutation {
      uncordonCluster(clusterId: "<cluster-id>") {
        id
        isCordoned
        cordonedAt
      }
    }
    ```
  </Step>
</Steps>

### Verify the rollback

Confirm that the return to unified mode is complete:

* The control plane and data plane components are healthy.
* Airflow Deployments are accessible and you can update them.
* Airflow task logs and component logs are continuous across the migration with no gap. This confirms the shared Elasticsearch configuration.
* Metrics history is intact.
* The cluster is out of maintenance mode and Deployment mutations succeed again. Create or update a test Deployment to confirm.

## After migration

A same-cluster split satisfies the prerequisite for disaster recovery: the Deployments are now eligible for failover. Enabling failover is a separate procedure and requires a separate destination cluster; it isn't provided by this migration. See [Enable data plane failover](/docs/astro-private-cloud/v-2-x/enable-data-plane-failover).
