OpenShift runs Kubernetes underneath, so most of what you deploy on it already works on any conformant cluster. Deployments, Services, ConfigMaps, Secrets, StatefulSets, and Helm charts move without changes. The work in an OpenShift to Kubernetes migration sits in a short list of OpenShift-only APIs: Routes, DeploymentConfigs, Security Context Constraints, Templates, and the build objects around them. This post walks through each one, shows the standard Kubernetes equivalent, and ends with a migration plan you can run in parallel with your existing clusters.
If you are still deciding whether to leave OpenShift, start with OpenShift vs Kubernetes, which covers what OpenShift adds and what it costs.
What changes and what stays the same
| OpenShift resource | Kubernetes equivalent | Effort |
|---|---|---|
| Deployment, StatefulSet, DaemonSet, Job, CronJob | Unchanged | None |
| Service, ConfigMap, Secret, PersistentVolumeClaim | Unchanged | None, apart from storage classes |
| Helm charts | Unchanged | Values for ingress and security context |
| Route | Gateway API HTTPRoute, or Ingress | Low |
| DeploymentConfig | Deployment | Low to medium, depending on triggers and hooks |
| Security Context Constraints | Pod Security Admission, plus explicit securityContext | Medium |
| Template | Helm chart or Kustomize | Low to medium |
| BuildConfig, ImageStream | CI pipeline and an OCI registry | Medium |
| Project | Namespace with quotas, limits, and network policies | Low |
| Operators from OperatorHub | Helm charts or operator manifests | Low to medium |
Routes to Gateway API or Ingress
A Route exposes a Service on a hostname, with optional TLS termination and traffic splitting between backends. Kubernetes has two standard APIs for this. Ingress is the older one, and it relies on controller-specific annotations for anything beyond host and path routing. Gateway API is its successor, with traffic splitting, header matching, redirects, and TLS options built into the API itself. Ingress NGINX, the most common Ingress controller, was retired by the Kubernetes project in March 2026, which makes Gateway API the natural target for a migration that starts now.
A typical Route with edge TLS and a canary backend looks like this:
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: shop
spec:
host: shop.example.com
to:
kind: Service
name: shop
weight: 90
alternateBackends:
- kind: Service
name: shop-canary
weight: 10
port:
targetPort: http
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect
The Gateway API version splits responsibilities. A platform team runs a shared Gateway that owns the listeners and certificates, and each application team attaches an HTTPRoute to it:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
spec:
parentRefs:
- name: public
namespace: gateway-system
sectionName: https
hostnames:
- shop.example.com
rules:
- backendRefs:
- name: shop
port: 8080
weight: 90
- name: shop-canary
port: 8080
weight: 10
Edge termination maps to an HTTPS listener on the Gateway with a certificate reference, usually issued by cert-manager. The Redirect policy for plain HTTP becomes a small HTTPRoute on the HTTP listener with a RequestRedirect filter. Passthrough termination maps to a TLSRoute, and re-encrypt maps to a backend TLS policy, so check that your Gateway implementation supports those two before you migrate Routes that use them. Route hostnames generated from the cluster’s wildcard domain need real DNS names on the new cluster.
Cloudfleet works with any Ingress controller or Gateway API implementation, and HTTP traffic enters through the load balancers Cloudfleet provisions for the Gateway’s Service.
DeploymentConfigs to Deployments
DeploymentConfigs are deprecated since OpenShift 4.14, and Red Hat recommends Deployments for new workloads, so this conversion is worth doing even if you stay on OpenShift. The Pod template is identical. The differences are in the fields around it:
| DeploymentConfig | Deployment |
|---|---|
apiVersion: apps.openshift.io/v1 | apiVersion: apps/v1 |
spec.selector as a plain label map | spec.selector.matchLabels |
strategy.type: Rolling with rollingParams | strategy.type: RollingUpdate with rollingUpdate |
strategy.type: Recreate | strategy.type: Recreate |
triggers of type ImageChange and ConfigChange | No equivalent: a new image tag in Git or an image automation tool triggers the rollout |
Lifecycle hooks (pre, mid, post) | Helm hooks, Argo CD sync hooks, or a Job that runs before the rollout |
oc rollout latest, oc rollback | kubectl rollout restart, kubectl rollout undo |
The triggers are the part that needs a decision. On OpenShift, an ImageChange trigger rolls out a new version as soon as an ImageStream tag moves. On Kubernetes, the image reference in the Deployment changes instead, usually through your CI pipeline committing a new tag to Git, or through Flux image automation or Argo CD Image Updater. The result is the same, and every rollout now has a commit behind it.
Lifecycle hooks typically run database migrations. A Helm pre-upgrade hook or an Argo CD PreSync hook runs the same container as a Job before the new Pods start.
Security Context Constraints to Pod Security Admission
Security Context Constraints are the change most likely to surprise you, because SCCs do two things at once. They validate Pods, like Pod Security Admission does, and they also mutate them. The default restricted-v2 SCC assigns every container a random UID from the namespace’s range, drops all Linux capabilities, sets the seccomp profile, and blocks privilege escalation, whether or not the manifest asks for it. Many manifests written for OpenShift therefore have no securityContext at all.
Pod Security Admission only validates. Label a namespace with the restricted level, and Pods are rejected unless their manifests set the right fields themselves:
apiVersion: v1
kind: Namespace
metadata:
name: shop
labels:
pod-security.kubernetes.io/enforce: restricted
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: shop
namespace: shop
spec:
selector:
matchLabels:
app: shop
template:
metadata:
labels:
app: shop
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: shop
image: registry.example.com/shop:1.8.2
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
The SCCs map to the Pod Security levels roughly as follows: restricted-v2 corresponds to restricted, nonroot-v2 and anyuid fit within baseline, and privileged corresponds to privileged. Workloads that need host access or extra capabilities belong in their own namespaces with a lower level.
The random UID deserves a second look. Images built for OpenShift usually make their writable directories owned by the root group, because OpenShift runs them with an arbitrary UID in group 0. On Kubernetes, the container runs as the user in the image or as runAsUser, so set an explicit non-root UID and an fsGroup that can write to mounted volumes. Images that declare USER root fail the restricted level and need a non-root user, either in the image or in the manifest.
To find out which SCC each workload uses today, read the openshift.io/scc annotation that OpenShift sets on every Pod:
oc get pods -A -o custom-columns='NAMESPACE:.metadata.namespace,POD:.metadata.name,SCC:.metadata.annotations.openshift\.io/scc'
Pods on restricted-v2 move to the restricted level with the fields above. Everything else needs a closer look. If you want Kubernetes to fill in secure defaults the way SCCs do, a policy engine such as Kyverno can mutate Pods before Pod Security Admission validates them.
Templates to Helm or Kustomize
An OpenShift Template is a list of objects with ${PARAMETER} placeholders, rendered with oc process. Templates map directly to Helm charts: parameters become values, and the objects become chart templates. Parameters generated with expressions such as [a-zA-Z0-9]{16} usually create passwords, and those are better handled by a secrets manager through the External Secrets Operator, or by Helm’s randAlphaNum for development environments.
When a Template exists only to stamp out a few environments, Kustomize is often the simpler target. Render the Template once with oc process, commit the output as a base, and keep the differences between environments in overlays.
Builds, ImageStreams, and Projects
BuildConfigs build images inside the cluster, often with Source-to-Image (S2I). On Kubernetes, builds usually move to the CI system you already use, such as GitHub Actions, GitLab CI, or Tekton, which pushes images to an OCI registry. Cloud Native Buildpacks replace S2I when you want builds without a Dockerfile. ImageStreams become plain image references with tags or digests in your registry.
A Project is a Namespace with extras that OpenShift applies when a team requests it: default quotas, limit ranges, network policies, and role bindings. On Kubernetes, the same defaults live in Git and are applied when the namespace is created, through your GitOps tool or a policy engine. OpenShift OAuth and groups map to your identity provider and Kubernetes RBAC role bindings.
A migration plan that keeps production running
The safest way to move is to run the new cluster next to OpenShift and shift traffic gradually.
- Take inventory. List the OpenShift-only objects with
oc get deploymentconfigs,routes,buildconfigs,imagestreams,templates -A, and record the SCC of every Pod with the command above. This list is the actual scope of the migration. - Convert manifests in Git. Replace Routes, DeploymentConfigs, and Templates, and add explicit security contexts. Keep the converted manifests deployable to OpenShift as well where possible, since Deployments and Ingress objects work there too.
- Prepare the target cluster. Install your Gateway API implementation, cert-manager, monitoring, and the operators your workloads need, and set Pod Security labels on the namespaces.
- Move stateful data. Use application-level replication for databases where you can, and Velero to back up and restore namespaces and persistent volumes where you cannot.
- Run both platforms in parallel. Deploy every application to the new cluster, test it, then move traffic with weighted DNS records, starting with a small share.
- Decommission. When the new cluster carries all traffic, remove the workloads from OpenShift and end the subscriptions at the next renewal.
With Cloudfleet, the target cluster can span the same places your OpenShift clusters run today. On-premises servers join as self-managed nodes with one command, cloud nodes are added by node auto-provisioning, and Cloudfleet operates the control plane, so there are no control plane machines to set up in your data center. Cloudfleet Kubernetes architects plan and run the migration with you, and the Migration Acceleration Program covers the overlap while both platforms run. The OpenShift alternative page has the details.
Frequently asked questions
Can I migrate from OpenShift to Kubernetes without downtime?
Yes. Run the new cluster in parallel, deploy every application to it, and move traffic gradually with weighted DNS. Stateful services need a replication or backup and restore step, which is the only part that can require a short maintenance window.
What is the Kubernetes equivalent of an OpenShift Route?
Gateway API HTTPRoute, or an Ingress. Gateway API covers Route features such as traffic splitting between backends without controller-specific annotations, and it is the recommended target for new migrations.
Are OpenShift DeploymentConfigs deprecated?
Yes. DeploymentConfigs are deprecated since OpenShift 4.14, and Red Hat recommends Kubernetes Deployments instead. Converting them is a good first step even before a migration.
What replaces Security Context Constraints in Kubernetes?
Pod Security Admission, which enforces the privileged, baseline, and restricted levels per namespace. Unlike SCCs, it does not set security fields for you, so manifests need an explicit securityContext. A policy engine such as Kyverno can add mutating defaults.
Do container images built for OpenShift run on Kubernetes?
Yes. They are standard OCI images. Images built for OpenShift’s random UIDs usually run without changes, as long as the manifest sets a non-root runAsUser and an fsGroup for writable volumes.

