Provisioning an OpenShift cluster is just the beginning. The real challenge is transforming a freshly deployed cluster with default settings into a production-ready environment that meets your organization’s requirements. In this guide, I’ll show you how to automate this process using Configuration as Code principles with Kustomize and SOPS.
OpenShift Cluster Provisioning Methods
Before diving into automation, let’s briefly review the three main ways to provision an OpenShift cluster:
1. Installer-Provisioned Infrastructure (IPI)
The OpenShift installer fully automates the deployment process, creating both the cluster and the underlying infrastructure (VMs, networks, load balancers, etc.) on supported cloud providers like AWS, Azure, GCP, or on-premises platforms like VMware.
Pros: Fastest deployment, fully automated, production-ready architecture
Cons: Less flexibility in infrastructure customization
2. User-Provisioned Infrastructure (UPI)
You manually provision and configure the underlying infrastructure before running the OpenShift installer. This gives you complete control over network topology, machine sizing, and security configurations.
Pros: Maximum flexibility and control, works on any platform
Cons: More complex, requires deep infrastructure knowledge
3. Managed OpenShift Services
Cloud providers offer managed OpenShift services like Red Hat OpenShift on AWS (ROSA), Azure Red Hat OpenShift (ARO), or OpenShift Dedicated. The provider handles cluster provisioning and management.
Pros: Minimal operational overhead, integrated cloud services
Cons: Less control, vendor lock-in, potentially higher costs
The Post-Deployment Challenge
Regardless of which provisioning method you choose, every newly deployed OpenShift cluster starts with default configurations. This means:
- ❌ Default certificates that need replacement
- ❌ No identity providers configured (only kubeadmin exists)
- ❌ Missing custom CAs for corporate proxies
- ❌ No operators installed for storage, backup, virtualization, etc.
- ❌ Default ingress controllers without custom certificates
- ❌ No custom configurations for networking, security, or compliance
The traditional approach involves:
- Logging into the OpenShift Web Console
- Clicking through multiple UI screens
- Manually configuring each component
- Repeating this process for every new cluster
- Hoping you remembered all the steps correctly
This manual approach is:
- ⏱️ Time-consuming: Can take hours or days per cluster
- 🐛 Error-prone: Easy to miss steps or make mistakes
- 📝 Undocumented: Configuration exists only in someone’s head
- 🔄 Non-repeatable: Each cluster ends up slightly different
- 🚫 Not version-controlled: No audit trail or rollback capability
The Configuration as Code Solution
Configuration as Code treats your cluster configuration the same way you treat application code:
- ✅ Version controlled in Git
- ✅ Reviewed through pull requests
- ✅ Tested before applying to production
- ✅ Repeatable across environments
- ✅ Documented through the code itself
- ✅ Auditable with complete history
By defining your cluster configuration in YAML files and using tools like Kustomize, you can:
- Apply the same configuration to multiple clusters consistently
- Track every change with Git history
- Quickly recover from misconfigurations
- Share knowledge across teams
- Automate deployments in CI/CD pipelines
Tools: Kustomize and SOPS
Kustomize: Configuration Management
Kustomize
is a Kubernetes-native configuration management tool that lets you customize raw YAML files without templates. It’s built into kubectl and oc (OpenShift CLI).
Key features:
- Patches: Modify existing resources without changing the original files
- Overlays: Maintain base configurations with environment-specific overlays
- Generators: Create ConfigMaps and Secrets from files or literals
- Transformers: Apply common changes (labels, namespaces) across resources
SOPS: Secret Management
SOPS (Secrets OPerationS) encrypts YAML, JSON, and other file formats while keeping the structure readable. It integrates with key management services like AWS KMS, Azure Key Vault, or GCP KMS.
Key features:
- Selective encryption: Only encrypt sensitive fields, keep structure visible
- Git-friendly: Encrypted files can be safely committed to version control
- Multiple key backends: AWS KMS, Azure Key Vault, GCP KMS, PGP, age
- Audit trail: Track who encrypted/decrypted what and when
Together with ksops (a Kustomize plugin for SOPS), you can manage secrets alongside regular configurations.
Project Structure
Here’s the structure for our OpenShift configuration repository:
kustomize/
├── base/
│ ├── kustomization.yaml
│ ├── proxy-cluster.yaml
│ └── ingresscontroller-default.yaml
└── overlays/
└── my_site/
├── kustomization.yaml
├── namespace.yaml
├── apiserver-cluster.yaml
├── oauth.yaml
├── custom-ca-configmap.enc.yaml
├── cert-api-secret.enc.yaml
├── cert-wildcard-secret.enc.yaml
├── oauth-secret.enc.yaml
├── secret-generator.yaml
└── subscriptions/
├── sub-oadp.yaml
├── sub-lvms.yaml
├── sub-nmstate.yaml
└── sub-kubevirt-hyperconverged.yaml
Base layer: Contains common configurations shared across all clusters
Overlay layer: Contains site-specific configurations and secrets
Configuration Examples
Base Configuration
The base layer defines resources that apply to all clusters:
proxy-cluster.yaml
apiVersion: config.openshift.io/v1
kind: Proxy
metadata:
name: cluster
spec:
trustedCA:
name: custom-ca
This configures OpenShift to trust your corporate CA certificates.
ingresscontroller-default.yaml
apiVersion: operator.openshift.io/v1
kind: IngressController
metadata:
name: default
namespace: openshift-ingress-operator
spec:
defaultCertificate:
name: cert-wildcard
This replaces the default self-signed certificate with your custom wildcard certificate.
base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: openshift-certificates-base
resources:
- proxy-cluster.yaml
- ingresscontroller-default.yaml
labels:
- includeSelectors: true
pairs:
de.db/team: responsible
managed-by: kustomize
Overlay Configuration
The overlay adds site-specific configurations:
oauth.yaml
apiVersion: config.openshift.io/v1
kind: OAuth
metadata:
name: cluster
spec:
identityProviders:
- mappingMethod: add
name: Azure
openID:
claims:
email:
- email
name:
- name
preferredUsername:
- customfieldifavailable
- preferred_username
- email
clientID: 123-123-123
clientSecret:
name: openid-client-secret
extraScopes: []
issuer: https://login.microsoftonline.com/123123123
type: OpenID
This configures Azure AD as an identity provider for OpenShift authentication.
Operator Subscriptions
Install and configure operators like OADP (backup), LVMS (storage), NMState (networking), and KubeVirt (virtualization):
# subscriptions/sub-oadp.yaml
apiVersion: v1
kind: Namespace
metadata:
name: openshift-adp
---
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
labels:
operators.coreos.com/redhat-oadp-operator.openshift-adp: ""
name: redhat-oadp-operator
namespace: openshift-adp
spec:
channel: stable-1.4
installPlanApproval: Manual
name: redhat-oadp-operator
source: redhat-operators
sourceNamespace: openshift-marketplace
startingCSV: oadp-operator.v1.4.6
---
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
name: openshift-adp-7nbcd
namespace: openshift-adp
spec:
targetNamespaces:
- openshift-adp
overlays/my_site/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
metadata:
name: test-openshift
resources:
- ../../base
- apiserver-cluster.yaml
- oauth.yaml
- ./subscriptions/sub-oadp.yaml
- ./subscriptions/sub-lvms.yaml
- ./subscriptions/sub-nmstate.yaml
- ./subscriptions/sub-kubevirt-hyperconverged.yaml
- namespace.yaml
labels:
- includeSelectors: true
pairs:
de.db/site: mysite
de.db/team: responsible
generators:
- ./secret-generator.yaml
Managing Secrets with SOPS
SOPS Configuration
Create a .sops.yaml file to configure encryption rules:
creation_rules:
- unencrypted_regex: "^(apiVersion|metadata|kind|type)$"
path_regex: (.enc.yaml|.enc.yml)
key_groups:
- kms:
- arn: arn:aws:kms:eu-central-1:123456789:key/abc123def456
Key points:
unencrypted_regex: Keep these fields unencrypted for Kubernetes to read the resource typepath_regex: Only encrypt files ending in.enc.yamlor.enc.ymlkms: Use AWS KMS for encryption (you can also use Azure Key Vault, GCP KMS, or age)
Encrypting Secrets
Create your secret file with sensitive data:
# cert-api-secret.enc.yaml
apiVersion: v1
kind: Secret
metadata:
name: cert-api
namespace: openshift-config
type: kubernetes.io/tls
stringData:
tls.crt: |
-----BEGIN CERTIFICATE-----
MIIDXTCCAkWgAwIBAgIJAKZ...
-----END CERTIFICATE-----
tls.key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BA...
-----END PRIVATE KEY-----
Encrypt the file with SOPS:
sops -e -i cert-api-secret.enc.yaml
After encryption, the file looks like:
apiVersion: v1
kind: Secret
metadata:
name: cert-api
namespace: openshift-config
type: kubernetes.io/tls
stringData:
tls.crt: ENC[AES256_GCM,data:8h5jW...,tag:abc123==]
tls.key: ENC[AES256_GCM,data:Kj9mP...,tag:def456==]
sops:
kms:
- arn: arn:aws:kms:eu-central-1:123456789:key/abc123def456
created_at: "2025-11-11T10:30:00Z"
enc: AQICAHh...
# ... more sops metadata
Notice: The structure remains readable (great for Git diffs!), but sensitive values are encrypted.
Secret Generator Configuration
Configure ksops to decrypt secrets during Kustomize build:
# secret-generator.yaml
apiVersion: viaduct.ai/v1
kind: ksops
metadata:
name: example-secret-generator
annotations:
config.kubernetes.io/function: |
exec:
path: ksops
files:
- ./cert-api-secret.enc.yaml
- ./cert-wildcard-secret.enc.yaml
- ./custom-ca-configmap.enc.yaml
- ./oauth-secret.enc.yaml
Deployment Process
Prerequisites
Install required tools:
# Install kustomize brew install kustomize # macOS # or download from https://kubectl.docs.kubernetes.io/installation/kustomize/ # Install SOPS brew install sops # macOS # or download from https://github.com/mozilla/sops/releases # Install ksops # Follow instructions at https://github.com/viaduct-ai/kustomize-sopsConfigure AWS credentials (if using AWS KMS):
export AWS_PROFILE=your-profile # or export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=...Login to OpenShift:
oc login https://api.your-cluster.com:6443
Step 1: Initial Deployment
Apply the configuration to your cluster:
# Build and preview the resources
kustomize build --enable-alpha-plugins kustomize/overlays/my_site | less
# Apply the configuration
kustomize build --enable-alpha-plugins kustomize/overlays/my_site | oc apply -f -
Step 2: Handle CRD Installation Failures
Important: Some resources will fail initially because the Custom Resource Definitions (CRDs) don’t exist yet. This is expected behavior when installing operators.
What happens:
- Operator Subscriptions create InstallPlans
- InstallPlans install CRDs and operators
- Custom resources (like
LVMCluster) fail because CRDs aren’t ready yet
Example error:
error: unable to recognize "STDIN": no matches for kind "LVMCluster"
in version "lvm.topolvm.io/v1alpha1"
Step 3: Approve InstallPlans
For security, operator installations use installPlanApproval: Manual, requiring manual approval:
- Open the OpenShift Web Console
- Navigate to: Operators → Installed Operators
- For each operator (OADP, LVMS, NMState, KubeVirt):
- Click on the operator namespace
- Find the InstallPlan in “Pending” state
- Click “Approve”
- Wait for operators to finish installing (watch the status change to “Succeeded”)
Alternatively, approve via CLI:
# List pending install plans
oc get installplan -A | grep -v "true"
# Approve a specific install plan
oc patch installplan <install-plan-name> -n <namespace> \
--type merge --patch '{"spec":{"approved":true}}'
Step 4: Re-apply Configuration
After operators are installed and CRDs are available:
# Apply again to create custom resources
kustomize build --enable-alpha-plugins kustomize/overlays/my_site | oc apply -f -
This time, resources like LVMCluster, DataProtectionApplication, etc., will be created successfully.
Step 5: Verify Deployment
Check that everything is running:
# Check operator installations
oc get csv -A
# Check custom resources
oc get lvmcluster -n openshift-storage
oc get dataprotectionapplication -n openshift-adp
# Verify OAuth configuration
oc get oauth cluster -o yaml
# Check ingress controller
oc get ingresscontroller default -n openshift-ingress-operator -o yaml
# Verify certificates
oc get secret -n openshift-config | grep cert
Best Practices
1. Use Git for Version Control
git add kustomize/
git commit -m "Add OpenShift cluster configuration"
git push
2. Separate Secrets from Configuration
- Keep all
.enc.yamlfiles encrypted - Never commit unencrypted secrets to Git
- Use different KMS keys for different environments
3. Use Overlays for Environments
overlays/
├── dev/
├── staging/
└── production/
4. Document Your Configurations
- Add comments explaining non-obvious configurations
- Maintain a README with deployment instructions
- Document any manual steps required
5. Test in Lower Environments First
- Deploy to dev/test clusters first
- Validate configurations before promoting to production
- Use GitOps tools like ArgoCD for automated deployments
6. Regular Backups
# Backup current cluster configuration
oc get all,secrets,configmaps -A -o yaml > cluster-backup-$(date +%Y%m%d).yaml
7. Use Labels Consistently
Apply meaningful labels for tracking and organization:
labels:
- includeSelectors: true
pairs:
app: myapp
environment: production
team: platform
managed-by: kustomize
Troubleshooting
SOPS Decryption Fails
# Error: Failed to get the data key required to decrypt the SOPS file
# Solution: Check AWS credentials and KMS key permissions
aws sts get-caller-identity
aws kms describe-key --key-id <your-kms-key-id>
Kustomize Build Fails
# Error: accumulating resources: accumulation err='accumulating resources...'
# Solution: Validate YAML syntax
yamllint kustomize/overlays/my_site/*.yaml
# Check kustomization.yaml references
kustomize build --enable-alpha-plugins kustomize/overlays/my_site
Operator Installation Hangs
# Check operator pod logs
oc logs -n <operator-namespace> -l name=<operator-name>
# Check InstallPlan status
oc get installplan -n <namespace> -o yaml
# Check for resource conflicts
oc get events -n <namespace> --sort-by='.lastTimestamp'
Advanced: GitOps Integration
For production environments, integrate with GitOps tools like ArgoCD:
# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: openshift-config
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/openshift-config
targetRevision: main
path: kustomize/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: false
selfHeal: true
This enables:
- Automatic sync when configurations change in Git
- Drift detection when manual changes are made
- Rollback capability to previous versions
- Visual dashboard for configuration status
Conclusion
Automating OpenShift cluster configuration with Kustomize and SOPS transforms cluster management from a manual, error-prone process into a repeatable, version-controlled workflow. By embracing Configuration as Code principles, you gain:
- ✅ Consistency across multiple clusters
- ✅ Repeatability for disaster recovery and new deployments
- ✅ Security through encrypted secrets management
- ✅ Auditability with complete Git history
- ✅ Collaboration through code reviews and pull requests
- ✅ Speed in deploying and updating configurations
The initial investment in setting up this automation pays dividends every time you need to deploy a new cluster, update configurations, or recover from issues.
Complete Example Repository
All the examples and configurations shown in this post are available in my GitHub repository:
blog-20251111-openshift-config
The repository includes:
- Complete Kustomize base and overlay configurations
- SOPS encrypted secrets examples
- Operator subscription definitions
- Ready-to-use
.sops.yamlconfiguration
Feel free to use it as a template for your own OpenShift automation!
Resources
- OpenShift Documentation
- Kustomize Documentation
- SOPS GitHub Repository
- ksops - Kustomize SOPS Plugin
- OpenShift Operators Hub
Have you automated your OpenShift cluster configurations? What challenges did you face? Share your experiences in the comments!