Skip to content

Bootstrap

First-time setup: provision namespaces, secrets, Argo CD, and trigger initial sync.

Purpose

Run once per cluster lifecycle (or after full teardown + rebuild). Gets the cluster from empty DOKS to a state where Argo CD is running and all Applications are Synced/Healthy.

When to use

  • First deploy to a new cluster
  • Full cluster rebuild after teardown
  • After cluster upgrade that requires re-bootstrapping

Step 1: Generate Doppler admin token

The Doppler Terraform provider needs a Personal Token (or Service Account token on Team/Enterprise plan) to create and manage the oep project structure. Service Tokens are read-only and tied to a single config - they cannot create projects or environments.

Generate a Personal Token (all plans)

  1. Log in at dashboard.doppler.com
  2. Click your avatar (top-right) → Personal Settings
  3. Select Tokens in the left sidebar
  4. Click Generate token → name it terraform-oep-infra → click Generate

Copy the token value. It is shown only once.

export TF_VAR_doppler_admin_token="dp.pt.xxxx..."

Generate a Service Account token (Team/Enterprise plans - preferred)

Service Accounts are dedicated machine identities - not tied to any person.

  1. Go to Workplace Settings (cog icon, top-left) → Service Accounts
  2. Click Add Service Account → name it terraform-oep-infra
  3. Set Workplace Role to Workplace Admin
  4. Generate a token under the service account → copy the value
export TF_VAR_doppler_admin_token="dp.sa.xxxx..."

For CI: add the token value as a GitHub org secret named DOPPLER_ADMIN_TOKEN.


Step 2: Generate Doppler service tokens

Three read-only Service Tokens, one per tenant environment. Used by ESO ClusterSecretStore to sync secrets from Doppler into Kubernetes Secrets.

  1. In dashboard.doppler.com, open project oep
  2. For each config (oep-stg, mansety-prd, us-prd):
  3. Click the config name → Access tab → Service TokensGenerate
  4. Name: k8s-<env>-<YYYY-MM> (e.g. k8s-oep-stg-2026-05)
  5. Scope: Read-Only
  6. Copy the token (shown once)

Store each token in three places:

Location Name Purpose
GH repo secret (unipuka/soa) DOPPLER_SERVICE_TOKEN_OEP_STG Used by soa CI workflows (doppler run)
GH org secret DOPPLER_SERVICE_TOKEN_OEP_STG Passed to TF plan/apply as TF_VAR_doppler_service_token_oep_stg
Doppler dashboard (source of truth) Rotation reference
export TF_VAR_doppler_service_token_oep_stg="dp.st.xxxx..."
export TF_VAR_doppler_service_token_mansety_prd="dp.st.xxxx..."
export TF_VAR_doppler_service_token_us_prd="dp.st.xxxx..."

For CI: add DOPPLER_ADMIN_TOKEN, DOPPLER_SERVICE_TOKEN_OEP_STG, DOPPLER_SERVICE_TOKEN_MANSETY_PRD, DOPPLER_SERVICE_TOKEN_US_PRD as GitHub org secrets. These are mapped to TF_VAR_* in the reusable plan/apply workflows.

Rotation runbook: rotate-secrets.md


Step 3: Set up Cloudflare Access IdPs (first-time only)

Before running terraform apply for the first time, configure the Cloudflare Zero Trust Identity Providers and extend the Cloudflare API token scope. This is required for docs_site.tf to apply without errors.

See Cloudflare Access IdP Setup for full instructions.


Step 4: Create Argo CD GitHub App

Argo CD uses a GitHub App (not a PAT) to clone unipuka-infra-ops. GitHub Apps have no expiry and support fine-grained repo permissions.

4.1 Create the app

  1. Go to github.com/organizations/unipuka/settings/apps/new
  2. Fill in:
  3. GitHub App name: unipuka-argocd
  4. Homepage URL: https://argocd.unipuka.app
  5. Webhook: uncheck "Active" (Argo CD uses polling, not webhooks)
  6. Repository permissions: Contents = Read-only, Metadata = Read-only
  7. Where can this be installed: Only on this account
  8. Click Create GitHub App

4.2 Capture App ID and generate private key

After creation:

  1. Copy the App ID from the app settings page (numeric, e.g. 12345678)
  2. Scroll to Private keysGenerate a private key
  3. Download the .pem file

4.3 Install the app

  1. On the app settings page → Install App → install on the unipuka org
  2. Select Only select repositories → choose unipuka-infra-ops
  3. Click Install
  4. After install, the URL ends with .../installations/<installationId> - copy the number

4.4 Export variables

export TF_VAR_argocd_github_app_id="12345678"
export TF_VAR_argocd_github_app_installation_id="987654321"
export TF_VAR_argocd_github_app_private_key="$(cat /path/to/unipuka-argocd.pem)"

For CI: add ARGOCD_GITHUB_APP_ID, ARGOCD_GITHUB_APP_INSTALLATION_ID, ARGOCD_GITHUB_APP_PRIVATE_KEY as GitHub org secrets.


Step 5: Run terraform apply

Prerequisites:

  • [ ] doctl kubernetes cluster kubeconfig save oep-prd-cluster - kubeconfig set
  • [ ] doppler_admin_token exported (Step 1)
  • [ ] 3x Doppler service tokens exported (Step 2)
  • [ ] Cloudflare Access IdPs configured + UUIDs in Doppler (Step 3)
  • [ ] cf_account_id filled in tenants.tf (from Step 3)
  • [ ] Argo CD GitHub App vars exported (Step 4)
  • [ ] terraform.tfvars populated (copy from terraform.tfvars.example)
  • [ ] .envrc populated with Spaces backend creds (direnv allow)
cd oep-infra

# Only needed first time or after provider version changes
direnv exec . terraform init

# Review changes
direnv exec . terraform plan

# Apply
direnv exec . terraform apply

Step 6: Verify

# Namespaces with labels
kubectl get ns --show-labels

# ESO ClusterSecretStores valid
kubectl get clustersecretstores

# DOCR pull secret present in all tenant namespaces
for ns in oep-stg mansety-prd us-prd external-secrets monitoring; do
  kubectl get secret do-registry -n $ns -o name
done

# Argo CD reachable (port-forward if HTTPRoute not yet set up)
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Open https://localhost:8080 in browser

# All applications healthy
argocd app list

Rollback

TF bootstrap is additive - existing cluster is unchanged.

  • Remove specific resources: terraform destroy -target=kubernetes_namespace.tenants
  • Argo CD removal: helm uninstall argocd -n argocd

V&V

  • argocd app list shows root, platform - both Synced/Healthy
  • Subsequent terraform apply produces an empty diff
  • kubectl get ns lists all 6 namespaces with labels
  • kubectl get clustersecretstores shows 3 stores all Valid