Single Cluster Production Setup
Deploy all OpenChoreo planes on a single Kubernetes cluster with your custom domain and TLS certificates.
Before You Beginβ
Read Deployment Planning to understand:
- Domain requirements per plane
- TLS certificate options
- HA considerations
Prerequisitesβ
- Kubernetes 1.32+ cluster (3+ nodes, 4 CPU / 8GB RAM each recommended)
- kubectl and Helm configured
- Your base domain (e.g.,
example.com) - DNS access to create records
- LoadBalancer support
- cert-manager installed in your cluster
Set your base domain:
export DOMAIN="example.com"
Verify cluster access:
kubectl version
helm version --short
kubectl get nodes
Install cert-manager
helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true
Wait for cert-manager to be ready:
kubectl wait --for=condition=available deployment/cert-manager -n cert-manager --timeout=120s
Step 1: Setup Control Planeβ
Deploy the Control Plane Chart
helm upgrade --install openchoreo-control-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-control-plane \
--version 0.8.0 \
--namespace openchoreo-control-plane \
--create-namespace \
--set global.baseDomain=openchoreo.${DOMAIN} \
--set global.tls.enabled=true \
--set "backstage.ingress.tls[0].secretName=control-plane-tls" \
--set "backstage.ingress.tls[0].hosts[0]=openchoreo.${DOMAIN}" \
--set "openchoreoApi.ingress.tls[0].secretName=control-plane-tls" \
--set "openchoreoApi.ingress.tls[0].hosts[0]=api.openchoreo.${DOMAIN}" \
--set "thunder.ocIngress.tls[0].secretName=control-plane-tls" \
--set "thunder.ocIngress.tls[0].hosts[0]=thunder.openchoreo.${DOMAIN}" \
--set thunder.configuration.server.publicUrl=https://thunder.openchoreo.${DOMAIN} \
--set thunder.configuration.gateClient.hostname=thunder.openchoreo.${DOMAIN} \
--set thunder.configuration.gateClient.port=443 \
--set thunder.configuration.gateClient.scheme="https"
Wait for pods to start:
kubectl get pods -n openchoreo-control-plane -w
Configure DNS
- Standard (GKE, AKS, etc.)
- AWS EKS
Wait for LoadBalancer to get an external IP (press Ctrl+C once EXTERNAL-IP appears):
kubectl get svc openchoreo-traefik -n openchoreo-control-plane -w
EKS LoadBalancers are private by default and return a hostname instead of an IP.
Make the LoadBalancer internet-facing:
kubectl patch svc openchoreo-traefik -n openchoreo-control-plane \
-p '{"metadata":{"annotations":{"service.beta.kubernetes.io/aws-load-balancer-scheme":"internet-facing"}}}'
Wait for the new LoadBalancer to be provisioned (this may take 1-2 minutes). Press Ctrl+C once EXTERNAL-IP (hostname) appears:
kubectl get svc openchoreo-traefik -n openchoreo-control-plane -w
For DNS records, use the LoadBalancer hostname (CNAME) or resolve it to an IP.
Create DNS records pointing to the LoadBalancer IP (or hostname for EKS):
| Record | Value |
|---|---|
openchoreo.${DOMAIN} | LoadBalancer IP |
api.openchoreo.${DOMAIN} | LoadBalancer IP |
thunder.openchoreo.${DOMAIN} | LoadBalancer IP |
Configure TLS
- Using cert-manager
- Bring Your Own Certificates
- HTTP-01 Solver
- DNS-01 Solver
- Existing Issuer
HTTP-01 validation requires your LoadBalancer to be publicly accessible on port 80.
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt
namespace: openchoreo-control-plane
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@${DOMAIN}
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- http01:
ingress:
ingressClassName: openchoreo-traefik
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: control-plane-tls
namespace: openchoreo-control-plane
spec:
secretName: control-plane-tls
issuerRef:
name: letsencrypt
kind: Issuer
dnsNames:
- "openchoreo.${DOMAIN}"
- "api.openchoreo.${DOMAIN}"
- "thunder.openchoreo.${DOMAIN}"
EOF
DNS-01 validation works with private clusters. Configure for your DNS provider.
Example for Cloudflare:
Create secret with API token:
kubectl create secret generic cloudflare-api-token \
--from-literal=api-token=YOUR_CLOUDFLARE_API_TOKEN \
-n openchoreo-control-plane
Create the Issuer and Certificate:
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt
namespace: openchoreo-control-plane
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@${DOMAIN}
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: control-plane-tls
namespace: openchoreo-control-plane
spec:
secretName: control-plane-tls
issuerRef:
name: letsencrypt
kind: Issuer
dnsNames:
- "openchoreo.${DOMAIN}"
- "api.openchoreo.${DOMAIN}"
- "thunder.openchoreo.${DOMAIN}"
EOF
See cert-manager DNS01 docs for other providers (Route53, Google Cloud DNS, Azure DNS, etc.).
If you have an existing Issuer or ClusterIssuer:
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: control-plane-tls
namespace: openchoreo-control-plane
spec:
secretName: control-plane-tls
issuerRef:
name: YOUR_ISSUER_NAME
kind: Issuer # or ClusterIssuer
dnsNames:
- "openchoreo.${DOMAIN}"
- "api.openchoreo.${DOMAIN}"
- "thunder.openchoreo.${DOMAIN}"
EOF
Wait for certificate (until READY shows True):
kubectl get certificate control-plane-tls -n openchoreo-control-plane -w
Create TLS secret with your certificate:
kubectl create secret tls control-plane-tls \
--cert=./path/to/cert.pem \
--key=./path/to/key.pem \
-n openchoreo-control-plane
Your certificate must be valid for:
openchoreo.${DOMAIN}api.openchoreo.${DOMAIN}thunder.openchoreo.${DOMAIN}
Use a wildcard cert (*.openchoreo.${DOMAIN}) or a SAN cert with all three domains.
Step 2: Setup Data Planeβ
Deploy the Data Plane Chart
helm upgrade --install openchoreo-data-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-data-plane \
--version 0.8.0 \
--namespace openchoreo-data-plane \
--set gateway.httpPort=19080 \
--set gateway.httpsPort=19443 \
--create-namespace
Configure DNS
- Standard (GKE, AKS, etc.)
- AWS EKS
Wait for LoadBalancer to get an external IP (press Ctrl+C once EXTERNAL-IP appears):
kubectl get svc gateway-default -n openchoreo-data-plane -w
Make the LoadBalancer internet-facing:
kubectl patch svc gateway-default -n openchoreo-data-plane \
-p '{"metadata":{"annotations":{"service.beta.kubernetes.io/aws-load-balancer-scheme":"internet-facing"}}}'
Wait for the new LoadBalancer to be provisioned (this may take 1-2 minutes). Press Ctrl+C once EXTERNAL-IP (hostname) appears:
kubectl get svc gateway-default -n openchoreo-data-plane -w
Create DNS records:
| Record | Value |
|---|---|
apps.openchoreo.${DOMAIN} | Gateway LoadBalancer IP |
*.apps.openchoreo.${DOMAIN} | Gateway LoadBalancer IP |
Configure TLS
The data plane gateway uses wildcard hostnames (*.apps.openchoreo.${DOMAIN}). HTTP-01 validation cannot issue wildcard certificates - only DNS-01 validation or bring-your-own certificates work.
- Using cert-manager (DNS-01)
- Bring Your Own Certificates
DNS-01 validation requires configuring your DNS provider. See cert-manager DNS01 docs for all supported providers.
Example for Cloudflare:
Create secret with API token:
kubectl create secret generic cloudflare-api-token \
--from-literal=api-token=YOUR_CLOUDFLARE_API_TOKEN \
-n openchoreo-data-plane
Create the Issuer and Certificate:
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt
namespace: openchoreo-data-plane
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@${DOMAIN}
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: openchoreo-gateway-tls
namespace: openchoreo-data-plane
spec:
secretName: openchoreo-gateway-tls
issuerRef:
name: letsencrypt
kind: Issuer
dnsNames:
- "apps.openchoreo.${DOMAIN}"
- "*.apps.openchoreo.${DOMAIN}"
EOF
Wait for certificate:
kubectl get certificate openchoreo-gateway-tls -n openchoreo-data-plane -w
kubectl create secret tls openchoreo-gateway-tls \
--cert=./path/to/apps-cert.pem \
--key=./path/to/apps-key.pem \
-n openchoreo-data-plane
Use a wildcard certificate for *.apps.openchoreo.${DOMAIN}
Register
CA_CERT=$(kubectl get secret cluster-agent-tls -n openchoreo-data-plane -o jsonpath='{.data.ca\.crt}' | base64 -d)
kubectl apply -f - <<EOF
apiVersion: openchoreo.dev/v1alpha1
kind: DataPlane
metadata:
name: default
namespace: default
spec:
agent:
enabled: true
clientCA:
value: |
$(echo "$CA_CERT" | sed 's/^/ /')
gateway:
organizationVirtualHost: "openchoreoapis.internal"
publicVirtualHost: "apps.openchoreo.${DOMAIN}"
secretStoreRef:
name: default
EOF
Verify:
kubectl get dataplane -n default
kubectl get pods -n openchoreo-data-plane
Step 3: Setup Build Plane (Optional)β
Deploy the Build Plane Chart
helm upgrade --install openchoreo-build-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-build-plane \
--version 0.8.0 \
--namespace openchoreo-build-plane \
--create-namespace \
--set clusterAgent.enabled=true \
--set external-secrets.enabled=false \
--set global.baseDomain=openchoreo.${DOMAIN}
Configure DNS - Create record pointing to the Control Plane LoadBalancer:
| Record | Value |
|---|---|
registry.openchoreo.${DOMAIN} | Control Plane LoadBalancer IP |
Configure TLS
The container registry must have a valid, trusted TLS certificate. Kubernetes nodes need to pull images from it.
- Using cert-manager
- Bring Your Own Certificates
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt
namespace: openchoreo-build-plane
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@${DOMAIN}
privateKeySecretRef:
name: letsencrypt-account-key
solvers:
- http01:
ingress:
ingressClassName: openchoreo-traefik
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: build-plane-tls
namespace: openchoreo-build-plane
spec:
secretName: build-plane-tls
issuerRef:
name: letsencrypt
kind: Issuer
dnsNames:
- "registry.openchoreo.${DOMAIN}"
EOF
kubectl create secret tls build-plane-tls \
--cert=./path/to/registry-cert.pem \
--key=./path/to/registry-key.pem \
-n openchoreo-build-plane
Wait for certificate:
kubectl get certificate build-plane-tls -n openchoreo-build-plane -w
Upgrade with TLS - After the certificate is ready:
helm upgrade --install openchoreo-build-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-build-plane \
--version 0.8.0 \
--namespace openchoreo-build-plane \
--reuse-values \
--set global.tls.enabled=true \
--set global.tls.secretName=build-plane-tls
Register
BP_CA_CERT=$(kubectl get secret cluster-agent-tls -n openchoreo-build-plane -o jsonpath='{.data.ca\.crt}' | base64 -d)
kubectl apply -f - <<EOF
apiVersion: openchoreo.dev/v1alpha1
kind: BuildPlane
metadata:
name: default
namespace: default
spec:
agent:
enabled: true
clientCA:
value: |
$(echo "$BP_CA_CERT" | sed 's/^/ /')
EOF
Verify:
kubectl get buildplane -n default
kubectl get pods -n openchoreo-build-plane
Step 4: Setup Observability Plane (Optional)β
- Minimal
- HA Mode
Single-node OpenSearch for development or small deployments.
helm upgrade --install openchoreo-observability-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-observability-plane \
--version 0.8.0 \
--namespace openchoreo-observability-plane \
--create-namespace \
--set openSearch.enabled=true \
--set openSearchCluster.enabled=false \
--set external-secrets.enabled=false \
--set clusterAgent.enabled=true \
--timeout 10m
Clustered OpenSearch using the OpenSearch Operator for high availability.
Install OpenSearch Operator:
helm repo add opensearch-operator https://opensearch-project.github.io/opensearch-k8s-operator/
helm repo update
helm upgrade --install opensearch-operator opensearch-operator/opensearch-operator \
--create-namespace \
--namespace openchoreo-observability-plane \
--version 2.8.0
Wait for the operator to be ready:
kubectl wait --for=condition=available \
deployment/opensearch-operator-controller-manager \
-n openchoreo-observability-plane --timeout=120s
Install Observability Plane:
helm upgrade --install openchoreo-observability-plane oci://ghcr.io/openchoreo/helm-charts/openchoreo-observability-plane \
--version 0.8.0 \
--namespace openchoreo-observability-plane \
--set external-secrets.enabled=false \
--set clusterAgent.enabled=true \
--timeout 10m
Register with the control plane:
OP_CA_CERT=$(kubectl get secret cluster-agent-tls -n openchoreo-observability-plane -o jsonpath='{.data.ca\.crt}' | base64 -d)
kubectl apply -f - <<EOF
apiVersion: openchoreo.dev/v1alpha1
kind: ObservabilityPlane
metadata:
name: default
namespace: default
spec:
agent:
enabled: true
clientCA:
value: |
$(echo "$OP_CA_CERT" | sed 's/^/ /')
observerURL: http://observer.openchoreo-observability-plane.svc.cluster.local:8080
EOF
Link the Data Plane (and Build Plane if installed) to use observability:
kubectl patch dataplane default -n default --type merge -p '{"spec":{"observabilityPlaneRef":"default"}}'
kubectl patch buildplane default -n default --type merge -p '{"spec":{"observabilityPlaneRef":"default"}}'
Verify:
kubectl get observabilityplane -n default
kubectl logs -n openchoreo-observability-plane -l app=cluster-agent --tail=10
DNS Records Summaryβ
Here's a complete list of all DNS records required for single-cluster setup:
| Record | Points To | Plane |
|---|---|---|
openchoreo.${DOMAIN} | Control Plane LoadBalancer IP | Control |
api.openchoreo.${DOMAIN} | Control Plane LoadBalancer IP | Control |
thunder.openchoreo.${DOMAIN} | Control Plane LoadBalancer IP | Control |
apps.openchoreo.${DOMAIN} | Data Plane Gateway LoadBalancer IP | Data |
*.apps.openchoreo.${DOMAIN} | Data Plane Gateway LoadBalancer IP | Data |
registry.openchoreo.${DOMAIN} | Control Plane LoadBalancer IP | Build (optional) |
Access URLsβ
| Service | URL |
|---|---|
| Console | https://openchoreo.${DOMAIN} |
| API | https://api.openchoreo.${DOMAIN} |
| Deployed Apps | https://<component>-<env>.apps.openchoreo.${DOMAIN} |
| Registry | https://registry.openchoreo.${DOMAIN} (if Build Plane) |
Default credentials: admin@openchoreo.dev / Admin@123
Next Stepsβ
- Deploy your first component
- Review Operations Guide for maintenance procedures
Cleanupβ
Delete plane registrations:
kubectl delete dataplane default -n default 2>/dev/null
kubectl delete buildplane default -n default 2>/dev/null
kubectl delete observabilityplane default -n default 2>/dev/null
Uninstall OpenChoreo components:
helm uninstall openchoreo-observability-plane -n openchoreo-observability-plane 2>/dev/null
helm uninstall opensearch-operator -n openchoreo-observability-plane 2>/dev/null
helm uninstall openchoreo-build-plane -n openchoreo-build-plane 2>/dev/null
helm uninstall openchoreo-data-plane -n openchoreo-data-plane
helm uninstall openchoreo-control-plane -n openchoreo-control-plane
helm uninstall cert-manager -n cert-manager
Delete namespaces:
kubectl delete namespace openchoreo-control-plane openchoreo-data-plane \
openchoreo-build-plane openchoreo-observability-plane cert-manager 2>/dev/null
Delete CRDs:
kubectl get crd -o name | grep -E '\.openchoreo\.dev$' | xargs -r kubectl delete
Troubleshootingβ
Certificate not issuingβ
kubectl describe certificate <name> -n <namespace>
kubectl get challenges -A
kubectl describe issuer <name> -n <namespace>
Common issues:
- DNS records not propagated yet
- Firewall blocking port 80 (for HTTP-01)
- DNS provider credentials incorrect (for DNS-01)
Agent not connectingβ
kubectl logs -n openchoreo-data-plane -l app=cluster-agent --tail=20
kubectl logs -n openchoreo-control-plane -l app=cluster-gateway --tail=20
Pods stuck in Pendingβ
kubectl describe pod <pod-name> -n <namespace>
Common causes: insufficient resources, PVC issues, node taints.