Running Ping Identity's product suite on Kubernetes typically means wrestling with the ping-devops Helm chart directly, a large umbrella chart with hundreds of values, no shared defaults across products, and no native Kubernetes status model. The pingone-operator wraps that chart in a set of purpose-built custom resources so you declare what you want and the operator figures out the Helm details.
In this post I'll walk through how the operator works, how to install it, and how to get PingFederate (and the rest of the suite) running in a cluster.
The design in one paragraph
The operator introduces six custom resource definitions:
-
PingEnvironmentshared configuration: tenant ID, tier (development / staging / production), base domain, and ingress defaults. -
PingFederate,PingDirectory,PingAccess,PingAuthorize,PingAuthorizePAP, one CR per product, each referencing aPingEnvironmentviaspec.environmentRef.
Underneath, everything still maps to a single ping-devops Helm release per environment. Products are enabled or disabled in that release based on which product CRs exist. You get independent Kubernetes API objects (separate RBAC, separate status, deploy or remove products without touching shared config) without needing separate Helm releases.
Prerequisites
| Requirement | Version |
|---|---|
| Kubernetes | 1.26+ |
| kubectl | matching cluster |
| Helm | 3.x (CLI, for install only) |
| Ping Identity DevOps account | register here |
You'll also need a devops-secret in whatever namespace products will be deployed to:
kubectl create secret generic devops-secret \
--namespace pingone \
--from-literal=PING_IDENTITY_ACCEPT_EULA=YES \
--from-literal=PING_IDENTITY_DEVOPS_USER=<your-devops-user> \
--from-literal=PING_IDENTITY_DEVOPS_KEY=<your-devops-key>
Installing the operator
Option A: from the OCI Helm chart
helm upgrade --install pingone-operator \
oci://ghcr.io/darkedges/charts/pingone-operator \
--version 0.1.0 \
--namespace pingone-system \
--create-namespace
Option B: from source (Docker Desktop)
git clone https://github.com/darkedges/pingone-operator
cd pingone-operator
make docker-desktop # builds image, loads it into Docker Desktop, deploys
Either way, verify the operator is running:
kubectl get pods -n pingone-system
# NAME READY STATUS RESTARTS AGE
# pingone-operator-controller-manager-... 1/1 Running 0 30s
Then install the CRDs if they aren't already:
make install
# or: kubectl apply -f config/crd/bases/
Creating your first environment
A PingEnvironment holds the shared config that all product CRs in the same namespace will inherit.
# env-dev.yaml
apiVersion: pingone.io/v1alpha1
kind: PingEnvironment
metadata:
name: env-dev
namespace: pingone
spec:
tenantId: myorg-dev # Helm release name: myorg-dev-ping
tier: development # development | staging | production
domain: dev.myorg.example.com # base domain for auto-derived hostnames
# Shared ingress: all product CRs inherit this unless they override it
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
kubectl apply -f env-dev.yaml
kubectl get pingenvironments -n pingone
# NAME PHASE AGE
# env-dev Ready 5s
No Helm release is created yet, the environment is a coordination point, not a trigger by itself.
Adding PingFederate
Create a PingFederate CR that references the environment:
# pf-dev.yaml
apiVersion: pingone.io/v1alpha1
kind: PingFederate
metadata:
name: dev-pf
namespace: pingone
spec:
environmentRef: env-dev # references the PingEnvironment above
version: "13.0.2-edge"
replicas: 1
engineIngress:
enabled: true
# hostname auto-derived: pf.dev.myorg.example.com
tlsSecretRef: pf-tls
adminIngress:
enabled: true
# hostname auto-derived: pf-admin.dev.myorg.example.com
tlsSecretRef: pf-admin-tls
config:
serverProfile:
url: https://github.com/pingidentity/pingidentity-server-profiles.git
path: getting-started/pingfederate
kubectl apply -f pf-dev.yaml
The PingFederate reconciler validates the CR and queues the PingEnvironment reconciler. That reconciler discovers all product CRs pointing at env-dev, builds the combined Helm values, and installs or upgrades the myorg-dev-ping release.
Watch it come up:
kubectl get pingfederates -n pingone
# NAME ENVIRONMENT PHASE AGE
# dev-pf env-dev Ready 90s
kubectl get pods -n pingone
# NAME READY STATUS AGE
# myorg-dev-ping-pingfederate-engine-0 1/1 Running 80s
# myorg-dev-ping-pingfederate-admin-0 1/1 Running 80s
Hostnames are derived automatically
When spec.domain is set on the PingEnvironment, you don't need to spell out every hostname. The operator derives them:
| Product | Auto-derived hostname |
|---|---|
| PingFederate engine | pf.<domain> |
| PingFederate admin | pf-admin.<domain> |
| PingDataConsole | pd-console.<domain> |
| PingAccess admin | pa-admin.<domain> |
| PingAccess engine | pa.<domain> |
| PingAuthorize | paz.<domain> |
| PingAuthorizePAP | paz-pap.<domain> |
Override any of them by setting hostname inside the component's ingress block:
engineIngress:
enabled: true
hostname: sso.myorg.example.com # explicit override
tlsSecretRef: sso-tls
Adding more products
Each product is its own CR. Add them in any order, the operator reconciles the full set each time any one of them changes.
PingDirectory
apiVersion: pingone.io/v1alpha1
kind: PingDirectory
metadata:
name: dev-pd
namespace: pingone
spec:
environmentRef: env-dev
replicas: 1
storageSize: 8Gi
config:
serverProfile:
url: https://github.com/pingidentity/pingidentity-server-profiles.git
path: baseline/pingdirectory
userBaseDN: "dc=myorg,dc=com"
PingAuthorize (with a startup dependency on PingDirectory)
apiVersion: pingone.io/v1alpha1
kind: PingAuthorize
metadata:
name: dev-paz
namespace: pingone
spec:
environmentRef: env-dev
replicas: 1
ingress:
enabled: true
tlsSecretRef: paz-tls
container:
waitFor:
- application: pingDirectory
service: ldaps
timeoutSeconds: 300
config:
serverProfile:
url: https://github.com/pingidentity/pingidentity-server-profiles.git
path: paz-pap-integration/pingauthorize
serverProfileLayers:
- name: paz
url: https://github.com/pingidentity/pingidentity-server-profiles.git
path: baseline/pingauthorize
PingAuthorizePAP
apiVersion: pingone.io/v1alpha1
kind: PingAuthorizePAP
metadata:
name: dev-pap
namespace: pingone
spec:
environmentRef: env-dev
ingress:
enabled: true
tlsSecretRef: paz-pap-tls
config:
serverProfile:
url: https://github.com/pingidentity/pingidentity-server-profiles.git
path: paz-pap-integration/pingauthorizepap
PingAccess (waits for PingFederate)
apiVersion: pingone.io/v1alpha1
kind: PingAccess
metadata:
name: dev-pa
namespace: pingone
spec:
environmentRef: env-dev
version: "8.1.0-edge"
adminIngress:
enabled: true
tlsSecretRef: pa-admin-tls
engineIngress:
enabled: true
tlsSecretRef: pa-tls
container:
waitFor:
- application: pingFederate
service: https
timeoutSeconds: 300
config:
serverProfile:
url: https://github.com/pingidentity/pingidentity-server-profiles.git
path: getting-started/pingaccess
Startup ordering with waitFor
The container.waitFor block controls which services a container waits for before it starts. Under the hood this maps to the WAIT_FOR env var in the ping-devops Docker images.
container:
waitFor:
- application: pingDirectory
service: ldaps
timeoutSeconds: 300
- application: pingFederate
service: https
timeoutSeconds: 300
Logical application names like pingDirectory, pingFederateAdmin, pingAccessEngine are resolved by the operator to the correct Helm sub-chart service names automatically.
Layered server profiles
The operator supports Ping Identity's layered profile pattern. Use serverProfileLayers to chain profiles:
config:
serverProfile:
url: https://github.com/myorg/ping-profiles.git
path: extensions/pingfederate
parent: baseline # chains to the layer named "baseline"
serverProfileLayers:
- name: baseline
url: https://github.com/pingidentity/pingidentity-server-profiles.git
path: baseline/pingfederate
The operator translates these to the SERVER_PROFILE_* and SERVER_PROFILE_<LAYER>_* env var convention the Docker images expect.
Resource sizing with tier
Set tier once on the PingEnvironment and the operator applies appropriate CPU/memory requests and limits to every product:
| Tier | PingFederate | PingDirectory | PingAccess | PingAuthorize |
|---|---|---|---|---|
development |
500m / 512Mi | 500m / 1Gi | 500m / 512Mi | 500m / 1Gi |
staging |
1 / 1Gi | 1 / 2Gi | 1 / 1Gi | 1 / 2Gi |
production |
2 / 2Gi | 2 / 4Gi | 2 / 2Gi | 2 / 4Gi |
Checking status
Each resource has its own Phase field and standard Kubernetes conditions:
kubectl get pingenvironments,pingfederates,pingdirectories -n pingone
# NAME PHASE AGE
# pingenvironment.pingone.io/env-dev Ready 10m
#
# NAME ENVIRONMENT PHASE AGE
# pingfederate.pingone.io/dev-pf env-dev Ready 8m
# pingdirectory.pingone.io/dev-pd env-dev Ready 7m
Drill into conditions with:
kubectl describe pingfederate dev-pf -n pingone
Removing a product
Delete the product CR. The operator detects the deletion, rebuilds Helm values with that product disabled, and upgrades the release in place. The other products keep running.
kubectl delete pingaccess dev-pa -n pingone
# PingAccess pods terminate; PingFederate and PingDirectory are unaffected
Delete the PingEnvironment to tear down the entire Helm release:
kubectl delete pingenvironment env-dev -n pingone
# Finalizer triggers Helm uninstall; all product pods are removed
What's next
- The source is at https://github.com/darkedges/pingone-kubernetes-provider
- The
examples/getting-started/directory has a complete working example for Docker Desktop with nginx ingress and cert-manager self-signed TLS - Full field reference is in
CONFIGURATION.md
Top comments (0)