
TL;DR
|
You wrote the feature. It works locally. Then you spend the next two hours on things that have nothing to do with the feature: a Terraform plan that wants to replace a database you didn't touch, a Kubernetes manifest that needs a new ingress rule, an IAM policy that's one permission short of what the deploy needs. None of this is the job. All of it is the job today.
Here's what that list actually looks like, and why none of it should be sitting on your plate.
Key takeaway: State files are supposed to be an implementation detail. In practice they're a recurring source of merge conflicts, drift, and multi-hour debugging sessions that have nothing to do with your application code.
You didn't write a bug. You're running terraform plan and staring at a diff that wants to replace your production database, not update it, tear it down and rebuild it from scratch, because someone changed a field last Tuesday (an engine version, a storage type, a name) that Terraform can only apply by destroying the resource and recreating it. Nobody told you that field was immutable. Terraform tells you now, right before it's about to happen.
Locking, remote backends, workspace strategy, drift detection: these are all legitimate infrastructure concerns. They are not feature work. Every hour spent reconciling state, or double-checking whether a plan's destructive diff is expected or a five-alarm mistake, is an hour where the thing you're actually building doesn't move forward.
Key takeaway: A Kubernetes deployment for a single service can require five or six separate resources before anything runs. Maintaining that surface area is a full discipline on its own, and it scales with every service you add, not with the value each service delivers.
Deployment, Service, Ingress or the newer Gateway API's HTTPRoute, ConfigMap, Secret, maybe an HPA if you want autoscaling. Whether you're hand-writing these or managing them through Helm templates and Kustomize overlays, the underlying surface area doesn't shrink, it just moves from raw files into template logic that fails in its own particular ways: a misaligned indent function silently dropping a field, a values override that doesn't apply where you expected. None of this describes what your application does. All of it describes how to get a container running in a scheduler.
The ratio is the tell. A microservice with 200 lines of business logic can carry 150 lines of Kubernetes configuration (a rough but common ratio) to keep it alive. That's not a complexity budget you signed up for when you wrote the feature.
Key takeaway: Getting a deploy pipeline the exact permissions it needs, no more and no less, is security work. Most developers either over-scope out of frustration or under-scope and spend an afternoon debugging an access denied error that has nothing to do with their code.
You need your CI pipeline's role to upload build artifacts to one S3 bucket. Write the policy against the bucket ARN alone, and every PutObject call fails, because object-level actions need the object path, the ARN with /* on the end, not just the bucket itself. Get that wrong and the pipeline fails with a bare AccessDenied error that says nothing about which of a dozen possible causes is the actual one. Go too far the other way and grant s3:* on every resource in the account to make the error go away, and you've just handed a build pipeline the ability to touch every bucket your company owns.
This is real expertise. It's just not the expertise a feature developer was hired to build.
Key takeaway: Getting two services to talk to each other reliably touches DNS, service mesh configuration, firewall rules, and load balancer health checks. None of it shows up in the feature you shipped. All of it can take the feature down if it's wrong.
Your service needs to call another service. In a well-run platform that's a hostname and a port. In practice it's a security group rule, a target group registration, a health check path that has to return exactly the right status code, and a client somewhere holding a cached connection to a pod that already got replaced, because the health check window was a few seconds longer than your deploy script assumed.
None of this is a debugging skill you're building toward a better feature. It's a separate discipline that happens to sit between you and shipping.
Key takeaway: Keeping every environment's secrets current, and rotating them without downtime, is operational work that has to happen correctly every time. One missed environment is a production incident waiting for the right moment.
A database credential rotates. It updates in the secrets manager, and the target environment picks up its own new value automatically on its next deploy, without someone manually copying values into staging and production configs by hand. Miss that automatic pickup somewhere and you've got a staging deploy that mysteriously can't connect to anything, discovered the next time someone needs staging to actually work.
Key takeaway: None of the work above disappears when a platform owns it. It moves to a layer where it's handled once, consistently, instead of being reinvented by every developer on every project.
The point isn't that this work is unnecessary. State management, access scoping, networking, and secrets rotation are all real requirements of running software in production. The question is who does them and how many times.
When the platform owns the delivery layer, this work happens once, in code, and applies automatically to every environment. A new branch gets a production-identical environment without anyone writing a Terraform module for it. A platform that owns this layer scopes deploy access as part of the environment model, rather than leaving each developer to hand-write an IAM policy from scratch. A secret updates in one place, and every environment picks it up on its next deploy, rather than a person manually propagating it environment by environment.
That's the actual trade. Not less rigor. The same rigor, applied once, instead of reinvented by every developer who touches deploy.
If you've spent more time this month on YAML than on your actual feature, that ratio is the signal. It's not a reflection of your skill. It's a reflection of where the work is sitting.
Read the developer guide to building apps without the infrastructure burden
Doesn't someone still have to configure the platform itself?
Yes, but once, not per project and not per developer. The difference is between configuring a shared delivery layer one time and every team reconstructing their own version of it from scratch. The work exists either way. What changes is whether it's duplicated across every project or centralized once.
Is this just infrastructure as code with extra steps?
No. Terraform and Kubernetes manifests are already declarative, that's not the distinction. The difference is between low-level provisioning definitions, where you're still declaring individual VPCs, IAM roles, and security groups yourself, and a single high-level application definition, where you describe what your app needs and the platform handles the low-level provisioning underneath it.
What if my team already has this automated with our own tooling?
Some teams do, and if your internal platform team has already built and maintained this layer well, the marginal benefit is smaller. The pattern to watch for is whether that internal tooling is actually maintained as a product, with an owner and a roadmap, or whether it's a collection of scripts that one person understands and everyone else works around.
Does this mean I lose control over my infrastructure choices?
No. The services you use, the language you write in, and the architecture you choose stay yours. What changes is who writes the Terraform, the Kubernetes manifests, and the IAM policies that make those choices actually run. Those are implementation details of delivery, not decisions about what you're building.