Skip to content

Console Deployment Guide

Deploy Console (api + worker + web) on AKS. Assumes:

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-latest

Apple Silicon note: If building on macOS ARM, add --platform linux/amd64 to Docker build commands or set DOCKER_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-latest

2. Create namespace

bash
kubectl create namespace console-dev

3. 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 gaeadev

4. 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.key

The 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:

VariableSource
LOG_LEVELLog verbosity (debug, info, warn, error)
DEVELOPMENT_MODEtrue for dev (allows non-HTTPS redirects), false for prod
CONSOLE_URLYour Console URL (e.g. https://console-dev.exto360.com)
ZITADEL_ISSUERFrom .bootstrap.env
ZITADEL_API_URLFrom .bootstrap.env
ZITADEL_ADMIN_ORG_IDFrom .bootstrap.env
EXTOID_PROJECT_IDFrom .bootstrap.env
CONSOLE_PORTAL_CLIENT_IDFrom .bootstrap.env
MOBILE_CLIENT_IDFrom .bootstrap.env
EMAIL_PROVIDEREmail provider (smtp, sendgrid)
EMAIL_FROM_ADDRSender address for outgoing emails
SECRET_STORE_PROVIDERazure-keyvault or noop
AZURE_KEY_VAULT_URLYour Key Vault URL (e.g. https://exto-console-dev.vault.azure.net/)
SEED_ADMIN_EMAILAdmin email for initial seed

patch-worker.yaml — fill in from .bootstrap.env and your infra:

VariableSource
LOG_LEVELLog verbosity (debug, info, warn, error)
DEVELOPMENT_MODEtrue for dev, false for prod
CONSOLE_URLYour Console URL
ZITADEL_ISSUERFrom .bootstrap.env
ZITADEL_API_URLFrom .bootstrap.env
ZITADEL_ADMIN_ORG_IDFrom .bootstrap.env
EXTOID_PROJECT_IDFrom .bootstrap.env
EMAIL_PROVIDEREmail provider (smtp, sendgrid)
EMAIL_FROM_ADDRSender address for outgoing emails
SECRET_STORE_PROVIDERazure-keyvault or noop
AZURE_KEY_VAULT_URLYour Key Vault URL
HEALTH_POLL_INTERVAL_SECSInterval 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/dev

9. 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/healthz

Updating

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-latest

Rollout 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-web

New tag

Update newTag in the overlay's kustomization.yaml, then re-apply:

bash
kubectl apply -k deploy/console/overlays/dev

Secrets Strategy

SecretWherePurpose
console-secrets (K8s Secret)Console namespacePostgreSQL, Redis, Zitadel PAT, service credentials, webhook secret, email key
console-agent-secretsInstance namespaceInstance token for console-agent to authenticate with Console API
Azure Key VaultConsole clusterInstance tokens written at runtime by Console API

No secrets are stored in the Git repo.

Environment Differences

SettingDevProd
Namespaceconsole-devconsole-prod
API replicas12
Web replicas11
Worker replicas11
Log leveldebugwarn
Email providersmtpsendgrid
DEVELOPMENT_MODEtruefalse
API CPU req / limit100m/500m200m/1000m
API mem req / limit128Mi/256Mi256Mi/512Mi
Worker CPU req / limit50m/300m100m/500m
Worker mem req / limit128Mi/256Mi256Mi/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 instances

Next steps