03/24/26

Why Kubernetes Is So Complicated (And What to Use Instead)

The layers of complexity behind container orchestration

7 Min Read

Kubernetes complexity comes from specific layers: YAML configuration, networking, storage, RBAC, debugging across distributed surfaces, and the upgrade treadmill. Each layer exists for a reason, but they compound quickly for teams that don't need all of them. Understanding where the complexity actually lives helps clarify whether you need the full stack or whether a simpler deployment model, like managed containers or infrastructure-from-code with Encore, would work.

YAML manifests and configuration sprawl

Every Kubernetes resource is defined in YAML. A single service that accepts HTTP traffic and connects to a database needs, at minimum: a Deployment (with container spec, resource limits, readiness probes, liveness probes, environment variables), a Service, an Ingress or Gateway resource, a ConfigMap or Secret for configuration, and a ServiceAccount. That's five YAML files before you've written a line of application logic.

Each manifest has its own schema, its own set of fields, and its own failure modes. Misindent a YAML block and the error message points you somewhere unhelpful. Forget a label selector and your Service quietly routes to nothing. The feedback loop between writing configuration and seeing the result is slow, especially compared to writing application code where your editor catches most mistakes immediately.

Teams deal with this by adding Helm for templating, Kustomize for overlays, or both. These tools reduce duplication but add their own abstraction layers, their own bugs, and their own learning curves. The configuration surface area grows rather than shrinks.

Networking has multiple overlapping layers

Kubernetes networking is where most teams hit their first real wall. A request from outside the cluster to your application passes through at least three layers: an Ingress controller (or Gateway API resource), a Service, and then a Pod. Each layer has its own configuration, its own failure modes, and its own debugging tools.

Services handle internal routing using label selectors and expose ClusterIP, NodePort, or LoadBalancer types depending on the use case. Ingress (or the newer Gateway API) handles external traffic routing, TLS termination, and path-based routing. Network Policies control which Pods can talk to which other Pods. And CoreDNS handles service discovery, so my-service.my-namespace.svc.cluster.local resolves to the right ClusterIP.

When something goes wrong in this stack, figuring out which layer is the problem takes real expertise. Is the Ingress controller misconfigured? Is the Service selector not matching the Pod labels? Is a NetworkPolicy blocking traffic? Is DNS resolution failing? Each scenario looks similar from the outside (the request fails), but the fix is completely different.

Storage requires its own mental model

Stateful applications on Kubernetes introduce another layer of configuration through PersistentVolumes, PersistentVolumeClaims, and StorageClasses. The abstraction separates the "I need 10GB of storage" request (the PVC) from the "here's actual disk on a cloud provider" provisioning (the PV), with StorageClasses defining the rules for dynamic provisioning.

In practice, this means understanding volume binding modes, reclaim policies, access modes (ReadWriteOnce vs. ReadWriteMany), and the specific behavior of your cloud provider's CSI driver. A misconfigured StorageClass can leave your PVCs stuck in a Pending state with error messages that require reading the CSI driver's documentation to decode.

RBAC and security configuration

Kubernetes RBAC (Role-Based Access Control) controls what users and service accounts can do inside the cluster. The model uses four resources: Roles, ClusterRoles, RoleBindings, and ClusterRoleBindings. Roles define permissions (which API verbs on which resource types), and Bindings attach those permissions to subjects.

Getting RBAC right is critical for multi-tenant clusters and production security, but the configuration is verbose and the debugging experience is poor. A permission denied error tells you that access was denied. It doesn't tell you which Role is missing, which Binding needs to change, or which permission the request required. You end up iterating through kubectl auth can-i commands and cross-referencing multiple YAML files to find the gap.

On top of RBAC, production clusters typically need Pod Security Standards, SecurityContexts for individual containers, and possibly an admission controller like OPA Gatekeeper or Kyverno to enforce organizational policies. Each layer adds configuration and potential failure points.

Debugging spans multiple surfaces

When something breaks in a Kubernetes environment, the problem could be in your application, the Pod configuration, the node the Pod is scheduled on, the networking layer, or the control plane itself. Debugging requires checking kubectl logs for application output, kubectl describe for events and scheduling decisions, kubectl get events for cluster-wide issues, and sometimes the kubelet or cloud provider logs on the node.

Distributed logging across multiple replicas of a service means a single request might touch three Pods, and correlating those logs requires either a centralized logging stack (Elasticsearch/Fluentd/Kibana, Loki/Grafana, or Datadog) or manual work with kubectl logs --selector. Most teams end up deploying an entire observability platform alongside their application just to maintain the same debugging experience they had with a single-server deployment.

The upgrade treadmill

Kubernetes releases three minor versions per year, and each version is supported for roughly 14 months. Managed services like EKS and GKE give you some buffer, but eventually you have to upgrade. Each upgrade can introduce API deprecations, behavior changes, and compatibility issues with the add-ons running in your cluster.

A typical upgrade cycle involves: updating the control plane version, updating node AMIs or node images, verifying that your Ingress controller, CNI plugin, CSI drivers, and cert-manager are compatible with the new version, testing workloads in a staging environment, and then rolling out to production. If you run multiple clusters, multiply that effort. If your Helm charts reference deprecated APIs, you'll discover that during the upgrade when things stop working.

Teams without a dedicated platform engineer often fall behind on upgrades, which creates a compounding problem: skipping versions makes the eventual upgrade harder, and running unsupported versions means missing security patches.

When the complexity is justified

All of this complexity exists to solve real problems. Organizations running 50+ services across multiple teams benefit from Kubernetes' namespace isolation, RBAC model, and consistent deployment API. Companies with dedicated platform teams can absorb the maintenance overhead and build internal tooling that makes the developer experience smoother. At sufficient scale, the alternatives (managing individual VMs or cloud-native services per application) become their own kind of complex.

If you have a platform team of two or more engineers, multiple teams deploying independently, and enough services that a consistent orchestration layer saves more time than it costs, Kubernetes is a reasonable choice. The operational investment compounds positively at that scale.

What to use instead for smaller teams

For teams without dedicated platform engineers, several options offer a better complexity-to-value ratio.

Managed container services like AWS ECS Fargate or Google Cloud Run handle container orchestration without exposing the Kubernetes abstraction layer. You still define containers and configure networking, but the surface area is smaller. ECS Fargate requires task definitions and load balancer configuration. Cloud Run is simpler still, handling scaling and TLS automatically, though it constrains you to request-driven workloads.

PaaS platforms like Railway, Render, and Fly.io abstract away infrastructure almost entirely. Push code, get a running service. The trade-off is limited control over the underlying resources and potential issues with compliance or data residency when the infrastructure lives in the provider's account.

Infrastructure-from-code takes a different approach. Instead of configuring infrastructure separately from your application, you declare what your application needs in your code, and the platform provisions the cloud resources automatically.

Encore Cloud works this way. You write a TypeScript (or Go) backend using Encore's open-source framework, where infrastructure like databases, Pub/Sub, and cron jobs are part of your application code. When you deploy, Encore provisions the corresponding AWS or GCP resources in your own cloud account. The YAML manifests, Helm charts, Ingress controllers, RBAC configuration, and upgrade treadmill all go away. The infrastructure exists because your code declared it, and Encore handles the provisioning, networking, and security configuration.

The resources it creates are standard cloud primitives: ECS Fargate, RDS, S3, SQS, CloudWatch. You can inspect them in the AWS console. If you stop using Encore, you can export Docker images and manage the infrastructure yourself. The point isn't lock-in; it's eliminating the configuration work that sits between your code and production.

Choosing your trade-off

Every option trades off control for simplicity. Kubernetes gives you maximum control and maximum complexity. Managed containers reduce the complexity while keeping you close to the cloud provider. PaaS platforms prioritize speed at the cost of flexibility. Infrastructure-from-code eliminates configuration while keeping production resources in your own cloud account.

The question isn't whether Kubernetes is good technology. It is. The question is whether the problems it solves are the problems your team actually has.

Ready to escape the maze of complexity?

Encore Cloud is the development platform for building robust type-safe distributed systems with declarative infrastructure.