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

# Upgrade to Astro Private Cloud 2.0 from 0.37

This guide provides instructions to upgrade directly from Astro Private Cloud (APC) 0.37.4 to 2.0. This is a major upgrade that includes platform-level infrastructure changes (NATS JetStream migration, component replacements) as well as a comprehensive `values.yaml` schema migration.

<Warning>
  **Upgrade requires platform downtime**

  This upgrade deletes and recreates several platform components (NATS, STAN, the APC API). The platform will be unavailable for creating or updating Deployments during the upgrade. Plan a maintenance window and notify your users before starting.
</Warning>

<Note>
  If you are already on APC 1.x, use the simpler [Upgrade to 2.0 from 1.x](/docs/astro-private-cloud/v-2-x/upgrade-1x-to-2) guide instead. That guide doesn't require the platform-level migration steps described here.
</Note>

## Prerequisites

* [Upgrade to APC 0.37.4](/docs/astro-private-cloud/v-0-37/upgrade-astronomer) if you're on an earlier version.

* Back up your platform database. At minimum, create a snapshot or backup of your PostgreSQL database (RDS snapshot, Azure backup, Cloud SQL backup, or `pg_dump`).

* Verify that all platform and Airflow Deployment Pods are healthy:

  ```bash wrap theme={null}
  kubectl get pods -n astronomer
  kubectl get pods -n astronomer-<deployment-release-name>
  ```

* If you use Astronomer Units (AU) for resource configuration, [convert to CPU/memory settings while still on 0.37.x](/docs/astro-private-cloud/v-1-x/breaking-changes-removals#removal-of-astronomer-units-au-and-standardization-of-cpu/memory-settings) — the steps live on the APC 1.0 breaking changes page because that migration was required for the 1.0 release. The AU-to-CPU/memory migration script was removed in 1.0 and isn't available in 2.0.

* Check for duplicate workspace labels in your database. The upgrade includes a migration that adds a unique constraint to workspace labels, and it will fail if duplicates exist:

  ```sql wrap theme={null}
  SET search_path TO "houston$default";
  SELECT label, COUNT(*) FROM "Workspace" GROUP BY label HAVING COUNT(*) > 1;
  ```

  If this query returns any results, rename or remove the duplicate workspaces before proceeding.

* Remove any deprecated Helm values from your `values.yaml` that are no longer recognized in 2.0. See [Breaking changes and removals](/docs/astro-private-cloud/v-2-x/breaking-changes-removals) for the full list.

* Python 3.10 or later installed on your local computer (required for the values migration script).

* Install the `ruamel.yaml` Python package:

  ```bash wrap theme={null}
  pip install ruamel.yaml
  ```

* (Optional) Capture logs from your NATS and STAN instances before the upgrade:

  ```bash wrap theme={null}
  kubectl logs -n astronomer -l component=nats --tail=1000 > nats-pre-upgrade.log
  kubectl logs -n astronomer -l component=stan --tail=1000 > stan-pre-upgrade.log
  ```

<Tip>
  For help with upgrade issues, see [Debug upgrade](/docs/astro-private-cloud/v-2-x/debug-upgrade).
</Tip>

<Warning>
  **Manual DNS and load balancer update required**

  When upgrading from APC 0.`x.x` to 2.0, APC creates a new control plane NGINX ingress Service (`astronomer-cp-nginx`) and a new LoadBalancer. The previous ingress and LoadBalancer are replaced.

  You must:

  * Update all DNS records to the new load balancer IP address.
  * Update firewall, allowlist, and security rules to point to the new public endpoint.
  * Re-issue or update any TLS/SSL certificates that reference the previous LoadBalancer hostname, if applicable.

  This change occurs because the control plane ingress Service name changes from `astronomer-nginx` (0.x) to `astronomer-cp-nginx` (2.0), which causes Kubernetes to provision a new external LoadBalancer with a new public IP/hostname. Prepare these updates before performing the upgrade.

  Example (your output will vary by cloud/provider):

  ```bash wrap theme={null}
  kubectl -n astronomer get svc astronomer-cp-nginx
  NAME                  TYPE           CLUSTER-IP      EXTERNAL-IP                                        PORT(S)                      AGE
  astronomer-cp-nginx  LoadBalancer   10.100.223.78   7666ac61ef6-1683718677.us-east-2.elb.amazonaws.com 80:32255/TCP,443:32647/TCP  25d
  ```
</Warning>

<Note>
  If you are on OpenShift and manage your own Routes or use a third-party ingress controller instead of the platform's built-in NGINX ingress, this LoadBalancer change doesn't apply to you. You can skip the DNS update step later in this guide.
</Note>

<Note>
  **Static or internal loadBalancerIP**

  If you explicitly set a static or internal `loadBalancerIP` under `nginx` in your `values.yaml` (for example, when using an internal GKE load balancer with `privateLoadBalancer: true`), the Helm chart passes that IP through to the new `astronomer-cp-nginx` Service. Kubernetes binds the new LoadBalancer to the same IP you provided, so the external IP doesn't change and no DNS update is required.

  In this scenario, you can skip the DNS update step later in this guide. You should still update firewall, allowlist, and TLS configurations if they reference the previous Service name.

  The behavior depends on the cloud provider's `LoadBalancer` implementation and whether it supports reusing a specified IP. Astronomer recommends verifying on your target platform before the production upgrade. For more information, see the [Kubernetes Service documentation](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer).
</Note>

## Step 1: Get the migration script

The migration scripts are located in the `bin/` folder of the [Astronomer Helm chart repository](https://github.com/astronomer/astronomer). Clone the repository and check out the `release-2.0` branch:

```bash wrap theme={null}
git clone https://github.com/astronomer/astronomer.git
cd astronomer
git checkout release-2.0
```

The script you need for this upgrade path is `bin/migrate-helm-chart-values-037x-to-2x.py`.

## Step 2: Migrate your `values.yaml`

The 0.37.x-to-2.x migration is a comprehensive transformation that includes feature flag restructuring, removal of obsolete components, key renames, value updates, new platform keys, and APC API configuration flag restructuring under `astronomer.houston.config.deployments`. The migration script automates the entire transformation.

### Back up your override file

```bash wrap theme={null}
cp my-values.yaml my-values.yaml.backup
```

### Preview changes (dry run)

```bash wrap theme={null}
./bin/migrate-helm-chart-values-037x-to-2x.py --dry-run my-values.yaml
```

Example output:

```text wrap theme={null}
Found 28 migration(s) to apply:
  global.rbacEnabled -> global.rbac.enabled
  global.sccEnabled -> global.scc.enabled
  ...
  global.singleNamespace -> (deleted)
  global.veleroEnabled -> (deleted)
  ...
  fluentd -> vector
  global.pgbouncer.krb5ConfSecretName -> global.pgbouncer.secretName
  global.pgbouncer.servicePort: "5432" -> "6543"
  ...
  (new) -> global.plane
  ...
```

If you see `No migrations needed`, your file is already compatible with 2.0.

### Run the migration

<Tabs>
  <Tab title="In-place with backup (recommended)">
    ```bash wrap theme={null}
    ./bin/migrate-helm-chart-values-037x-to-2x.py --in-place --backup my-values.yaml
    ```

    This modifies your file directly and creates `my-values.yaml.bak` as a backup.
  </Tab>

  <Tab title="Write to a new file">
    ```bash wrap theme={null}
    ./bin/migrate-helm-chart-values-037x-to-2x.py my-values.yaml migrated-values.yaml
    ```
  </Tab>

  <Tab title="Output to stdout">
    ```bash wrap theme={null}
    ./bin/migrate-helm-chart-values-037x-to-2x.py my-values.yaml > migrated-values.yaml
    ```
  </Tab>
</Tabs>

### Review the migrated file

Open the migrated file and verify the changes look correct. Pay special attention to:

* **New default values** — review the [New keys added with defaults](#new-keys-added-with-defaults) table and override any defaults that don't match your environment.
* **Fluentd-to-Vector rename** — if you had custom Fluentd configuration, only the resource values carry over. See [Breaking changes](/docs/astro-private-cloud/v-2-x/breaking-changes-removals) for migration guidance.
* **PgBouncer port** — confirm the new port (6543) works for your setup, or override it back to 5432.
* **Unified mode** — Ensure `global.plane.mode` is set to `"unified"`. This is equivalent to how 0.37.x operates and is required for the initial upgrade.

## Step 3: Validate the Helm upgrade (dry run)

Run a Helm upgrade dry run to verify that the upgrade will succeed:

```bash wrap theme={null}
helm upgrade -f migrated-values.yaml -n astronomer astronomer astronomer/astronomer --version 2.0.x --dry-run
```

Replace `2.0.x` with the specific patch version you are upgrading to.

<Warning>
  Don't proceed with the remaining steps until the dry run completes successfully. If it fails, resolve the reported errors first.
</Warning>

## Step 4: Delete existing STAN and NATS StatefulSets

Before upgrading, you must migrate from STAN to JetStream. Run the following commands to remove legacy STAN components:

```bash wrap theme={null}
kubectl delete sts <release-name>-stan
kubectl delete sts <release-name>-nats --cascade=orphan
```

## Step 5: Patch `astronomer-bootstrap` secret with database name

Ensure the `astronomer-bootstrap` secret includes a database name suffix in `connection` (for example, `/postgres`). This prevents Postgres connection errors during or after the upgrade.

### Check your current connection string

```bash wrap theme={null}
kubectl get secret -n astronomer astronomer-bootstrap \
  --template='{{.data.connection | base64decode }}'
```

A connection string with a database name looks like:

```text wrap theme={null}
postgresql://user:pass@host:5432/postgres
                                 ^^^^^^^^
                                 database name present
```

A connection string without a database name looks like:

```text wrap theme={null}
postgresql://user:pass@host:5432
                                ^
                                no database name
```

If your connection string already includes a database name, skip to [Step 6](#step-6-delete-the-apc-deployment).

### Determine the correct database name

The default database name depends on your cloud provider:

* **AWS RDS**: `postgres` (standard default; `main` may appear only in specific legacy or custom configurations)
* **Azure Database for PostgreSQL**: `postgres`
* **GCP Cloud SQL**: `postgres`

If you're unsure, connect to your PostgreSQL instance and run `\l` to list available databases.

### Patch the secret

```bash wrap theme={null}
NAMESPACE=astronomer
DB_NAME=postgres

CURRENT=$(kubectl -n "$NAMESPACE" get secret astronomer-bootstrap -o jsonpath='{.data.connection}' | base64 -d)
NEW="${CURRENT%/}/$DB_NAME"
kubectl -n "$NAMESPACE" patch secret astronomer-bootstrap --type=merge -p "{\"data\":{\"connection\":\"$(printf '%s' "$NEW" | base64 -w0)\"}}"
```

## Step 6: Delete the APC Deployment

Delete the APC Deployment to avoid a known Helm patch conflict related to environment variable ordering changes between versions.

```bash wrap theme={null}
kubectl delete deployment/<release-name>-houston -n astronomer --cascade=orphan
```

The `--cascade=orphan` flag keeps the APC Pods running during the delete operation. The Helm upgrade in the next step recreates the Deployment with the correct configuration.

<Tip>
  If you skip this step and encounter a Helm patch error during the upgrade, see [Debug upgrade](/docs/astro-private-cloud/v-2-x/debug-upgrade#helm-upgrade-fails-with-patch-conflict) for the workaround.
</Tip>

## Step 7: Upgrade to APC 2.0

Ensure that your migrated `values.yaml` includes the unified mode configuration:

```yaml wrap theme={null}
global:
  plane:
    mode: "unified"
```

<Note>
  After upgrading to 2.0 in unified mode, you can optionally add separate data planes to run Airflow Deployments in other clusters.
</Note>

Complete all pre-upgrade steps (database backup, STAN/NATS deletion, bootstrap secret patch, APC Deployment deletion, values migration) before running the upgrade command.

```bash wrap theme={null}
helm upgrade -f migrated-values.yaml -n astronomer astronomer astronomer/astronomer --version 2.0.x
```

Replace `2.0.x` with the specific patch version you are upgrading to.

<Note>
  **Airgapped environments**

  For airgapped environments that can't access the internet, download the Helm chart `.tgz` file directly from the Astronomer Helm repository:

  ```text wrap theme={null}
  https://helm.astronomer.io/astronomer-<version>.tgz
  ```

  Replace `<version>` with the specific version you are upgrading to. Upload this file to your internal artifact repository, then reference it in your `helm upgrade` command.
</Note>

## Step 8: Restart NATS and APC API components

After the platform upgrade completes and all Pods are running, restart NATS and APC API components to ensure the new JetStream components and APC API services are connected and synchronized:

```bash wrap theme={null}
kubectl rollout restart sts/<release-name>-nats
kubectl rollout restart deploy/<release-name>-houston
kubectl rollout restart deploy/<release-name>-houston-worker
```

After the rollout completes, verify the Pods have been recreated by checking their age:

```bash wrap theme={null}
kubectl get pods -n astronomer -l "component in (nats,houston,houston-worker)" -o wide
```

All Pods should show a recent `AGE` (a few minutes). If any Pods show an older age, delete them manually to force recreation:

```bash wrap theme={null}
kubectl delete pod -n astronomer -l component=nats
kubectl delete pod -n astronomer -l component=houston
kubectl delete pod -n astronomer -l component=houston-worker
```

## Step 9: Update DNS records

APC 2.0 creates a new control plane NGINX ingress Service (`astronomer-cp-nginx`) with a new LoadBalancer. *You must complete this step before you can access the Astro UI or API*.

Skip this step if either of the following applies:

* You are on OpenShift and manage your own Routes or use a third-party ingress controller.
* You explicitly set a static `loadBalancerIP` under `nginx` in your `values.yaml`. In this case, the new Service binds to the same IP and no DNS update is required. Confirm by comparing the `EXTERNAL-IP` returned by the following command to the IP in your `values.yaml`.

Get the new load balancer address:

```bash wrap theme={null}
kubectl -n astronomer get svc astronomer-cp-nginx
```

Update your DNS records to point to the new load balancer IP or hostname. This includes all subdomains for your base domain (for example, `app.<baseDomain>`, `houston.<baseDomain>`, `registry.<baseDomain>`).

## Step 10: Upgrade all Airflow Deployments

After you have validated that all platform Pods are healthy, upgrade your Airflow Deployments to ensure compatibility with 2.0.

<Note>
  Existing Airflow Deployments typically continue to function after the platform upgrade without immediate action. However, Astronomer recommends upgrading Deployments to ensure full compatibility with the new platform version.
</Note>

To upgrade Deployments, use one of the following approaches:

* **Astro UI**: Upgrade each Deployment manually from the Astro UI.
* **APC API**: Use the APC API `upsertDeployment` mutation for programmatic or bulk upgrades.
* **Astro CLI**: Use the Astro CLI's [astro deploy](/docs/cli/v1.43/astro-deploy) command.

## Step 11: Validate the upgrade

<Steps>
  <Step title="Confirm that NATS Pods are running with JetStream enabled">
    Check if the JetStream job is created:

    ```bash wrap theme={null}
    kubectl -n astronomer get jobs | grep jetstream
    ```

    Check if NATS Pods are running:

    ```bash wrap theme={null}
    kubectl -n astronomer get pods -l component=nats
    ```
  </Step>

  <Step title="Verify APC Worker Pods are healthy and processing events">
    Check if APC Worker Pods are running:

    ```bash wrap theme={null}
    kubectl -n astronomer get pods -l component=houston-worker
    ```
  </Step>

  <Step title="Verify there are no remaining references to STAN">
    This command should return no results:

    ```bash wrap theme={null}
    kubectl -n astronomer get statefulsets | grep stan
    ```
  </Step>

  <Step title="Verify Vector is running (replaces Fluentd)">
    ```bash wrap theme={null}
    kubectl -n astronomer get pods -l component=vector
    ```
  </Step>

  <Step title="Verify you can access the Astro UI">
    Navigate to `app.<baseDomain>` in your browser and confirm the UI loads.
  </Step>
</Steps>

## Values migration reference

`bin/migrate-helm-chart-values-037x-to-2x.py` on the [Astronomer Helm chart `release-2.0` branch](https://github.com/astronomer/astronomer) includes **every** shared rule from [Values migration reference](/docs/astro-private-cloud/v-2-x/upgrade-1x-to-2#values-migration-reference) in `bin/helm_chart_values_migration_shared.py`, **and** the 0.37-only steps in the tables below. The script runs, in order: (1) `apply_global_feature_flag_rules_to_all`; (2) the 0.37 `MIGRATIONS` list in `migrate-helm-chart-values-037x-to-2x.py` (every `DeleteKey`, `RenameKey`, `SetValue`, and `AddKeyIfMissing` in that file); (3) the same `MIGRATIONS` against nested `global` maps; (4) `apply_houston_config_flag_migrations`; (5) `apply_houston_deployment_migrations`; (6) `apply_nginx_csp_policy_migrations`. The 1.x script uses the same APC API and nginx steps but a different order and adds `strictSchemaCheck` at the end; the 0.37 script adds `strictSchemaCheck` during step (2). Use `--dry-run` to see the exact list for your file.

The following tables are the **complete** 0.37-only portion (the full `MIGRATIONS` list from that script).

### Deleted keys (0.37 script)

| Key                                         | Notes                                                                        |
| ------------------------------------------- | ---------------------------------------------------------------------------- |
| `global.singleNamespace`                    | Obsolete feature                                                             |
| `global.veleroEnabled`                      | Obsolete feature                                                             |
| `global.enableHoustonInternalAuthorization` | Replaced by install-mode behavior                                            |
| `global.nodeExporterSccEnabled`             | No longer used                                                               |
| `global.stan`                               | STAN removed (JetStream)                                                     |
| `tags.stan`                                 | STAN tag removed                                                             |
| `stan` (top-level)                          | STAN resources removed                                                       |
| `kibana` (top-level)                        | See [Breaking changes](/docs/astro-private-cloud/v-2-x/breaking-changes-removals) |
| `prometheus-blackbox-exporter` (top-level)  | See [Breaking changes](/docs/astro-private-cloud/v-2-x/breaking-changes-removals) |

### Renamed keys (0.37 script)

| Old path                              | New name / path               | Notes                                                                |
| ------------------------------------- | ----------------------------- | -------------------------------------------------------------------- |
| `fluentd` (top-level key)             | `vector` (top-level)          | Subtree kept; custom Fluentd config isn't auto-translated to Vector. |
| `global.pgbouncer.krb5ConfSecretName` | `global.pgbouncer.secretName` | Same Secret object                                                   |

### Value updates (only when the current value matches the "old" column)

| Path                            | Old             | New             | Notes                                             |
| ------------------------------- | --------------- | --------------- | ------------------------------------------------- |
| `global.pgbouncer.servicePort`  | `5432` (string) | `6543` (string) | Set back to `5432` in overrides if required       |
| `global.nats.jetStream.enabled` | `false`         | `true`          | Set to `false` in overrides to keep JetStream off |

### New keys added with defaults

The migration script adds these keys with default values if they aren't already present. Review each key and override the default if it doesn't match your environment.

| Key Path                              | Default Value | Description                                               | Action Needed?                                                               |
| ------------------------------------- | ------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `global.authHeaderSecretName`         | `~` (null)    | Name of Kubernetes secret for cross-plane authentication. | Set this if running in multi-plane mode. Not needed for unified mode.        |
| `global.plane.mode`                   | `"unified"`   | Platform operating mode: `control`, `data`, or `unified`. | Change to `control` or `data` if running a multi-plane deployment.           |
| `global.plane.domainPrefix`           | `""`          | Cluster identifier prefix for multi-plane DNS.            | Set to your cluster ID if running in multi-plane mode.                       |
| `global.podLabels`                    | `{}`          | Labels applied to every pod.                              | Add labels if needed for monitoring, cost allocation, or policy enforcement. |
| `global.logging.provider`             | `~` (null)    | Logging provider identifier.                              | Set if using a specific logging backend.                                     |
| `nats.init.resources.requests.cpu`    | `"75m"`       | NATS init container CPU request.                          | Override if your cluster needs different resource settings.                  |
| `nats.init.resources.requests.memory` | `"30Mi"`      | NATS init container memory request.                       | Override if your cluster needs different resource settings.                  |
| `nats.init.resources.limits.cpu`      | `"250m"`      | NATS init container CPU limit.                            | Override if your cluster needs different resource settings.                  |
| `nats.init.resources.limits.memory`   | `"100Mi"`     | NATS init container memory limit.                         | Override if your cluster needs different resource settings.                  |

## Roll back the upgrade

If you need to revert the values migration:

1. Restore your backup:

   ```bash wrap theme={null}
   cp my-values.yaml.backup my-values.yaml
   ```

2. To downgrade the chart after a Helm upgrade:

   ```bash wrap theme={null}
   helm rollback astronomer <previous-revision> --namespace astronomer
   ```
