✦ For everyone, free.

Practical knowledge for real and everyday life

Home

18.3.1.5 Kubernetes Deployment Portability

A focused guide to Kubernetes Deployment Portability, connecting core concepts with practical Docker and container operations.

Kubernetes deployment portability concerns what genuinely moves unchanged between different Kubernetes distributions and cloud providers, EKS, GKE, AKS, a self-managed on-premises cluster, versus what does not, since the core API and the OCI image format are portable by design, while several common, practically necessary configuration details are deliberately provider-specific and require explicit handling to avoid quiet, hard-to-diagnose lock-in.

What is genuinely portable

The core Kubernetes API, Deployments, Services, ConfigMaps, and the underlying OCI image format itself, work identically across any conformant Kubernetes distribution, since Kubernetes conformance testing specifically verifies this core API behavior remains consistent regardless of provider:

apiVersion: apps/v1
kind: Deployment
spec:
  replicas: 3
  template:
    spec:
      containers:
        - image: registry.example.com/my-api:1.4.2

This basic manifest deploys identically on any conformant cluster, which is the genuine, reliable portability guarantee Kubernetes provides at its core, distinct from the provider-specific extensions layered on top of that core that most real deployments also depend on to some degree.

Storage classes are provider-specific

A StorageClass referenced by a PersistentVolumeClaim maps to entirely different, provider-specific underlying storage implementations, EBS on AWS, Persistent Disk on GCP, Azure Disk on Azure, with no universal, portable name guaranteed to exist identically across every provider:

apiVersion: v1
kind: PersistentVolumeClaim
spec:
  storageClassName: gp3

This manifest works on a cluster with a gp3 storage class defined (typically AWS), but fails or behaves unexpectedly on a cluster from a different provider with no class of that exact name; templating the storage class name as a per-environment variable, rather than hardcoding a single provider's naming convention directly, is the practical mitigation for this specific portability gap.

LoadBalancer service type behavior differs by provider

A Service of type LoadBalancer triggers each cloud provider's own, distinct mechanism for actually provisioning an external load balancer, with meaningfully different annotations and configuration options available depending on which provider's specific implementation is handling the request:

apiVersion: v1
kind: Service
metadata:
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: nlb
spec:
  type: LoadBalancer

This annotation is meaningful only on AWS-based clusters; deploying the identical manifest to a different provider either ignores the annotation silently or fails depending on that provider's own handling of unrecognized annotations, which is exactly the kind of quiet, easily overlooked portability gap worth being deliberate about.

Ingress controller differences

While the Ingress resource itself is part of the portable core API, its actual behavior depends entirely on which ingress controller is installed and handling it, and different controllers, NGINX Ingress, Traefik, a cloud provider's own managed controller, support meaningfully different annotation sets and feature scope beyond the portable core specification:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /

This annotation has meaning only if the NGINX ingress controller specifically is actually installed and processing this resource; a cluster using a different controller would ignore it, again silently, producing behavior that diverges from what was actually intended without any explicit error indicating the mismatch.

Using templating tools to manage portability gaps explicitly

Helm and Kustomize both provide a structured way to express the genuinely portable core manifest once while explicitly parameterizing the provider-specific details, storage class names, load balancer annotations, ingress controller-specific configuration, that differ between deployment targets:

storageClassName: {{ .Values.storageClass }}
helm install my-api ./chart --set storageClass=gp3 --namespace production-aws
helm install my-api ./chart --set storageClass=standard --namespace production-gcp

This approach makes the actual, real portability gaps explicit and deliberately managed, rather than either hardcoding one provider's specific values directly into the base manifest, or assuming a manifest will work unchanged everywhere when it genuinely will not for these specific, provider-dependent fields.

Avoiding unintentional vendor lock-in

Reviewing manifests periodically for accumulated, provider-specific annotations and configuration that crept in incrementally, often added during a specific incident or feature need without anyone considering the longer-term portability implication, catches drift toward unintentional lock-in before a genuine multi-provider or migration need makes that drift considerably more costly to untangle.

grep -rn "aws-load-balancer\|gcp\|azure" manifests/

A search like this across a project's manifests surfaces exactly how much provider-specific configuration has actually accumulated, which is a useful, concrete starting point for deciding whether that accumulated coupling is acceptable given the organization's actual, realistic likelihood of ever needing to migrate or run multi-provider.

Common mistakes

  • Assuming the entire Kubernetes manifest format is universally portable, missing that several commonly used, practically necessary fields, storage classes, load balancer annotations, ingress controller-specific configuration, are deliberately provider-specific.
  • Hardcoding a single provider's storage class or load balancer annotation directly into a base manifest intended to be reused across multiple deployment targets.
  • Not noticing that an unrecognized, provider-specific annotation is silently ignored rather than producing an explicit error, masking a portability gap until the actual resulting behavior diverges from what was intended.
  • Not using a templating tool like Helm or Kustomize to explicitly parameterize the genuinely provider-specific portions of a manifest, leaving portability gaps implicit and unmanaged.
  • Never reviewing accumulated provider-specific configuration periodically, allowing unintentional vendor lock-in to deepen gradually without anyone deliberately deciding whether that coupling is actually acceptable.

Kubernetes deployment portability holds reliably at the core API and OCI image level, but several practically necessary configuration details, storage, load balancing, ingress, are deliberately provider-specific, and managing this honestly with explicit templating, rather than assuming uniform portability or accepting unreviewed, accumulated lock-in, is what keeps a multi-provider or future-migration option genuinely realistic rather than only theoretically available.