Appearance
Console Deployment Guide
Deploy Console (api + worker + web) on AKS. Assumes:
- Zitadel is running — see Zitadel AKS Setup
- Bootstrap is complete — see Bootstrap Guide
Architecture
┌─────────────────────────────────────────────┐
│ Console Namespace (console-dev / console-prod) │
│ │
│ ┌──────────┐ ┌───────────┐ ┌─────────┐ │
│ │ console-api│ │console-worker│ │console-web│ │
│ │ (Go) │ │ (Go) │ │ (nginx) │ │
│ └────┬─────┘ └─────┬─────┘ └─────────┘ │
│ │ │ │
│ ├───────────────┤ │
│ ▼ ▼ │
│ PostgreSQL Zitadel Redis │
└─────────────────────────────────────────────┘1. Build Docker images
All images must be built for linux/amd64 (AKS node architecture):
bash
# Build all images
make docker-all REGISTRY=gaeadev.azurecr.io TAG=dev-latest
# Or individually
make docker-api REGISTRY=gaeadev.azurecr.io TAG=dev-latest
make docker-worker REGISTRY=gaeadev.azurecr.io TAG=dev-latest
make docker-web REGISTRY=gaeadev.azurecr.io TAG=dev-latestApple Silicon note: If building on macOS ARM, add
--platform linux/amd64to Docker build commands or setDOCKER_DEFAULT_PLATFORM=linux/amd64.
Push to your registry:
bash
docker push gaeadev.azurecr.io/console-api:dev-latest
docker push gaeadev.azurecr.io/console-worker:dev-latest
docker push gaeadev.azurecr.io/console-web:dev-latest2. Create namespace
bash
kubectl create namespace console-dev3. Create image pull secret
If your ACR is not attached to the AKS cluster:
bash
kubectl -n console-dev create secret docker-registry console-dev-pull-secret \
--docker-server=gaeadev.azurecr.io \
--docker-username=<acr-username> \
--docker-password=<acr-password>Alternatively, attach ACR to AKS (no pull secret needed):
bash
az aks update -n <aks-cluster> -g <resource-group> --attach-acr gaeadev4. Create TLS certificate secret
bash
kubectl -n console-dev create secret tls console-dev.exto360.com \
--cert=path/to/tls.crt \
--key=path/to/tls.keyThe ingress references this secret for HTTPS termination. Use cert-manager for automatic provisioning if preferred.
5. Create Console secrets
Values come from .bootstrap.env (generated by bootstrap) and your infrastructure:
bash
kubectl -n console-dev create secret generic console-secrets \
--from-literal=DATABASE_URL='postgres://<user>:<pass>@<host>:5432/<db>?sslmode=require' \
--from-literal=REDIS_URL='redis://redis:6379' \
--from-literal=CONSOLE_SERVICE_CLIENT_ID='<from .bootstrap.env>' \
--from-literal=CONSOLE_SERVICE_CLIENT_SECRET='<from .bootstrap.env>' \
--from-literal=ZITADEL_WEBHOOK_SECRET='<from .bootstrap.env>' \
--from-literal=EMAIL_API_KEY='<email provider key>'To update a single secret value later:
bash
kubectl -n console-dev patch secret console-secrets --type merge \
-p '{"data":{"ZITADEL_WEBHOOK_SECRET":"'$(echo -n '<new-value>' | base64)'"}}'6. Configure the overlay
Edit deploy/console/overlays/dev/:
kustomization.yaml — set your registry and tag:
yaml
images:
- name: registry.io/exto/console-api
newName: gaeadev.azurecr.io/console-api
newTag: "dev-latest"
- name: registry.io/exto/console-worker
newName: gaeadev.azurecr.io/console-worker
newTag: "dev-latest"
- name: registry.io/exto/console-web
newName: gaeadev.azurecr.io/console-web
newTag: "dev-latest"patch-api.yaml — fill in from .bootstrap.env and your infra:
| Variable | Source |
|---|---|
LOG_LEVEL | Log verbosity (debug, info, warn, error) |
DEVELOPMENT_MODE | true for dev (allows non-HTTPS redirects), false for prod |
CONSOLE_URL | Your Console URL (e.g. https://console-dev.exto360.com) |
ZITADEL_ISSUER | From .bootstrap.env |
ZITADEL_API_URL | From .bootstrap.env |
ZITADEL_ADMIN_ORG_ID | From .bootstrap.env |
EXTOID_PROJECT_ID | From .bootstrap.env |
CONSOLE_PORTAL_CLIENT_ID | From .bootstrap.env |
MOBILE_CLIENT_ID | From .bootstrap.env |
EMAIL_PROVIDER | Email provider (smtp, sendgrid) |
EMAIL_FROM_ADDR | Sender address for outgoing emails |
SECRET_STORE_PROVIDER | azure-keyvault or noop |
AZURE_KEY_VAULT_URL | Your Key Vault URL (e.g. https://exto-console-dev.vault.azure.net/) |
SEED_ADMIN_EMAIL | Admin email for initial seed |
patch-worker.yaml — fill in from .bootstrap.env and your infra:
| Variable | Source |
|---|---|
LOG_LEVEL | Log verbosity (debug, info, warn, error) |
DEVELOPMENT_MODE | true for dev, false for prod |
CONSOLE_URL | Your Console URL |
ZITADEL_ISSUER | From .bootstrap.env |
ZITADEL_API_URL | From .bootstrap.env |
ZITADEL_ADMIN_ORG_ID | From .bootstrap.env |
EXTOID_PROJECT_ID | From .bootstrap.env |
EMAIL_PROVIDER | Email provider (smtp, sendgrid) |
EMAIL_FROM_ADDR | Sender address for outgoing emails |
SECRET_STORE_PROVIDER | azure-keyvault or noop |
AZURE_KEY_VAULT_URL | Your Key Vault URL |
HEALTH_POLL_INTERVAL_SECS | Interval for instance health polling (default: 300) |
ingress.yaml — set your Console domain and TLS secret name
7. Azure Key Vault RBAC
Console API/Worker write instance tokens to Azure Key Vault at runtime. The AKS kubelet managed identity needs Key Vault Secrets Officer:
bash
az role assignment create \
--role "Key Vault Secrets Officer" \
--assignee <aks-kubelet-identity-object-id> \
--scope /subscriptions/<sub>/resourcegroups/<rg>/providers/Microsoft.KeyVault/vaults/<vault-name>8. Deploy
bash
# Preview
kubectl apply -k deploy/console/overlays/dev --dry-run=client -o yaml | head -50
# Deploy
kubectl apply -k deploy/console/overlays/dev9. Verify
bash
# Check pods
kubectl -n console-dev get pods
# API logs — look for "console api starting", "preflight passed"
kubectl -n console-dev logs deploy/console-api --tail=30
# Worker logs
kubectl -n console-dev logs deploy/console-worker --tail=30
# Web — should return HTML
curl -s https://console-dev.exto360.com/ | head -5
# API health
curl -s https://console-dev.exto360.com/healthzUpdating
Rebuild and push images
bash
make docker-all REGISTRY=gaeadev.azurecr.io TAG=dev-latest
docker push gaeadev.azurecr.io/console-api:dev-latest
docker push gaeadev.azurecr.io/console-worker:dev-latest
docker push gaeadev.azurecr.io/console-web:dev-latestRollout restart (same tag)
If using a mutable tag like dev-latest:
bash
kubectl -n console-dev rollout restart deploy/console-api
kubectl -n console-dev rollout restart deploy/console-worker
kubectl -n console-dev rollout restart deploy/console-webNew tag
Update newTag in the overlay's kustomization.yaml, then re-apply:
bash
kubectl apply -k deploy/console/overlays/devSecrets Strategy
| Secret | Where | Purpose |
|---|---|---|
console-secrets (K8s Secret) | Console namespace | PostgreSQL, Redis, Zitadel PAT, service credentials, webhook secret, email key |
console-agent-secrets | Instance namespace | Instance token for console-agent to authenticate with Console API |
| Azure Key Vault | Console cluster | Instance tokens written at runtime by Console API |
No secrets are stored in the Git repo.
Environment Differences
| Setting | Dev | Prod |
|---|---|---|
| Namespace | console-dev | console-prod |
| API replicas | 1 | 2 |
| Web replicas | 1 | 1 |
| Worker replicas | 1 | 1 |
| Log level | debug | warn |
| Email provider | smtp | sendgrid |
| DEVELOPMENT_MODE | true | false |
| API CPU req / limit | 100m/500m | 200m/1000m |
| API mem req / limit | 128Mi/256Mi | 256Mi/512Mi |
| Worker CPU req / limit | 50m/300m | 100m/500m |
| Worker mem req / limit | 128Mi/256Mi | 256Mi/512Mi |
Directory Structure
deploy/
├── docker/
│ ├── Dockerfile.api # Go multi-stage → distroless
│ ├── Dockerfile.worker # Go multi-stage → distroless
│ ├── Dockerfile.console-agent # Go multi-stage → distroless
│ ├── Dockerfile.web # Node build → nginx:alpine
│ └── nginx.conf # SPA fallback + asset caching
├── console/
│ ├── base/
│ │ ├── kustomization.yaml
│ │ ├── api-deployment.yaml
│ │ ├── api-service.yaml
│ │ ├── worker-deployment.yaml
│ │ ├── web-deployment.yaml
│ │ └── web-service.yaml
│ └── overlays/
│ ├── dev/ # 1 replica, debug, smtp
│ └── prod/ # 2 replicas, warn, sendgrid
└── console-agent/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ ├── serviceaccount.yaml
│ ├── role.yaml
│ └── rolebinding.yaml
└── overlays/
├── qa1/ # Instance cluster overlay
├── qa2/ # Instance cluster overlay
└── example/ # Template for new instancesNext steps
- Console-Agent Setup — deploy agents to instance clusters
- Troubleshooting — common issues and fixes

