Cluster topology overview (physical and logical layout of the full OCP enterprise cluster)

Diagram 2 – Networking & security zones (SDN, NetworkPolicy, and multi-tenancy boundaries)

Diagram 3 : Day-2 GitOps and CI/CD pipeline (how code flows from Git to production)

Now let’s deploy. Here are the complete manifests and scripts covering every layer of the architecture above.
Deployment playbook
Prerequisites checklist
Before running anything, confirm:
- Pull secret from
cloud.redhat.comdownloaded aspull-secret.json - DNS wildcard
*.apps.<cluster>.<domain>andapi.<cluster>.<domain>resolving - 3 master nodes: 8 vCPU / 32 GB RAM / 120 GB OS disk each
- Minimum 3 worker nodes: 8 vCPU / 32 GB RAM / 100 GB OS disk + data disks
- NTP synchronized across all nodes
openshift-install,oc, andhelmbinaries installed
1 · Install config — install-config.yaml
apiVersion: v1baseDomain: example.commetadata: name: prod-clustercontrolPlane: architecture: amd64 hyperthreading: Enabled name: master replicas: 3 platform: vsphere: # swap for aws/azure/baremetal as needed cpus: 8 coresPerSocket: 4 memoryMB: 32768 osDisk: diskSizeGB: 120compute: - architecture: amd64 hyperthreading: Enabled name: worker replicas: 3 platform: vsphere: cpus: 8 coresPerSocket: 4 memoryMB: 32768 osDisk: diskSizeGB: 100networking: clusterNetwork: - cidr: 10.128.0.0/14 hostPrefix: 23 machineNetwork: - cidr: 192.168.10.0/24 networkType: OVNKubernetes serviceNetwork: - 172.30.0.0/16platform: vsphere: vcenter: vcenter.example.com username: administrator@vsphere.local password: "{{ VCENTER_PASSWORD }}" datacenter: DC1 defaultDatastore: datastore1 folder: /DC1/vm/OCP network: VM NetworkpullSecret: '{{ PULL_SECRET }}'sshKey: '{{ SSH_PUBLIC_KEY }}'fips: false
# Bootstrap the clusteropenshift-install create cluster --dir ./install-config --log-level=info
2 · Infrastructure MachineConfigPool — move platform workloads off workers
# infra-mcp.yamlapiVersion: machineconfiguration.openshift.io/v1kind: MachineConfigPoolmetadata: name: infraspec: machineConfigSelector: matchExpressions: - key: machineconfiguration.openshift.io/role operator: In values: [worker, infra] nodeSelector: matchLabels: node-role.kubernetes.io/infra: ""
# Label your infrastructure nodesoc label node infra-node-1 infra-node-2 infra-node-3 \ node-role.kubernetes.io/infra=""# Apply the MachineConfigPooloc apply -f infra-mcp.yaml# Move the ingress controller to infra nodesoc patch ingresscontroller/default -n openshift-ingress-operator \ --type=merge -p '{ "spec": { "nodePlacement": { "nodeSelector": { "matchLabels": {"node-role.kubernetes.io/infra": ""} }, "tolerations": [{ "key": "node-role.kubernetes.io/infra", "effect": "NoSchedule" }] } } }'# Move the internal image registry to infra nodesoc patch config/cluster -n openshift-image-registry \ --type=merge -p '{ "spec": { "nodeSelector": {"node-role.kubernetes.io/infra": ""}, "tolerations": [{ "key": "node-role.kubernetes.io/infra", "effect": "NoSchedule" }] } }'
3 · Cluster autoscaler + MachineAutoscaler
# cluster-autoscaler.yamlapiVersion: autoscaling.openshift.io/v1kind: ClusterAutoscalermetadata: name: defaultspec: resourceLimits: maxNodesTotal: 24 cores: min: 8 max: 192 memory: min: 4 max: 768 scaleDown: enabled: true delayAfterAdd: 10m delayAfterDelete: 5m delayAfterFailure: 30s unneededTime: 5m utilizationThreshold: "0.4"---apiVersion: autoscaling.openshift.io/v1beta1kind: MachineAutoscalermetadata: name: worker-autoscaler namespace: openshift-machine-apispec: minReplicas: 3 maxReplicas: 12 scaleTargetRef: apiVersion: machine.openshift.io/v1beta1 kind: MachineSet name: prod-cluster-worker
4 · NetworkPolicy — production namespace baseline
# netpol-production.yamlapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-all namespace: productionspec: podSelector: {} policyTypes: [Ingress, Egress]---apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-from-ingress namespace: productionspec: podSelector: {} ingress: - from: - namespaceSelector: matchLabels: network.openshift.io/policy-group: ingress---apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-same-namespace namespace: productionspec: podSelector: {} ingress: - from: - podSelector: {} egress: - to: - podSelector: {}---apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-dns namespace: productionspec: podSelector: {} egress: - ports: - protocol: UDP port: 53 - protocol: TCP port: 53
5 · ResourceQuota + LimitRange
# quota-production.yamlapiVersion: v1kind: ResourceQuotametadata: name: production-quota namespace: productionspec: hard: requests.cpu: "40" requests.memory: 80Gi limits.cpu: "80" limits.memory: 160Gi persistentvolumeclaims: "20" pods: "200" services: "50" services.loadbalancers: "2"---apiVersion: v1kind: LimitRangemetadata: name: production-limits namespace: productionspec: limits: - type: Container default: cpu: 500m memory: 512Mi defaultRequest: cpu: 100m memory: 128Mi max: cpu: "8" memory: 16Gi - type: Pod max: cpu: "16" memory: 32Gi - type: PersistentVolumeClaim max: storage: 500Gi
6 · LDAP / OAuth identity provider
# oauth-ldap.yamlapiVersion: config.openshift.io/v1kind: OAuthmetadata: name: clusterspec: identityProviders: - name: corporate-ldap mappingMethod: claim type: LDAP ldap: attributes: id: [dn] email: [mail] name: [cn] preferredUsername: [sAMAccountName] bindDN: "CN=ocp-bind,OU=ServiceAccounts,DC=example,DC=com" bindPassword: name: ldap-bind-secret ca: name: ldap-ca-cert insecure: false url: "ldaps://ldap.example.com/OU=Users,DC=example,DC=com?sAMAccountName?sub" tokenConfig: accessTokenMaxAgeSeconds: 86400
# Create bind password secretoc create secret generic ldap-bind-secret \ --from-literal=bindPassword='<BIND_PASSWORD>' \ -n openshift-config# Create CA configmapoc create configmap ldap-ca-cert \ --from-file=ca.crt=/path/to/ldap-ca.crt \ -n openshift-configoc apply -f oauth-ldap.yaml# Remove the default kubeadmin after verifying LDAP loginoc delete secret kubeadmin -n kube-system
7 · RBAC — cluster roles for enterprise teams
# rbac-teams.yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: name: platform-adminsroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-adminsubjects: - kind: Group name: ocp-platform-admins # synced from LDAP apiGroup: rbac.authorization.k8s.io---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: dev-team-edit namespace: productionroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: editsubjects: - kind: Group name: dev-team apiGroup: rbac.authorization.k8s.io---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: qa-team-view namespace: productionroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: viewsubjects: - kind: Group name: qa-team apiGroup: rbac.authorization.k8s.io
8 · OpenShift GitOps (ArgoCD) — operator + App of Apps
# Install OpenShift GitOps operator via CLIcat <<EOF | oc apply -f -apiVersion: operators.coreos.com/v1alpha1kind: Subscriptionmetadata: name: openshift-gitops-operator namespace: openshift-operatorsspec: channel: latest name: openshift-gitops-operator source: redhat-operators sourceNamespace: openshift-marketplaceEOF# Wait for ArgoCD instance to be readyoc wait --for=condition=Available deployment/openshift-gitops-server \ -n openshift-gitops --timeout=300s
# app-of-apps.yaml — root ArgoCD applicationapiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: cluster-config namespace: openshift-gitopsspec: project: default source: repoURL: https://github.com/your-org/ocp-cluster-config targetRevision: main path: apps destination: server: https://kubernetes.default.svc namespace: openshift-gitops syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true - PrunePropagationPolicy=foreground
9 · Cluster monitoring customization
# monitoring-config.yamlapiVersion: v1kind: ConfigMapmetadata: name: cluster-monitoring-config namespace: openshift-monitoringdata: config.yaml: | prometheusK8s: nodeSelector: node-role.kubernetes.io/infra: "" tolerations: - key: node-role.kubernetes.io/infra effect: NoSchedule retention: 15d volumeClaimTemplate: spec: storageClassName: ocs-storagecluster-ceph-rbd resources: requests: storage: 500Gi alertmanagerMain: nodeSelector: node-role.kubernetes.io/infra: "" tolerations: - key: node-role.kubernetes.io/infra effect: NoSchedule grafana: enabled: true telemeterClient: enabled: false # disable telemetry for air-gapped / regulated envs
10 · etcd encryption + backup
# etcd-encryption.yamlapiVersion: config.openshift.io/v1kind: APIServermetadata: name: clusterspec: encryption: type: aescbc # enables encryption-at-rest for etcd
# Automated etcd backup — run as a CronJob on master nodescat <<'EOF' | oc apply -f -apiVersion: batch/v1kind: CronJobmetadata: name: etcd-backup namespace: openshift-etcdspec: schedule: "0 2 * * *" jobTemplate: spec: template: spec: hostNetwork: true hostPID: true nodeSelector: node-role.kubernetes.io/master: "" tolerations: - operator: Exists serviceAccountName: etcd-backup-sa containers: - name: etcd-backup image: registry.redhat.io/openshift4/ose-cli:latest command: - /bin/bash - -c - | /usr/local/bin/cluster-backup.sh /home/core/etcd-backup volumeMounts: - mountPath: /home/core/etcd-backup name: backup-dir volumes: - name: backup-dir hostPath: path: /home/core/etcd-backup restartPolicy: OnFailureEOF
11 · Image policy — only allow signed images in production
# image-policy.yamlapiVersion: config.openshift.io/v1alpha1kind: ImagePolicymetadata: name: production-image-policyspec: scopes: - "registry.example.com/prod/*" policy: rootOfTrust: policyType: PublicKey publicKey: keyData: "{{ BASE64_COSIGN_PUBLIC_KEY }}" rekorKeyData: "{{ BASE64_REKOR_PUBLIC_KEY }}"
12 · Post-install validation
#!/usr/bin/env bashset -euo pipefailecho "=== Cluster health ==="oc get nodesoc get clusteroperators | grep -v "True.*False.*False"echo "=== etcd member health ==="oc rsh -n openshift-etcd etcd-$(oc get nodes -l node-role.kubernetes.io/master \ -o jsonpath='{.items[0].metadata.name}') \ etcdctl member list --write-out=tableecho "=== Operator status ==="oc get co | awk '$3=="False" || $4=="True" || $5=="True"'echo "=== Certificate expiry (alert if < 30d) ==="oc get secret -A -o json | \ python3 -c "import json,sys,base64,datetimefrom cryptography import x509data=json.load(sys.stdin)for item in data['items']: for k,v in item.get('data',{}).items(): if k.endswith('.crt'): try: cert=x509.load_pem_x509_certificate(base64.b64decode(v)) days=(cert.not_valid_after_utc.replace(tzinfo=None)-datetime.datetime.utcnow()).days if days < 30: print(f\"WARN: {item['metadata']['namespace']}/{item['metadata']['name']} expires in {days}d\") except: pass"echo "=== MachineConfigPool status ==="oc get mcpecho "=== All checks complete ==="
Enterprise best-practice summary
| Concern | Decision |
|---|---|
| Network plugin | OVN-Kubernetes (required for EgressIP, EgressFirewall, network segmentation) |
| Node topology | Separate master / infra / compute pools — never mix roles |
| etcd | Encryption at rest (aescbc) + nightly backup to S3 or NFS |
| Identity | LDAP/OIDC with group sync — remove kubeadmin after day 1 |
| Multi-tenancy | default-deny-all NetworkPolicy per namespace + ResourceQuota + LimitRange |
| SCCs | restricted-v2 for all workloads unless a specific capability is justified |
| Image supply chain | Build → scan (ACS/Trivy) → sign (cosign) → promote digest (never mutable tags) |
| GitOps | ArgoCD App of Apps, selfHeal: true, manual sync gate for production |
| Observability | Prometheus on infra nodes, 15-day retention, alert to PagerDuty/Slack |
| Cluster upgrades | Stable channel, pause MCPs before upgrade, upgrade masters then workers |
| Backup | etcd nightly + Velero for namespace-level PV snapshots |