| 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
oc CLI installed and logged in to the target cluster
kustomize installed (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
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>
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)