> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-majid-docs-content-expansion.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# TiDB

> Deploy TiDB on Control Plane using the Template Catalog. Covers the prerequisite credentials secret, distributed MySQL-compatible clustering with TiKV storage and PD coordination, scheduled S3 and GCS backups, and migrating from template version 1.x.

<Warning>
  **Template version 2.0.0 is a breaking change, and one of the changes is a data-loss hazard.**

  * **The template no longer creates a GVC.** It deploys into a GVC you already have. `gvc.name` is gone, and `gvc.locations` / `gvc.pdReplicas` moved to the top-level `locations` and `pdReplicas`.
  * **Never upgrade a 1.x release onto 2.0.0 in place.** A 1.x release owns the GVC it created, and Helm deletes what a chart stops declaring — the upgrade destroys that GVC and everything inside it. Install a new release instead: [Migrating from 1.x](#migrating-from-1-x).
  * **`exposeServer` and `devMode` are removed**, and a values file that still sets either one fails at render. See [Access](#access) and [Locations](#locations).
  * **`internal_access` with `type: workload-list` never worked before 2.0.0** — a duplicate key discarded your list. It now reaches the stored spec and is enforced.
</Warning>

## Overview

TiDB is a distributed, MySQL-compatible database designed for horizontal scalability and high availability. It separates compute from storage across three components: a SQL processing layer (TiDB Server), a distributed key-value store (TiKV), and a placement driver (PD) that manages cluster metadata and scheduling.

This template deploys a TiDB cluster across one or more Control Plane locations using PingCAP's official images, with optional scheduled backups to S3 or GCS.

Database credentials are **not** template values. The init job and TiDB Server read the root password, application user, password and database name from a dictionary secret you create before installing, so none of them pass through Helm or land in the release.

### What Gets Created

* **Stateful PD Workload** — (`RELEASE_NAME-pd`): the placement driver quorum, `pdReplicas` members spread evenly across your locations. Uses `replicaDirect` addressing so each PD member is individually reachable.
* **Stateful TiKV Workload** — (`RELEASE_NAME-tikv`): distributed storage nodes. Replica count per location is controlled by `locations[].replicas`.
* **Standard TiDB Server Workload** — (`RELEASE_NAME-server`): MySQL-compatible SQL layer on port `4000`. Per-location replica count follows `locations[].replicas`.
* **DB Init Cron Workload** *(optional, on by default)* — (`RELEASE_NAME-tidb-db-init`): a scheduled job that sets the root password and creates the application database and user, then fast-exits on every later run.
* **Backup Cron Workload** *(optional)* — A scheduled job that uses TiDB's `br` tool to write a full cluster snapshot to AWS S3 or GCS, unsuspended in exactly one location.
* **Volume Set** — PD storage (`RELEASE_NAME-tidb-pd-vs`): `volumeset.pd.capacity` GiB with no autoscaling, ext4, general-purpose SSD, with 7-day snapshot retention.
* **Volume Set** — TiKV storage (`RELEASE_NAME-tidb-tikv-vs`): configurable capacity with optional autoscaling, ext4, general-purpose SSD, with 7-day snapshot retention.
* **Secrets** — Opaque secrets containing startup scripts for PD, TiKV, TiDB Server, and the database init job. **No credential secret** — the passwords live only in the prerequisite secret you create.
* **Identity & Two Policies** — A shared identity bound to all workloads, with `reveal` on the template's own secrets plus exactly the credentials secret you created, `view` on the one GVC you install into so PD can confirm at boot that the GVC really has every location you listed, and cloud storage access when backup is enabled.

<Note>
  **This template does not create a GVC.** It deploys into a GVC you already have — every resource lands in the GVC you install into, so `cpln workload exec`, `cpln logs` and uninstalling all work against that GVC, and uninstalling can never delete it. Every location you list in `locations` must already be on that GVC, and a GVC location you did not list simply runs nothing.
</Note>

## Prerequisites

Two things must be in place before you install: a GVC with the right locations, and a credentials secret.

### A GVC with your locations

**A GVC must already exist, and it must contain every location you list in `locations`.** The requirement is one-directional — the GVC may have *more* locations than you list, and nothing TiDB-related runs in those. Check what a GVC has before installing:

```bash theme={null}
cpln gvc get GVC_NAME -o json
```

The locations are under `spec.staticPlacement.locationLinks`. To add a missing one:

```bash theme={null}
cpln gvc add-location GVC_NAME --location aws-us-east-2
```

Every workload in a GVC runs in every location that GVC has, so add locations to a shared GVC deliberately.

<Warning>
  **A location the GVC does not have is not caught at install time.** The install succeeds — the platform does not validate it — and PD then refuses to bootstrap, restarting with this in the logs:

  ```text theme={null}
  [tidb-pd] FATAL: locations declared in values are not in GVC 'my-gvc': aws-us-west-2
  [tidb-pd] GVC 'my-gvc' has: aws-us-east-1 aws-us-east-2
  ```

  A PD member that **already holds data** logs a `WARNING` instead and keeps serving, so this check can never take down a running cluster.
</Warning>

### Database credentials

**One secret must exist before you install**, when `autoCreateDatabase.enabled` is `true` (the default). It holds the credentials your applications put in their connection strings, plus the cluster's root password. The values never pass through Helm, so they do not land in the release. Secrets are org-level, so no GVC flag is involved.

<Steps>
  <Step title="Create the database credentials secret">
    A [dictionary secret](/guides/create-secret/dictionary) holding exactly four keys — `rootPassword`, `user`, `password` and `db`. The init job sets the root password, then creates that user and that database:

    ```bash theme={null}
    cpln secret create-dictionary --name my-tidb-credentials \
      --entry rootPassword='YOUR-ROOT-PASSWORD' \
      --entry user=myuser \
      --entry password='YOUR-STRONG-PASSWORD' \
      --entry db=mydb
    ```

    Set `autoCreateDatabase.credentialsSecretName` to the name you used. Secret names are org-wide, so give each release its own.
  </Step>

  <Step title="Read the secret back later">
    Pass `-o yaml`. A bare `cpln secret reveal` prints only a summary table, not the values:

    ```bash theme={null}
    cpln secret reveal my-tidb-credentials -o yaml
    ```
  </Step>
</Steps>

<Warning>
  **Create the secret before installing, or the deployment wedges silently.** The template refuses to render when `autoCreateDatabase.credentialsSecretName` is blank, but a name pointing at a secret that does not exist installs "successfully" and then never starts. The container never runs, so `cpln logs` returns **zero lines** — there is nothing to log, and every summary surface just looks like a slow deploy. The one place the reason appears is `status.versions[].message`:

  ```bash theme={null}
  cpln workload get-deployments RELEASE_NAME-server --gvc GVC_NAME -o yaml
  ```

  ```text theme={null}
  The secret my-tidb-credentials no longer exists. Workload updates are paused until the
  secret is added or the reference to the secret removed.
  ```

  Use `get-deployments` — plain `cpln workload get` has no `versions` key and will show you nothing. Creating the missing secret repairs it on its own with no further action, in roughly **5.5 to 10.5 minutes** measured across several templates, or run `cpln workload force-redeployment RELEASE_NAME-server --gvc GVC_NAME` to clear it in about 90 seconds.
</Warning>

Backups need a bucket and a Control Plane [cloud account](/guides/create-cloud-account) before they can be enabled — see [AWS S3](#aws-s3) or [GCS](#gcs). Nothing else is required.

## Installation

Create the [prerequisite secret](#database-credentials) first, then install by whichever method you prefer:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Migrating from 1.x

Template versions through 1.8.1 created their own GVC, so that GVC is part of the 1.x release's manifest. Version 2.0.0 does not declare it — and Helm deletes what a chart stops declaring.

<Warning>
  **An in-place upgrade from 1.x to 2.0.0 destroys the cluster.** Measured on a sibling template with the guard removed: the upgrade **deleted the GVC and every workload, volume set and identity inside it in about six seconds, while printing `upgraded successfully`.** Reading the GVC back afterwards returned `404`. The volume sets hold your data.

  The chart ships a render-time refusal so this cannot happen by accident: any leftover `gvc` key in your values aborts the upgrade before a single API call is made, leaving your cluster untouched and running.

  ```text theme={null}
  Error: execution error at (tidb/templates/identity.yaml:1:4): tidb 2.0.0: the `gvc` values key was
  REMOVED. This chart no longer creates a GVC -- it deploys into the GVC you install into ... DO NOT
  `helm upgrade` a 1.x release onto 2.0.0 ... See `Migrating from 1.x` in the README.
  ```

  **The guard cannot cover one case: an upgrade run with no values file at all.** A 1.x release installed on pure defaults has no `gvc` key for the chart to see, so nothing fires and the deletion proceeds. Do not run an upgrade of a 1.x release against the 2.0.0 chart under any circumstances — install a new release and move the data across.
</Warning>

<Steps>
  <Step title="Recover the credentials the existing cluster uses">
    Versions up to 1.7.0 took the credentials as plain Helm values and wrote them into a chart-owned secret named after the release. Read them out of your current values file, or out of that secret:

    ```bash theme={null}
    cpln secret reveal RELEASE_NAME-tidb-user -o yaml
    ```

    Pass `-o yaml`. A bare `cpln secret reveal` prints only a summary table, not the values. Any password that came from a pre-1.8.0 default was published in the public template repository — treat it as compromised and choose a new one for the new cluster.
  </Step>

  <Step title="Back up the old cluster">
    Enable `backup` on the 1.x release, or run `br backup full` by hand against the old PD endpoint.
  </Step>

  <Step title="Choose the GVC for the new release">
    Create or pick a GVC and make sure it has exactly the locations you intend to list in `locations`. See [Prerequisites](#prerequisites).
  </Step>

  <Step title="Create the credentials secret and install 2.0.0 as a NEW release">
    Follow [Database credentials](#database-credentials), then install into that GVC with a **different release name** — secret names are org-wide, so a same-named release collides with the 1.x one even in another GVC.
  </Step>

  <Step title="Restore and cut over">
    Restore into the new cluster (see [Restoring a Backup](#restoring-a-backup)), then point your applications at the new `RELEASE_NAME-server` endpoint.
  </Step>

  <Step title="Uninstall the old release against the GVC you installed it into">
    Not the `tidb-gvc` it created — the GVC you passed at install time is where Helm tracks the release, and uninstalling from there takes the created GVC with it.
  </Step>
</Steps>

Values keys that moved or were removed in 2.0.0:

* `gvc.locations` is now the top-level `locations`, `gvc.pdReplicas` is now the top-level `pdReplicas`, and `gvc.name` is gone entirely.
* `devMode` is gone. It only waived a three-location requirement that no longer exists — `locations` may hold a single location, and PD's replication factor is now derived from the number of TiKV nodes you configure rather than mode-switched.
* `exposeServer` is gone. It never published the MySQL port: opening public inbound does nothing for port 4000 without a direct load balancer the chart does not render, so the only thing the canonical endpoint would have served was TiDB's **unauthenticated status API** on port 10080. Reach the server over internal GVC DNS or `cpln port-forward` instead.
* `replicas: 0` on a location is refused. 1.x turned it into a suspended location, and suspending a location permanently withdraws that workload's endpoints from other locations' service discovery. Remove the location from `locations` instead.

<Note>
  The guards fire on the mere presence of the removed keys, so `exposeServer: false` and `devMode: false` fail too. Anyone carrying a 1.x values file forward will hit an error naming the key and its replacement, rather than a silent misconfiguration.
</Note>

## Configuration

The default `values.yaml` for this template:

```yaml theme={null}
# ─── Locations ────────────────────────────────────────────────────────────────
# This chart deploys into the GVC you install into — it does NOT create one.
# Every location listed here MUST already exist in that GVC, or PD refuses to
# bootstrap with a named error (see Prerequisites in the README).
# Extra locations in the GVC are fine: nothing TiDB-related runs in them.
#
# `replicas` is the number of TiKV nodes AND TiDB server nodes in that location.
# The default — one location, three nodes — survives the loss of a node.
# Surviving the loss of a whole LOCATION needs at least three locations.
locations:
  - name: aws-us-east-1
    replicas: 3

# PD (placement driver) members, spread evenly across `locations`. PD is Raft
# based, so odd counts only: 1, 3, 5 or 7. 1 is a single point of failure and is
# for testing only.
pdReplicas: 3

images:
  server: pingcap/tidb:v8.5.7
  tikv: pingcap/tikv:v8.5.7
  pd: pingcap/pd:v8.5.7

resources:
  pd:
    cpu: 2
    memory: 4Gi
  server:
    cpu: 2
    memory: 2Gi
  tikv:
    cpu: 2
    memory: 4Gi

autoCreateDatabase: # Enable to automatically create the database on initialization
  enabled: true
  deployInitWorkload: true # Set to false after the DB has been initialized to remove the init workload and save resources
  # REQUIRED PREREQUISITE SECRET when autoCreateDatabase.enabled — CREATE IT
  # BEFORE YOU INSTALL. A `dictionary` secret holding exactly four keys:
  # `rootPassword`, `user`, `password` and `db`. If it does not exist at install
  # time the deployment WEDGES silently — `cpln logs` returns nothing at all.
  # See Prerequisites in the README for the create-dictionary command.
  credentialsSecretName: my-tidb-credentials
  # How often the init job runs. It fast-exits once the database exists, so
  # this only controls how soon after install that happens.
  schedule: "*/5 * * * *"
volumeset:
  tikv:
    capacity: 10 # initial capacity in GiB (minimum is 10)
    autoscaling:
      enabled: false # Set to true to enable autoscaling
      maxCapacity: 100 # Maximum capacity in GiB
      minFreePercentage: 10 # Minimum free percentage before scaling triggers
      scalingFactor: 1.2 # Multiplier applied to current capacity when scaling
  pd:
    capacity: 10 # initial capacity in GiB (minimum is 10)

external_access: # Set if client is outside the GVC or in another location
  server_outboundAllowCIDR: []
  tikv_outboundAllowCIDR: [] # Note: when backup.enabled is true, the template automatically allows outbound access (0.0.0.0/0) regardless of this value.
  pd_outboundAllowCIDR: []

# Who may reach each tier from inside the org. Every workload this release
# creates is ALWAYS allowed, whatever is set here — the tiers have to reach each
# other, and each tier's own replicas have to reach each other.
internal_access:
  server:
    type: same-gvc # options: same-gvc, same-org, workload-list
    workloads:  # Note: can only be used if type is workload-list
      #- //gvc/GVC_NAME/workload/WORKLOAD_NAME
  tikv:
    type: same-gvc # options: same-gvc, same-org, workload-list
    workloads:  # Note: can only be used if type is workload-list
      #- //gvc/GVC_NAME/workload/WORKLOAD_NAME
  pd:
    type: same-gvc # options: same-gvc, same-org, workload-list
    workloads:  # Note: can only be used if type is workload-list
      #- //gvc/GVC_NAME/workload/WORKLOAD_NAME

backup:
  enabled: false
  image: ghcr.io/controlplane-com/backup-images/tidb-backup:8.5.7
  schedule: "0 2 * * *"  # daily at 2am UTC
  activeDeadlineSeconds: 14400  # 4 hours max per backup job
  location: aws-us-east-1  # MUST be one of `locations` above; put it near your bucket

  resources:
    cpu: 1
    memory: 1Gi

  provider: aws # Options: aws or gcp

  aws:
    bucket: my-backup-bucket
    region: us-east-1
    cloudAccountName: my-backup-cloudaccount
    policyName: my-backup-policy
    prefix: tidb/backups

  gcp:
    bucket: my-backup-bucket
    cloudAccountName: my-backup-cloudaccount
    prefix: tidb/backups
```

### Locations

* `locations` — List of Control Plane locations. Every one must already exist in the GVC you install into.
* `locations[].replicas` — Number of TiKV **and** TiDB Server replicas in that location. Must be at least 1; `0` is refused at render.
* `pdReplicas` — Total number of PD members across all locations, spread evenly with any remainder going to the first ones. PD is Raft-based, so it must be `1`, `3`, `5` or `7`. A value of `1` is a single point of failure and is for testing only.

The default — one location, three TiKV nodes, three PD members — survives the loss of a node. It does **not** survive the loss of a location. For that, use three locations with PD spread one per location:

```yaml theme={null}
locations:
  - name: aws-us-east-1
    replicas: 1
  - name: aws-us-west-2
    replicas: 1
  - name: aws-eu-central-1
    replicas: 1
pdReplicas: 3
```

PD then keeps quorum when one location goes away, and TiKV spreads each region's three copies one per location.

<Note>
  GVC locations you did not list show as red in the console, with `This workload location is deactivated because maxScale is set to 0.` That is the mechanism that keeps a shared GVC safe — it is what a healthy install looks like, not a fault.
</Note>

### Images and Resources

* `images.server` / `images.tikv` / `images.pd` — Container images for each tier. Bump these together with `backup.image`: from v8.5.7, `br` enforces a version match with the cluster.
* `resources.pd.cpu` / `resources.pd.memory` — CPU and memory per PD member.
* `resources.server.cpu` / `resources.server.memory` — CPU and memory per TiDB Server replica.
* `resources.tikv.cpu` / `resources.tikv.memory` — CPU and memory per TiKV replica.

The shipped defaults are sized for testing. For production, PD wants 4–8 CPU / 8–16Gi, TiDB Server 8–16 CPU / 16–32Gi (it scales with concurrent connections), and TiKV 8–16 CPU / 32–64Gi (memory-hungry for caching).

### Database Initialization

* `autoCreateDatabase.enabled` — Wires the credentials secret into the TiDB Server and init workload, and grants the identity `reveal` on it.
* `autoCreateDatabase.deployInitWorkload` — Deploys the init job that sets the root password and creates the application database and user.
* `autoCreateDatabase.credentialsSecretName` — Name of the dictionary secret holding `rootPassword`, `user`, `password` and `db`. See [Database credentials](#database-credentials).
* `autoCreateDatabase.schedule` — How often the init job runs.

The init job is a **cron workload**, and that is deliberate. It fast-exits once the database exists — measured at 191 to 317 milliseconds of script body — so every run after the first is a no-op, and `schedule` really only controls how soon after install the database appears. With the default `*/5 * * * *`, a measured install had the database created **61 seconds** after `helm install` finished, well ahead of the five-minute worst case.

<Note>
  Set `autoCreateDatabase.deployInitWorkload: false` and upgrade if you would rather remove the job entirely once the cluster is initialized. It is cheap to leave on; the trade is a workload that runs forever for a one-time purpose.
</Note>

<Note>
  Credentials are applied on first initialization only. Changing the secret afterwards does **not** change the cluster — it only changes what clients present when they authenticate. To rotate on an existing cluster, run `ALTER USER` inside TiDB first, then update the secret, then force a redeployment: a `cpln://` reference is resolved when a replica starts and is never re-resolved while it runs.
</Note>

### Storage

**TiKV storage** (configurable):

* `volumeset.tikv.capacity` — Initial volume size in GiB (minimum 10).
* `volumeset.tikv.autoscaling.enabled` — Automatically expand volumes as they fill. When enabled:
  * `maxCapacity` — Maximum volume size in GiB.
  * `minFreePercentage` — Trigger a scale-up when free space drops below this percentage.
  * `scalingFactor` — Multiply current capacity by this factor when scaling up.

**PD storage** (fixed):

* `volumeset.pd.capacity` — Initial volume size in GiB for PD metadata (minimum 10). PD only holds cluster metadata, so it has no autoscaling knob.

Both volume sets retain snapshots for 7 days and create a final snapshot on deletion.

### Access

**Internal access** — configured per component (`server`, `tikv`, `pd`):

| Type            | Description                                                   |
| --------------- | ------------------------------------------------------------- |
| `same-gvc`      | Allow access from all workloads in the same GVC (recommended) |
| `same-org`      | Allow access from all workloads in the same organization      |
| `workload-list` | Allow access only from the workloads listed in `workloads`    |

<Note>
  **List only your clients.** Every workload this release creates is always allowed, whatever you set — the three tiers have to reach each other, and each tier's own replicas have to reach each other, so a `workload-list` naming only your applications would otherwise cut the cluster off from itself. Before 2.0.0 the `workloads` list never reached the stored spec at all, so `workload-list` blocked everything.
</Note>

An access-knob change takes time to propagate — up to about ten minutes has been measured across this catalog. Keep re-polling rather than concluding the knob is broken.

**External access:**

* `external_access.server_outboundAllowCIDR` / `tikv_outboundAllowCIDR` / `pd_outboundAllowCIDR` — Outbound CIDR allowlists for each component, for reaching external services. When `backup.enabled` is `true`, TiKV outbound access is automatically set to `0.0.0.0/0` so nodes can upload directly to cloud storage, regardless of `tikv_outboundAllowCIDR`.

**There is no public inbound access.** The TiDB Server workload takes none, and `exposeServer` was removed in 2.0.0 because it never published the MySQL port. Reach the server over internal GVC DNS, or forward the port:

```bash theme={null}
cpln port-forward RELEASE_NAME-server 4000:4000 --gvc GVC_NAME
```

`cpln port-forward` is a top-level command, not a `cpln workload` subcommand, and it works against a workload with no public inbound at all.

### Connecting to TiDB

TiDB Server is MySQL-compatible. Connect using any MySQL client from a workload in the same GVC:

| What                          | Where                                          | Credentials                                                    |
| ----------------------------- | ---------------------------------------------- | -------------------------------------------------------------- |
| MySQL protocol (applications) | `RELEASE_NAME-server.GVC_NAME.cpln.local:4000` | `user` / `password` from the credentials secret; database `db` |
| MySQL protocol (root)         | same                                           | `root` / `rootPassword` from the credentials secret            |
| PD HTTP API (cluster state)   | `RELEASE_NAME-pd.GVC_NAME.cpln.local:2379`     | none — internal only                                           |

```bash theme={null}
mysql -h RELEASE_NAME-server.GVC_NAME.cpln.local -P 4000 -u myuser -p
```

Always use the fully qualified `.GVC_NAME.cpln.local` form. The bare workload name is not reliable on this platform — whether it resolves depends on the workload type.

The `pingcap/tidb` image ships **no** MySQL client, so run the command from another workload in the same GVC — a throwaway `mysql:8` workload works. Depending on how many replicas and locations you configured, the cluster can take a few minutes to accept connections.

### Ports

| Workload    | Port    | Protocol | Description                                                           |
| ----------- | ------- | -------- | --------------------------------------------------------------------- |
| TiDB Server | `4000`  | TCP      | MySQL-compatible SQL port                                             |
| TiDB Server | `10080` | HTTP     | TiDB status and metrics — unauthenticated, and not publicly reachable |
| PD          | `2379`  | TCP      | PD client port                                                        |
| PD          | `2380`  | TCP      | PD peer (Raft) port                                                   |
| TiKV        | `20160` | TCP      | TiKV data port                                                        |
| TiKV        | `20180` | TCP      | TiKV status port                                                      |

## Backup

Backup is disabled by default. When enabled, a cron workload uses TiDB's `br` tool to take a full cluster snapshot on the configured schedule and upload it to AWS S3 or GCS.

* `backup.enabled` — Enable scheduled backups.
* `backup.image` — The backup image. Its version must match the cluster: from v8.5.7, `br` enforces the check even with `--check-requirements=false`, so bump this together with `images.*`.
* `backup.schedule` — Cron expression for backup frequency (default: daily at 2am UTC).
* `backup.provider` — `aws` or `gcp`.
* `backup.location` — The Control Plane location where the backup job runs. It **must be one of your `locations`**, and the chart refuses to render otherwise: the cron is suspended everywhere else, so a mismatch would mean the backup never ran anywhere with no failed run to observe. Set it to the location nearest your bucket to minimize transfer cost and latency.
* `backup.activeDeadlineSeconds` — Maximum time allowed per backup job in seconds (default: `14400` / 4 hours).
* `backup.resources.cpu` / `backup.resources.memory` — Resources for the backup cron container.

<Note>
  When `backup.enabled` is `true`, the template automatically grants TiKV outbound access to `0.0.0.0/0` so nodes can upload data directly to cloud storage. This overrides `external_access.tikv_outboundAllowCIDR`.
</Note>

### AWS S3

Before enabling backup with `provider: aws`, complete the following in your AWS account:

1. Create an S3 bucket. Set `backup.aws.bucket` to its name and `backup.aws.region` to its region.
2. If you do not have a Cloud Account set up, refer to the docs to [Create a Cloud Account](/guides/create-cloud-account). Set `backup.aws.cloudAccountName` to its name.
3. Create an IAM policy with the following JSON, replacing `YOUR_BUCKET_NAME`:

```json theme={null}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListBucket",
                "s3:GetObjectVersion",
                "s3:DeleteObjectVersion",
                "s3:GetBucketLocation",
                "s3:AbortMultipartUpload",
                "s3:ListBucketMultipartUploads",
                "s3:ListMultipartUploadParts"
            ],
            "Resource": [
                "arn:aws:s3:::YOUR_BUCKET_NAME",
                "arn:aws:s3:::YOUR_BUCKET_NAME/*"
            ]
        }
    ]
}
```

4. Set `backup.aws.policyName` to the name of the policy created in step 3.
5. Set `backup.aws.prefix` to the folder path where backups will be stored.

### GCS

Before enabling backup with `provider: gcp`, complete the following in your GCP account:

1. Create a GCS bucket. Set `backup.gcp.bucket` to its name.
2. If you do not have a Cloud Account set up, refer to the docs to [Create a Cloud Account](/guides/create-cloud-account). Set `backup.gcp.cloudAccountName` to its name.
3. Grant the Cloud Account's service account the **Storage Admin** (`roles/storage.admin`) role on that bucket. The chart also binds `roles/storage.objectAdmin` to the workload identity.
4. Set `backup.gcp.prefix` to the folder path where backups will be stored.

<Note>
  **Switch providers with a fresh install, not an upgrade.** An identity's cloud binding is never removed once set — the API merges rather than replaces — so a release switched from `aws` to `gcp` keeps the old provider's binding attached even though the chart no longer renders it.
</Note>

## Restoring a Backup

Backups land at `BUCKET/PREFIX/tidb-TIMESTAMP/`. Restore with `br restore full`, run from a workload **inside the GVC** — `*.cpln.local` names do not resolve from anywhere else, and the `ghcr.io/controlplane-com/backup-images/tidb-backup` image is the one that carries a matching `br`.

**AWS S3:**

```sh theme={null}
br restore full \
  --pd="RELEASE_NAME-pd.GVC_NAME.cpln.local:2379" \
  --storage="s3://BUCKET_NAME/PREFIX/tidb-TIMESTAMP" \
  --s3.region="BUCKET_REGION"
```

**GCS:**

```sh theme={null}
br restore full \
  --pd="RELEASE_NAME-pd.GVC_NAME.cpln.local:2379" \
  --storage="gcs://BUCKET_NAME/PREFIX/tidb-TIMESTAMP"
```

<Warning>
  **This restore path is the upstream procedure and has not been exercised end to end against a backup produced by this template.** In particular, the SST object layout differs between the S3 (`1/<name>`) and GCS (`1_<name>`) backends. Rehearse a restore into a scratch release before you need one.
</Warning>

<Note>
  The `br` binary version must match your TiDB cluster version. Download it from the [TiDB Community Toolkit](https://docs.pingcap.com/tidb/stable/download-ecosystem-tools).
</Note>

## Important Notes

* **Never upgrade a 1.x release onto 2.0.0 in place** — it deletes the GVC the 1.x chart created and everything in it. Install a new release: [Migrating from 1.x](#migrating-from-1-x).
* **The GVC must contain every location you list**, and may contain more. A missing location is not caught at install: PD exits with `FATAL: locations declared in values are not in GVC ...`. A PD member that already holds data logs a `WARNING` instead and keeps serving.
* **PD's replication factor is fixed when the cluster first bootstraps.** It is the number of TiKV nodes you configure, capped at 3, and PD persists it — scaling TiKV up later does not raise it. Start with at least 3 TiKV nodes if you ever want 3-way replication.
* **There is no public access to the MySQL port.** Reach the server over internal GVC DNS or with `cpln port-forward`.
* **Release names must be unique per org** — secrets are org-wide, so two releases with the same name collide even in different GVCs.
* **Credentials apply on first initialization only.** Rotate with `ALTER USER` inside TiDB first, then update the secret, then force a redeployment.

## External References

<CardGroup cols={2}>
  <Card title="TiDB Documentation" icon="book" href="https://docs.pingcap.com/tidb/stable">
    Official TiDB documentation
  </Card>

  <Card title="TiDB Architecture" icon="sitemap" href="https://docs.pingcap.com/tidb/stable/tidb-architecture/">
    How PD, TiKV and TiDB Server fit together
  </Card>

  <Card title="TiDB Community Toolkit" icon="screwdriver-wrench" href="https://docs.pingcap.com/tidb/stable/download-ecosystem-tools">
    Download `br` and other TiDB ecosystem tools
  </Card>

  <Card title="Backup and Restore" icon="rotate-left" href="https://docs.pingcap.com/tidb/stable/backup-and-restore-overview/">
    The BR backup and restore overview
  </Card>

  <Card title="Backup Image Source" icon="github" href="https://github.com/controlplane-com/backup-images/tree/main/tidb-backup">
    Source code for the TiDB backup container image
  </Card>

  <Card title="TiDB Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/tidb">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
