DEV Community

Cover image for Deploy Ping Identity Products on Kubernetes with a Single Operator
DarkEdges
DarkEdges

Posted on

Deploy Ping Identity Products on Kubernetes with a Single Operator

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:

  • PingEnvironment shared configuration: tenant ID, tier (development / staging / production), base domain, and ingress defaults.
  • PingFederate, PingDirectory, PingAccess, PingAuthorize, PingAuthorizePAP, one CR per product, each referencing a PingEnvironment via spec.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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then install the CRDs if they aren't already:

make install
# or: kubectl apply -f config/crd/bases/
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode
kubectl apply -f env-dev.yaml
kubectl get pingenvironments -n pingone
# NAME      PHASE   AGE
# env-dev   Ready   5s
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
kubectl apply -f pf-dev.yaml
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Drill into conditions with:

kubectl describe pingfederate dev-pf -n pingone
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What's next

Top comments (0)