# TiPOC Environment Setup

Complete this page before running `init`.

## 0. What You Need By Deployment Type

Check the deployment type you selected in the [README](README.md) against the table
below. Make sure every item is ready before you continue.

### tiup (local single node)

| # | What you need | How to get it |
| ---: | --- | --- |
| 1 | Go compiler | `go version` (1.21+) |
| 2 | tipoc binary | `cd TiPoC && go build -o tipoc ./cmd/tipoc/` (Linux ARM: add `GOOS=linux GOARCH=arm64`) |
| 3 | Local TiDB instance | `tiup playground v7.5.0` (ensure `127.0.0.1:4000` is reachable) |

### essential (TiDB Cloud, shared TiKV)

In addition to the tiup requirements:

| # | What you need | How to get it |
| ---: | --- | --- |
| 4 | TiDB Cloud cluster | Create in TiDB Cloud console; record the SQL endpoint |
| 5 | EC2 nodes | Count depends on data size; see [§2](#2-prepare-ec2-nodes) |
| 6 | 1 S3 bucket | See [§5](#5-create-s3-bucket-and-prefixes) |
| 7 | EC2 → TiDB connectivity | Public allowlist or PrivateLink; see [§3](#3-public-endpoint-sql) / [§4](#4-privatelink-sql) |
| 8 | TiDB → S3 connectivity | IAM role + S3 permissions; see [§6](#6-ec2-s3-permissions) / [§7](#7-tidb-import-access-to-s3) |

### premium (TiDB Cloud, dedicated TiKV)

Same as essential. EC2 node count depends on data size and target QPS. See
[node-and-time-planning.md](node-and-time-planning.md) for sizing tables.

---

## 1. Choose One SQL Path

| Deployment | SQL path | Required action |
| --- | --- | --- |
| TiUP | local `127.0.0.1:4000` | Start TiUP and verify SQL. |
| Essential | public endpoint or PrivateLink | Verify EC2-to-TiDB SQL. |
| Premium | public endpoint or PrivateLink | Verify EC2-to-TiDB SQL. |

Do not mix public endpoint and PrivateLink in one attempt.

Cloud default:

```bash
TIPOC_PD_ENDPOINT=''
TIPOC_GLOBAL_SORT_URI_REF=target-managed
TIPOC_BR_ENABLED=false
```

Use this default unless the environment owner explicitly provides an approved
PD endpoint, managed backup path, or self-managed Global Sort workspace.

## 2. Prepare EC2 Nodes

Create EC2 nodes in the same region and VPC path as the TiDB SQL endpoint and
S3 bucket when possible.

Each EC2 node needs:

| Item | Requirement |
| --- | --- |
| OS | Amazon Linux 2023 or Ubuntu 22.04 |
| CPU architecture | Match the `tipoc` binary |
| Disk | Enough for logs, receipts, binary, and temporary files |
| Network | Outbound SQL `4000`, S3, SSH, observability endpoints, and generation-worker access to the coordinator prepare port |
| IAM | Prefer an instance role scoped to the attempt S3 prefix |

Verify the fleet binary before any long-running stage. The expected
architecture must match every node, and every node must run the same binary
hash:

```bash
TIPOC_BIN='/data/tipoc/tipoc'
EXPECTED_ARCH='aarch64'       # or x86_64
EXPECTED_SHA256='<sha256>'

uname -m
file "${TIPOC_BIN}"
sha256sum "${TIPOC_BIN}"
test "$(uname -m)" = "${EXPECTED_ARCH}"
test "$(sha256sum "${TIPOC_BIN}" | awk '{print $1}')" = "${EXPECTED_SHA256}"
```

Run this check on every generation and pressure node. Do not start `prepare` or
`run` if any node reports a different architecture, a non-executable binary, or
a different SHA-256 digest.

If creating EC2 nodes with AWS CLI, use this template:

```bash
AWS_REGION='<region>'
AMI_ID='<ami-id>'
INSTANCE_TYPE='<instance-type>'
KEY_NAME='<ec2-keypair-name>'
SUBNET_ID='<subnet-id>'
SECURITY_GROUP_ID='<security-group-id>'
INSTANCE_PROFILE='<instance-profile-name>'
POC_NAME='<poc-name>'

aws ec2 run-instances \
  --region "${AWS_REGION}" \
  --image-id "${AMI_ID}" \
  --instance-type "${INSTANCE_TYPE}" \
  --key-name "${KEY_NAME}" \
  --subnet-id "${SUBNET_ID}" \
  --security-group-ids "${SECURITY_GROUP_ID}" \
  --iam-instance-profile Name="${INSTANCE_PROFILE}" \
  --block-device-mappings '[{"DeviceName":"/dev/xvda","Ebs":{"VolumeSize":200,"VolumeType":"gp3","DeleteOnTermination":true}}]' \
  --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=${POC_NAME}-node-a},{Key=Project,Value=tipoc-poc}]"
```

Get node addresses:

```bash
aws ec2 describe-instances \
  --region "${AWS_REGION}" \
  --filters "Name=tag:Project,Values=tipoc-poc" "Name=instance-state-name,Values=running" \
  --query 'Reservations[].Instances[].{Name:Tags[?Key==`Name`]|[0].Value,InstanceId:InstanceId,PrivateIp:PrivateIpAddress,PublicIp:PublicIpAddress}' \
  --output table
```

**Network setup for private subnets.** If your EC2 nodes were created in a
private subnet (private IPs only, e.g. `172.x.x.x`), you need to give them
internet access for SSH and outbound traffic:

```bash
# 1. Allocate Elastic IPs (one per node)
ALLOC_ID=$(aws ec2 allocate-address --region "${AWS_REGION}" --domain vpc \
  --query 'AllocationId' --output text)

# 2. Associate each EIP with an instance
aws ec2 associate-address --region "${AWS_REGION}" \
  --instance-id <instance-id> --allocation-id "${ALLOC_ID}"

# 3. Ensure the subnet's route table has a default route to an Internet Gateway
#    (otherwise SSH will timeout even with an EIP assigned)
IGW_ID=$(aws ec2 describe-internet-gateways --region "${AWS_REGION}" \
  --filters "Name=attachment.vpc-id,Values=<vpc-id>" \
  --query 'InternetGateways[0].InternetGatewayId' --output text)
RTB_ID=$(aws ec2 describe-route-tables --region "${AWS_REGION}" \
  --filters "Name=vpc-id,Values=<vpc-id>" "Name=association.main,Values=true" \
  --query 'RouteTables[0].RouteTableId' --output text)
aws ec2 create-route --region "${AWS_REGION}" \
  --route-table-id "${RTB_ID}" --destination-cidr-block 0.0.0.0/0 \
  --gateway-id "${IGW_ID}"
```

If the instances were created in a public subnet with `MapPublicIpOnLaunch=true`,
this step is not needed.

Install base tools:

```bash
sudo yum install -y mysql jq awscli rsync tmux tar gzip nc || true
sudo apt-get update && sudo apt-get install -y mysql-client jq awscli rsync tmux tar gzip netcat-openbsd || true
```

Create the remote attempt root on every EC2 node:

```bash
TIPOC_SSH_USER='ec2-user'
TIPOC_SSH_KEY_PATH='/path/to/key.pem'
REMOTE_ROOT='/data/tipoc/<poc-name>/attempt-001'

ssh -i "${TIPOC_SSH_KEY_PATH}" "${TIPOC_SSH_USER}@<node-ip>" \
  "mkdir -p ${REMOTE_ROOT}/{bin,commands,logs,evidence}"
```

Validate SSH:

```bash
ssh -i "${TIPOC_SSH_KEY_PATH}" "${TIPOC_SSH_USER}@<node-ip>" \
  'uname -a && df -h && free -h'
```

For distributed source generation, the coordinator exposes an HTTP prepare
coordination endpoint. Use the coordinator private IP and allow same-cluster
workers to reach it before starting `prepare`.

Default port:

```bash
TIPOC_PREPARE_COORDINATOR_PORT=18080
```

Security-group check:

```bash
AWS_REGION='<region>'
EC2_SG_ID='<ec2-generation-node-security-group-id>'

aws ec2 describe-security-groups \
  --region "${AWS_REGION}" \
  --group-ids "${EC2_SG_ID}" \
  --query 'SecurityGroups[].IpPermissions[?FromPort==`18080` || ToPort==`18080`]'
```

If no same-security-group inbound rule exists for TCP `18080`, add one before
the run. This is required even when SSH works; SSH on port `22` does not prove
workers can reach the prepare coordinator.

```bash
aws ec2 authorize-security-group-ingress \
  --region "${AWS_REGION}" \
  --group-id "${EC2_SG_ID}" \
  --protocol tcp \
  --port 18080 \
  --source-group "${EC2_SG_ID}"
```

After the coordinator process is running, validate from every non-coordinator
node:

```bash
COORDINATOR_PRIVATE_IP='<coordinator-private-ip>'
nc -vz "${COORDINATOR_PRIVATE_IP}" 18080
```

## 3. Public Endpoint SQL

Use this path when TiDB Cloud allows public SQL access.

Steps:

1. Get the TiDB SQL public hostname.
2. Find every EC2 egress IP or NAT Gateway EIP.
3. Add those IPs to the TiDB Cloud allowlist.
4. Validate DNS, TCP, and SQL login from every EC2 node.

Commands from each EC2 node:

```bash
SQL_HOST='<public-sql-host>'
SQL_PORT='4000'

curl -fsS https://checkip.amazonaws.com
getent hosts "${SQL_HOST}" || nslookup "${SQL_HOST}"
nc -vz "${SQL_HOST}" "${SQL_PORT}"
mysql --ssl-mode=REQUIRED \
  -h "${SQL_HOST}" \
  -P "${SQL_PORT}" \
  -u "${TIDB_USER}" \
  -p \
  -e 'select tidb_version();'
```

Wrapper binding:

```bash
TIPOC_SQL_ENDPOINT='<public-sql-host>:4000'
TIPOC_TIDB_CREDENTIALS_REF='env:TIDB_DSN'
```

## 4. PrivateLink SQL

Use this path when EC2 nodes connect to TiDB Cloud through AWS PrivateLink.

Steps:

1. Create or identify an Interface VPC Endpoint in the same VPC path as the EC2
   generation and pressure nodes.
2. Register the generated `vpce-...` endpoint id in TiDB Cloud, or ask the TiDB
   Cloud owner to accept the endpoint connection.
3. Wait until the endpoint is `available`.
4. Enable private DNS after the endpoint has been accepted, or use one of the
   AWS endpoint DNS names directly as the SQL host.
5. Put EC2 nodes in subnets that can reach the endpoint.
6. Allow outbound TCP `4000` from EC2 security groups.
7. Allow inbound TCP `4000` to the endpoint security group from EC2.
8. Validate DNS, TCP, and SQL login from every EC2 node.

Do not derive the SQL host from the endpoint id. The endpoint service name, the
endpoint id, and an `Active` or `available` state are not connectable SQL hosts
by themselves. Use the private DNS name only after it resolves from the EC2 VPC,
or use one of the `DnsEntries` returned by `describe-vpc-endpoints`.

If creating the endpoint with AWS CLI, use the TiDB Cloud provided service
name. If the service requires owner acceptance, create the endpoint without
private DNS first:

```bash
AWS_REGION='<region>'
VPC_ID='<vpc-id>'
SUBNET_IDS='<subnet-a> <subnet-b>'
ENDPOINT_SG_ID='<endpoint-security-group-id>'
TIDB_PRIVATELINK_SERVICE_NAME='<service-name-from-tidb-cloud>'

aws ec2 create-vpc-endpoint \
  --region "${AWS_REGION}" \
  --vpc-endpoint-type Interface \
  --vpc-id "${VPC_ID}" \
  --service-name "${TIDB_PRIVATELINK_SERVICE_NAME}" \
  --subnet-ids ${SUBNET_IDS} \
  --security-group-ids "${ENDPOINT_SG_ID}" \
  --no-private-dns-enabled \
  --query 'VpcEndpoint.{EndpointId:VpcEndpointId,State:State,Dns:DnsEntries[*].DnsName}' \
  --output table
```

Copy the returned `EndpointId` and register or approve it in TiDB Cloud. Then
check endpoint state:

```bash
VPC_ENDPOINT_ID='<vpce-id>'

aws ec2 describe-vpc-endpoints \
  --region "${AWS_REGION}" \
  --vpc-endpoint-ids "${VPC_ENDPOINT_ID}" \
  --query 'VpcEndpoints[].{EndpointId:VpcEndpointId,State:State,PrivateDns:PrivateDnsEnabled,Dns:DnsEntries[*].DnsName}' \
  --output table
```

When the endpoint is `available`, enable private DNS if the environment owner
expects the TiDB Cloud private hostname to resolve inside the VPC:

```bash
aws ec2 modify-vpc-endpoint \
  --region "${AWS_REGION}" \
  --vpc-endpoint-id "${VPC_ENDPOINT_ID}" \
  --private-dns-enabled
```

Allow SQL traffic from the EC2 node security group to the endpoint security
group. If the same security group is attached to both EC2 nodes and the endpoint,
use a same-security-group inbound rule:

```bash
EC2_SG_ID='<ec2-node-security-group-id>'

aws ec2 authorize-security-group-ingress \
  --region "${AWS_REGION}" \
  --group-id "${ENDPOINT_SG_ID}" \
  --protocol tcp \
  --port 4000 \
  --source-group "${EC2_SG_ID}"
```

Commands from each EC2 node:

```bash
SQL_HOST='<privatelink-sql-host>'
SQL_PORT='4000'

getent hosts "${SQL_HOST}" || nslookup "${SQL_HOST}"
nc -vz "${SQL_HOST}" "${SQL_PORT}"
mysql --ssl-mode=REQUIRED \
  -h "${SQL_HOST}" \
  -P "${SQL_PORT}" \
  -u "${TIDB_USER}" \
  -p \
  -e 'select tidb_version();'
```

If PrivateLink is expected but DNS resolves to a public route, stop and fix
DNS or use an explicit AWS VPC endpoint DNS name. If private DNS does not resolve
yet but an AWS endpoint DNS name resolves and TCP `4000` passes from every EC2
node, record the endpoint DNS name used for `TIPOC_SQL_ENDPOINT`.

Wrapper binding:

```bash
TIPOC_SQL_ENDPOINT='<privatelink-sql-host>:4000'
TIPOC_TIDB_CREDENTIALS_REF='env:TIDB_DSN'
```

### 4.1 SQL Credential Materialization For Remote Runs

`env:TIDB_DSN` is a secret reference, not the secret itself. Before a remote
Cloud prepare or run consumes that reference, a human or approved secret
automation must materialize the credential on the machine that starts `tipoc`.
Do not paste the DSN into chat, manifests, receipts, issue text, shell history,
or command arguments.

For a coordinator-owned remote run, use a secret file path under the attempt
root and have the launcher export `TIDB_DSN` from that file without printing it:

```bash
TIPOC_SSH_USER='ec2-user'
TIPOC_SSH_KEY_PATH='/path/to/key.pem'
COORDINATOR_HOST='<coordinator-ssh-host>'
REMOTE_ROOT='/data/tipoc/<run-id>/attempt-001'

ssh -i "${TIPOC_SSH_KEY_PATH}" "${TIPOC_SSH_USER}@${COORDINATOR_HOST}" \
  "umask 077 && mkdir -p '${REMOTE_ROOT}/secrets'"

read -r -s -p 'TiDB DSN: ' TIPOC_DSN
printf '\n'
printf '%s\n' "${TIPOC_DSN}" | \
  ssh -i "${TIPOC_SSH_KEY_PATH}" "${TIPOC_SSH_USER}@${COORDINATOR_HOST}" \
    "cat > '${REMOTE_ROOT}/secrets/tidb-dsn' && chmod 600 '${REMOTE_ROOT}/secrets/tidb-dsn'"
unset TIPOC_DSN

ssh -i "${TIPOC_SSH_KEY_PATH}" "${TIPOC_SSH_USER}@${COORDINATOR_HOST}" \
  "test -s '${REMOTE_ROOT}/secrets/tidb-dsn' && echo dsn_secret=present"
```

The DSN must point at the SQL host selected in the public-endpoint or
PrivateLink section. For PrivateLink, use the TiDB Cloud private hostname only
after it resolves from the EC2 VPC, or use the AWS endpoint DNS name that passed
TCP `4000` from every EC2 node.

Minimum non-printing validation:

1. Confirm the secret reference exists without printing its content.
2. Run a SQL probe that executes `SELECT 1` and `SELECT tidb_version()` without
   echoing the DSN.
3. If import will read S3, run the small import probe or record explicit owner
   proof before starting `prepare --ops import` or `prepare --ops import,add-index`.

## 5. Create S3 Bucket And Prefixes

Cloud prepare needs two S3 access paths:

1. EC2 generation nodes write generated CSV source objects.
2. TiDB import service reads the same source objects.

Create the bucket:

```bash
AWS_REGION='<region>'
BUCKET='<globally-unique-bucket-name>'

aws s3api create-bucket \
  --bucket "${BUCKET}" \
  --region "${AWS_REGION}" \
  --create-bucket-configuration LocationConstraint="${AWS_REGION}"
```

For `us-east-1`:

```bash
aws s3api create-bucket \
  --bucket "${BUCKET}" \
  --region us-east-1
```

Block public access and enable default encryption:

```bash
aws s3api put-public-access-block \
  --bucket "${BUCKET}" \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

aws s3api put-bucket-encryption \
  --bucket "${BUCKET}" \
  --server-side-encryption-configuration \
  '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
```

Create attempt prefixes:

```bash
DEPLOYMENT='<essential-or-premium>'
RUN_ID='<run-id>'
ATTEMPT_ID='attempt-001'
BASE="tipoc/${DEPLOYMENT}/${RUN_ID}/${ATTEMPT_ID}"

for prefix in source br scratch evidence; do
  aws s3api put-object \
    --bucket "${BUCKET}" \
    --key "${BASE}/${prefix}/.keep" \
    --body /dev/null
done
```

Wrapper binding:

```bash
TIPOC_OBJECT_STORE_KIND='s3'
TIPOC_OBJECT_STORE_URI="s3://${BUCKET}/${BASE}"
TIPOC_OBJECT_STORE_CREDENTIAL_REF='env:AWS_PROFILE'
TIPOC_GLOBAL_SORT_URI_REF=target-managed
AWS_PROFILE='<profile-name>'
AWS_REGION='<region>'
```

For Cloud target-managed Global Sort, keep `TIPOC_GLOBAL_SORT_URI_REF` set to
`target-managed` so the compiled manifest leaves `prepare.global_sort_uri_ref`
empty. Use a user-side base Global Sort workspace URI only for a self-managed
environment that explicitly requires it.

## 6. EC2 S3 Permissions

Use an EC2 instance role when possible. Scope it to the attempt prefix.

Example policy:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::<bucket>",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["tipoc/<deployment>/<run-id>/<attempt-id>/*"]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:AbortMultipartUpload",
        "s3:ListMultipartUploadParts",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::<bucket>/tipoc/<deployment>/<run-id>/<attempt-id>/*"
    }
  ]
}
```

Validate from the coordinator and generation nodes:

```bash
aws s3 ls "s3://${BUCKET}/${BASE}/"
echo probe > /tmp/tipoc-s3-probe.txt
aws s3 cp /tmp/tipoc-s3-probe.txt "s3://${BUCKET}/${BASE}/source/probe.txt"
aws s3 cp "s3://${BUCKET}/${BASE}/source/probe.txt" -
aws s3 rm "s3://${BUCKET}/${BASE}/source/probe.txt"
```

Validate multipart upload:

```bash
dd if=/dev/zero of=/tmp/tipoc-multipart-probe.bin bs=1M count=64
aws s3 cp /tmp/tipoc-multipart-probe.bin "s3://${BUCKET}/${BASE}/source/multipart-probe.bin"
aws s3 rm "s3://${BUCKET}/${BASE}/source/multipart-probe.bin"
rm -f /tmp/tipoc-multipart-probe.bin
```

## 7. TiDB Import Access To S3

EC2 S3 access is not enough. TiDB must also read the generated source objects
during `IMPORT INTO ... FROM FILE FORMAT 'CSV'`.

For TiPOC prepare, treat `source/` as the attempt-level parent prefix. The
generated files are organized under table-level child prefixes, for example
`source/<table>/...`; a table can have many source objects. Do not describe this
layout as one source file per table. IAM, TiDB Cloud external storage access, and
probes should cover the parent `source/` prefix, while each table import uses
the table's child prefix or object pattern.

Before `prepare --ops import` or `prepare --ops import,add-index`, record one
proof for the exact attempt source prefix and its table child prefixes:

| Proof type | Acceptable evidence |
| --- | --- |
| TiDB Cloud external storage access | The TiDB Cloud external storage access configuration validates against the exact bucket and attempt source prefix using an approved Role ARN or AWS access key. |
| Credential-bearing import URI | The object-store URI used by TiDB includes approved credentials whose lifetime covers the full import and index window plus the approved safety margin. |
| Small import probe | A small `IMPORT INTO` probe succeeds from the same bucket, prefix class, region, and credential mode. |
| Owner proof | The service owner confirms TiDB can list and read the exact attempt source prefix, including table child prefixes, and records the source of that confirmation. |

Preferred large-import mode is TiDB Cloud AWS Role ARN access with the TiDB
Cloud-provided external ID. Use Role ARN mode for long `IMPORT INTO` and
`add-index` windows because per-run STS query credentials can expire before all
tables finish. A bucket policy or EC2 instance role alone is not enough under
TiDB security enhanced mode when the import URI is a naked `s3://...` path
without credential fields or `role-arn`; that mode rejects S3-like imports that
do not carry an access key/secret key pair or an approved Role ARN.

Supported operator paths:

1. Manual UI-guided setup: open the TiDB Cloud Import page for the target
   deployment, follow the page's permission/configuration workflow, and record
   only redacted evidence of the TiDB Cloud AWS account ID, external ID, role
   validation result, bucket, parent source prefix, and region. The AWS admin or
   environment owner creates the IAM role requested by the workflow, or
   explicitly confirms that the workflow-generated role for this import attempt
   remains valid.
   Treat Role ARN values as attempt-scoped unless the environment owner states
   that the role is durable; do not reuse a historical Role ARN by assumption.
2. AI/automation setup: with explicit AWS admin approval, create or update an
   IAM role whose trust policy allows the TiDB Cloud AWS account ID and requires
   the TiDB Cloud external ID. Attach read-only source-prefix permissions for
   `s3:ListBucket` and `s3:GetObject`. The current coordinator role may lack
   `iam:*` and `cloudformation:*`; in that case use an AWS admin/operator or a
   preconfigured role from the environment owner instead of attempting local
   mutation.

In both paths, the operator must obtain the TiDB Cloud-provided AWS account ID
and external ID, validate a role with read-only access to the exact attempt
source prefix, and then use the import URI shape accepted by the target cluster's
security mode. Treat the external ID as part of the TiDB Cloud UI permission
workflow and AWS role trust relationship unless a probe proves the product
accepts an explicit SQL URI parameter for the target cluster. Security enhanced
mode has live evidence rejecting explicit `external-id` in an `IMPORT INTO` URI,
so do not assume that parameter is legal in SEM. For every new import attempt,
reopen the TiDB Cloud Import page or automation evidence and confirm the current
Role ARN before generating source URIs.

Probe the exact target cluster before the full import and record only redacted
URI shapes and pass/fail results. Candidate shapes:

```text
# SEM-compatible candidate after TiDB Cloud UI/trust setup:
s3://<bucket>/tipoc/<deployment>/<run-id>/<attempt-id>/source/<table>/<part-or-pattern>.csv?region=<aws-region>&role-arn=<ROLE_ARN>

# Non-SEM or product-accepted explicit external ID shape:
s3://<bucket>/tipoc/<deployment>/<run-id>/<attempt-id>/source/<table>/<part-or-pattern>.csv?region=<aws-region>&role-arn=<ROLE_ARN>&external-id=<EXTERNAL_ID>
```

Use a separate proof for each access plane:

| Plane | Required access |
| --- | --- |
| EC2 generation nodes | List the attempt prefix, write source objects, multipart upload, read probe objects, delete probe objects. |
| TiDB import worker | List the bucket under the attempt source prefix and read every generated source object under the table child prefixes used by `IMPORT INTO`. |
| TiDB self-managed Global Sort, only when configured | Write, read, list, and delete under the configured scratch prefix used by `CLOUD_STORAGE_URI`. |

For Cloud target-managed Global Sort, keep `TIPOC_GLOBAL_SORT_URI_REF` set to
`target-managed` and confirm the compiled manifest leaves
`prepare.global_sort_uri_ref` empty. In that mode, do not invent a user-side
scratch prefix. For self-managed Global Sort, the TiDB import worker must also
have scratch access before import starts.

Minimum S3 permissions for the TiDB import-worker source read path:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::<bucket>",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["tipoc/<deployment>/<run-id>/<attempt-id>/source/*"]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject"],
      "Resource": "arn:aws:s3:::<bucket>/tipoc/<deployment>/<run-id>/<attempt-id>/source/*"
    }
  ]
}
```

For self-managed Global Sort scratch, add the scratch-prefix permissions only
when `prepare.global_sort_uri_ref` is not empty:

```json
{
  "Effect": "Allow",
  "Action": ["s3:ListBucket"],
  "Resource": "arn:aws:s3:::<bucket>",
  "Condition": {
    "StringLike": {
      "s3:prefix": ["tipoc/<deployment>/<run-id>/<attempt-id>/scratch/*"]
    }
  }
},
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"],
  "Resource": "arn:aws:s3:::<bucket>/tipoc/<deployment>/<run-id>/<attempt-id>/scratch/*"
}
```

For TiDB Cloud external storage access, use the configuration path exposed by
the TiDB Cloud console for the target deployment. On AWS-backed TiDB Cloud
targets, the access mode is normally either an approved Role ARN or an AWS access
key. The IAM principal, external id, Role ARN, access-key owner, or integration
name must come from the TiDB Cloud console or the environment owner. Do not
guess it from the cluster id.

If the source URI uses query-string credentials such as `access-key`,
`secret-access-key`, or `session-token`, do not print the URI. Record only the
bucket, prefix, region, credential reference, and expiration time. The expiration
must be later than the expected end of the full `import,add-index` window plus
the approved safety margin. Expired, unknown, or too-short credential TTL blocks
import. For long imports, replace temporary query credentials with Role ARN mode
before launch.

Small import probe pattern:

1. Write a tiny CSV under a disposable source-prefix child path.
2. Create a disposable probe table in the target database.
3. Run `IMPORT INTO` against that probe object using the same credential mode
   and URI parameter shape planned for the full import.
4. Verify the expected row count.
5. Drop the probe table and delete the probe object after evidence is recorded.

Template:

```sql
CREATE TABLE IF NOT EXISTS tipoc_import_probe (
  id BIGINT PRIMARY KEY,
  note VARCHAR(32)
);

IMPORT INTO tipoc_import_probe (id, note)
FROM 's3://<bucket>/tipoc/<deployment>/<run-id>/<attempt-id>/source/_probe/probe.csv?<redacted-or-approved-query>'
FORMAT 'CSV'
WITH
  SKIP_ROWS = 1;

SELECT COUNT(*) FROM tipoc_import_probe;
DROP TABLE tipoc_import_probe;
```

Do not store the raw probe SQL if it contains query-string credentials. Store a
redacted transcript plus the row-count result.

For TiDB Cloud Role ARN mode, record these non-secret fields:

| Field | Example |
| --- | --- |
| AWS role ARN | `arn:aws:iam::<account-id>:role/<role-name>` |
| Bucket and prefix | `s3://<bucket>/tipoc/<deployment>/<run-id>/<attempt-id>/source/` |
| Region | `<region>` |
| External id or trust binding | Reference to the TiDB Cloud generated trust condition, not the secret value |
| Validation receipt | Screenshot id, UI task id, or operator note that validation passed |

The role may be generated for one import workflow and recreated for a later
attempt. Treat the ARN as an environment binding, not as a reusable product
default. Keep the TiPOC generated source layout unchanged: TPC-E source objects
are table-scoped under the attempt source prefix, and TiPOC generated import
SQL may point each table at its own table prefix. Do not flatten or move source
objects only to match a UI flow that expects all files under one prefix unless
the import plan is regenerated and reviewed.

If TiDB cannot read the source prefix, stop. Do not replace import with
`INSERT`, `LOAD DATA`, local upload, or manual table fill.

## 8. Observability

Clinic and Loki are for evidence collection. They are not product inputs to
`tipoc init`, `tipoc prepare`, or `tipoc run`.

Record these when available:

| Binding | Example |
| --- | --- |
| Clinic cluster id | `<cluster-id>` |
| Clinic credential reference | `env:CLINIC_API_KEY` |
| Clinic environment | `prod` or approved target |
| Loki/Grafana access | Explore URL, redacted cURL, or exported JSON |
| TiUP Prometheus | `http://<host>:9090` |
| TiUP Grafana | `http://<host>:3000` |

For TiUP:

```bash
curl -fsS http://<tiup-host>:9090/-/ready
curl -fsS http://<tiup-host>:3000/api/health
```

For Cloud, collect at least SQL latency, TiDB CPU, TiKV CPU, TiDB-to-TiKV
latency, Commit/Prewrite latency, backoff, region errors, and slow queries
when those signals are available.

## 9. Pre-init Checklist

Do not run `init` until every applicable item is done:

| Check | Done |
| --- | --- |
| EC2 nodes are created and reachable by SSH. |  |
| SQL DNS, TCP `4000`, and TLS SQL login pass from every EC2 node. |  |
| Public allowlist or PrivateLink route is proven. |  |
| S3 bucket and attempt prefixes exist. |  |
| EC2 can write, read, delete, and multipart-upload under the source prefix. |  |
| TiDB import service can read the source prefix. |  |
| Cloud SQL-only POC has `TIPOC_PD_ENDPOINT=''` and `TIPOC_BR_ENABLED=false`. |  |
| Generation and pressure node lists are finalized. |  |
| Cleanup policy is written down. |  |

---

**Next**: Environment ready? Go to [deployment-poc-sop.md](deployment-poc-sop.md) and start with `render-only`, then `init`.
