name: ambient description: >- Install and verify Ambient Code Platform on an OpenShift cluster using quay.io images. Use when deploying Ambient to any OpenShift namespace — production, ephemeral PR test instances, or developer clusters. Covers secrets, kustomize deploy, rollout verification, and troubleshooting.
Ambient Installer Skill
You are an expert in deploying the Ambient Code Platform to OpenShift clusters. This skill covers everything needed to go from an empty namespace to a running Ambient installation using images from quay.io.
Developer registry override: If you need to use images from the OpenShift internal registry instead of quay.io (e.g. for local dev builds), see
docs/internal/developer/local-development/openshift.md.
Platform Components
| Deployment | Image | Purpose |
|---|---|---|
backend-api |
quay.io/ambient_code/vteam_backend |
Go REST API, manages K8s CRDs |
frontend |
quay.io/ambient_code/vteam_frontend |
NextJS web UI |
agentic-operator |
quay.io/ambient_code/vteam_operator |
Kubernetes operator |
ambient-api-server |
quay.io/ambient_code/vteam_api_server |
Stateless API server |
ambient-api-server-db |
(postgres sidecar) | API server database |
public-api |
quay.io/ambient_code/vteam_public_api |
External API gateway |
postgresql |
(upstream) | Unleash feature flag DB |
minio |
(upstream) | S3 object storage |
unleash |
(upstream) | Feature flag service |
Runner pods (vteam_claude_runner, vteam_state_sync) are spawned dynamically by the operator — they are not standing deployments.
Credential sidecar containers are injected into session pods when the corresponding credential type is configured:
< /dev/null | Sidecar Container | Image | Port | Provider |
|-------------------|-------|------|----------|
| credential-github | quay.io/ambient_code/vteam_credential_github | 8091 | GitHub PAT / App |
| credential-jira | quay.io/ambient_code/vteam_credential_jira | 8092 | Jira / Atlassian |
| credential-k8s | quay.io/ambient_code/vteam_credential_k8s | 8093 | Kubeconfig |
| credential-google | quay.io/ambient_code/vteam_credential_google | 8094 | Google Workspace |
Prerequisites
ocCLI installed and logged in to the target clusterkustomizeinstalled (curl -s https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh | bash)- Target namespace already exists and is Active
- Quay.io images are accessible from the cluster (public repos or image pull secret in place)
Step 1: Apply CRDs and RBAC (cluster-scoped, once per cluster)
oc apply -k components/manifests/base/crds/
oc apply -k components/manifests/base/rbac/
These are idempotent. On a shared cluster where CRDs already exist from another namespace, this is safe to re-run.
Step 2: Create Required Secrets
All secrets must exist before applying the kustomize overlay. The deployment will fail if any are missing.
NAMESPACE=<target-namespace>
oc create secret generic minio-credentials -n $NAMESPACE \
--from-literal=root-user=<MINIO_ROOT_USER> \
--from-literal=root-password=<MINIO_ROOT_PASSWORD>
oc create secret generic postgresql-credentials -n $NAMESPACE \
--from-literal=db.host=postgresql \
--from-literal=db.port=5432 \
--from-literal=db.name=postgres \
--from-literal=db.user=postgres \
--from-literal=db.password=<POSTGRES_PASSWORD>
oc create secret generic unleash-credentials -n $NAMESPACE \
--from-literal=database-url=postgres://postgres:<POSTGRES_PASSWORD>@postgresql:5432/unleash \
--from-literal=database-ssl=false \
--from-literal=admin-api-token='*:*.<UNLEASH_ADMIN_TOKEN>' \
--from-literal=client-api-token=default:development.<UNLEASH_CLIENT_TOKEN> \
--from-literal=frontend-api-token=default:development.<UNLEASH_FRONTEND_TOKEN> \
--from-literal=default-admin-password=<UNLEASH_ADMIN_PASSWORD>
oc create secret generic github-app-secret -n $NAMESPACE \
--from-literal=GITHUB_APP_ID="<GITHUB_APP_ID>" \
--from-literal=GITHUB_PRIVATE_KEY="<GITHUB_PRIVATE_KEY>" \
--from-literal=GITHUB_CLIENT_ID="<GITHUB_CLIENT_ID>" \
--from-literal=GITHUB_CLIENT_SECRET="<GITHUB_CLIENT_SECRET>" \
--from-literal=GITHUB_STATE_SECRET=<GITHUB_STATE_SECRET>
Use --dry-run=client -o yaml | oc apply -f - to make secret creation idempotent on re-runs.
Credential Encryption Key (required for production)
Credential tokens are encrypted at rest with AES-256-GCM. Generate a key and create the secret:
ENCRYPTION_KEY=$(openssl rand -base64 32)
oc create secret generic credential-encryption-key -n $NAMESPACE \
--from-literal=keyring="{\"1\":\"$ENCRYPTION_KEY\"}" \
--from-literal=version=1
The hcmais overlay mounts this as CREDENTIAL_ENCRYPTION_KEYRING and CREDENTIAL_ENCRYPTION_KEY_VERSION. After first deploy, encrypt existing tokens:
oc exec deploy/ambient-api-server -n $NAMESPACE -- ambient-api-server encrypt-credentials
To rotate: add new key to keyring JSON, bump version, restart, re-run encrypt-credentials. To skip in dev: set CREDENTIAL_ENCRYPTION_ALLOW_PLAINTEXT=true. See specs/security/credential-encryption.spec.md.
Anthropic API Key (required for runner pods)
oc create secret generic ambient-runner-secrets -n $NAMESPACE \
--from-literal=ANTHROPIC_API_KEY=<key>
Vertex AI (optional, instead of direct Anthropic)
oc create secret generic ambient-vertex -n $NAMESPACE \
--from-file=ambient-code-key.json=/path/to/service-account-key.json
If using Vertex, set USE_VERTEX=1 in the operator ConfigMap (see Step 4).
Step 3: Deploy with Kustomize
Scripted (preferred for ephemeral/PR namespaces)
components/pr-test/install.sh encapsulates Steps 2–6 into a single script. It copies secrets from the source namespace, deploys via a temp-dir kustomize overlay (no git working tree mutations), patches configmaps, and waits for rollouts:
bash components/pr-test/install.sh <namespace> <image-tag>
Production deploy (make deploy)
For the production namespace (ambient-code), use:
make deploy
# calls components/manifests/deploy.sh — handles OAuth, restores kustomization after apply
deploy.sh mutates kustomization.yaml in-place and restores it post-apply. It also handles the OpenShift OAuth OAuthClient (requires cluster-admin). Use make deploy only for the canonical production namespace.
Manual (for debugging or one-off namespaces)
Use a temp dir to avoid modifying the git working tree:
IMAGE_TAG=<tag> # e.g. latest, pr-42-amd64, abc1234
NAMESPACE=<target-namespace>
TMPDIR=$(mktemp -d)
cp -r components/manifests/overlays/production/. "$TMPDIR/"
pushd "$TMPDIR"
kustomize edit set namespace $NAMESPACE
kustomize edit set image \
quay.io/ambient_code/vteam_frontend:latest=quay.io/ambient_code/vteam_frontend:$IMAGE_TAG \
quay.io/ambient_code/vteam_backend:latest=quay.io/ambient_code/vteam_backend:$IMAGE_TAG \
quay.io/ambient_code/vteam_operator:latest=quay.io/ambient_code/vteam_operator:$IMAGE_TAG \
quay.io/ambient_code/vteam_claude_runner:latest=quay.io/ambient_code/vteam_claude_runner:$IMAGE_TAG \
quay.io/ambient_code/vteam_state_sync:latest=quay.io/ambient_code/vteam_state_sync:$IMAGE_TAG \
quay.io/ambient_code/vteam_api_server:latest=quay.io/ambient_code/vteam_api_server:$IMAGE_TAG \
quay.io/ambient_code/vteam_public_api:latest=quay.io/ambient_code/vteam_public_api:$IMAGE_TAG
oc apply -k . -n $NAMESPACE
oc set env deployment/ambient-control-plane -n $NAMESPACE \
GITHUB_MCP_IMAGE=quay.io/ambient_code/vteam_credential_github:$IMAGE_TAG \
JIRA_MCP_IMAGE=quay.io/ambient_code/vteam_credential_jira:$IMAGE_TAG \
K8S_MCP_IMAGE=quay.io/ambient_code/vteam_credential_k8s:$IMAGE_TAG \
GOOGLE_MCP_IMAGE=quay.io/ambient_code/vteam_credential_google:$IMAGE_TAG
popd
rm -rf "$TMPDIR"
Step 4: Configure the Operator ConfigMap
The operator needs to know which runner images to spawn and whether to use Vertex AI:
NAMESPACE=<target-namespace>
IMAGE_TAG=<tag>
oc patch configmap operator-config -n $NAMESPACE --type=merge -p "{
\"data\": {
\"AMBIENT_CODE_RUNNER_IMAGE\": \"quay.io/ambient_code/vteam_claude_runner:$IMAGE_TAG\",
\"STATE_SYNC_IMAGE\": \"quay.io/ambient_code/vteam_state_sync:$IMAGE_TAG\",
\"USE_VERTEX\": \"0\",
\"CLOUD_ML_REGION\": \"\",
\"ANTHROPIC_VERTEX_PROJECT_ID\": \"\",
\"GOOGLE_APPLICATION_CREDENTIALS\": \"\"
}
}"
Also patch the agent registry ConfigMap so runner image refs point to the PR tag:
REGISTRY=$(oc get configmap ambient-agent-registry -n $NAMESPACE \
-o jsonpath='{.data.agent-registry\.json}')
REGISTRY=$(echo "$REGISTRY" | sed \
"s|quay.io/ambient_code/vteam_claude_runner[@:][^\"]*|quay.io/ambient_code/vteam_claude_runner:$IMAGE_TAG|g")
REGISTRY=$(echo "$REGISTRY" | sed \
"s|quay.io/ambient_code/vteam_state_sync[@:][^\"]*|quay.io/ambient_code/vteam_state_sync:$IMAGE_TAG|g")
oc patch configmap ambient-agent-registry -n $NAMESPACE --type=merge \
-p "{\"data\":{\"agent-registry.json\":$(echo "$REGISTRY" | jq -Rs .)}}"
Step 5: Wait for Rollout
NAMESPACE=<target-namespace>
for deploy in backend-api frontend agentic-operator postgresql minio unleash public-api; do
oc rollout status deployment/$deploy -n $NAMESPACE --timeout=300s
done
ambient-api-server-db and ambient-api-server may take longer due to DB init:
oc rollout status deployment/ambient-api-server-db -n $NAMESPACE --timeout=300s
oc rollout status deployment/ambient-api-server -n $NAMESPACE --timeout=300s
Step 6: Verify Installation
Pod Status
oc get pods -n $NAMESPACE
Expected — all pods Running:
NAME READY STATUS RESTARTS
agentic-operator-xxxxx 1/1 Running 0
ambient-api-server-xxxxx 1/1 Running 0
ambient-api-server-db-xxxxx 1/1 Running 0
backend-api-xxxxx 1/1 Running 0
frontend-xxxxx 2/2 Running 0
minio-xxxxx 1/1 Running 0
postgresql-xxxxx 1/1 Running 0
public-api-xxxxx 1/1 Running 0
unleash-xxxxx 1/1 Running 0
Frontend shows 2/2 because of the oauth-proxy sidecar in the production overlay.
Routes
oc get route -n $NAMESPACE
Health Check
BACKEND_HOST=$(oc get route backend-route -n $NAMESPACE -o jsonpath='{.spec.host}')
curl -s https://$BACKEND_HOST/health
Expected: {"status":"healthy"}
Database Tables
oc exec deployment/ambient-api-server-db -n $NAMESPACE -- \
psql -U ambient -d ambient_api_server -c "\dt"
Expected: 6 tables (events, migrations, project_settings, projects, sessions, users).
API Server gRPC Streams
oc logs deployment/ambient-api-server -n $NAMESPACE --tail=20 | grep "gRPC stream"
Expected:
gRPC stream started /ambient.v1.ProjectService/WatchProjects
gRPC stream started /ambient.v1.SessionService/WatchSessions
SDK Environment Setup
export AMBIENT_TOKEN="$(oc whoami -t)"
export AMBIENT_PROJECT="$(oc project -q)"
export AMBIENT_API_URL="$(oc get route public-api-route -n $NAMESPACE \
--template='https://{{.spec.host}}')"
Cross-Namespace Image Pull (Required for Runner Pods)
The operator creates runner pods in dynamically-created project namespaces. Those pods pull images from quay.io directly — no cross-namespace image access issue with quay. However, if you're using the OpenShift internal registry, grant pull access:
oc policy add-role-to-group system:image-puller system:serviceaccounts --namespace=$NAMESPACE
Troubleshooting
ImagePullBackOff
oc describe pod <pod-name> -n $NAMESPACE | grep -A5 "Events:"
- If pulling from quay.io: verify the tag exists (
skopeo inspect docker://quay.io/ambient_code/vteam_backend:<tag>) - If private: create an image pull secret and link it to the default service account
API Server TLS Certificate Missing
oc annotate service ambient-api-server \
service.beta.openshift.io/serving-cert-secret-name=ambient-api-server-tls \
-n $NAMESPACE
sleep 15
oc rollout restart deployment/ambient-api-server -n $NAMESPACE
JWT Configuration
Production uses Red Hat SSO JWKS (--jwk-cert-url=https://sso.redhat.com/...). For ephemeral test instances, JWT validation may need to be disabled or pointed at a different issuer. Check the ambient-api-server-jwt-args-patch.yaml in the production overlay and adjust as needed for non-production contexts.
CrashLoopBackOff
oc logs deployment/<name> -n $NAMESPACE --tail=100
oc describe pod -l app=<name> -n $NAMESPACE
Common causes: missing secret, wrong DB credentials, missing ConfigMap key.
Rollout Timeout
oc get events -n $NAMESPACE --sort-by='.lastTimestamp' | tail -20
CLI Access
acpctl login \
--url https://$(oc get route ambient-api-server -n $NAMESPACE -o jsonpath='{.spec.host}') \
--token $(oc whoami -t)