<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: vivek Itp</title>
    <description>The latest articles on DEV Community by vivek Itp (@vivek_itp).</description>
    <link>https://dev.to/vivek_itp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4118048%2Fc6bc377d-ae65-4385-89f6-c3d3fe39cfbf.png</url>
      <title>DEV Community: vivek Itp</title>
      <link>https://dev.to/vivek_itp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vivek_itp"/>
    <language>en</language>
    <item>
      <title>Error messages are part of the platform</title>
      <dc:creator>vivek Itp</dc:creator>
      <pubDate>Wed, 09 Sep 2026 18:47:15 +0000</pubDate>
      <link>https://dev.to/vivek_itp/error-messages-are-part-of-the-platform-30lc</link>
      <guid>https://dev.to/vivek_itp/error-messages-are-part-of-the-platform-30lc</guid>
      <description>&lt;p&gt;Nobody reads a platform's documentation on a good day. On a good day the pipeline is green, the deploy went out, and the developer never thinks about the platform at all. The platform only gets read when something breaks, and what gets read is the error message.&lt;/p&gt;

&lt;p&gt;That makes the error message the most-read text a platform team ever writes. It is also, almost always, the text nobody on the platform team wrote on purpose. It is whatever the underlying tool printed, passed through untouched, with a stack trace attached.&lt;/p&gt;

&lt;p&gt;I spent a long time treating errors as something that happened to the platform rather than something the platform produced. This post is what changed my mind, and what I do differently now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between what it said and what it meant
&lt;/h2&gt;

&lt;p&gt;Here are the kinds of messages developers actually see, next to what the platform team knows they mean. The gap between the two columns is where support requests come from.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the developer saw&lt;/th&gt;
&lt;th&gt;What it actually meant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ImagePullBackOff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The image tag you deployed was never pushed. Your build job probably failed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exit code 137&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The runner ran out of memory. Not your code.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Error: forbidden: User cannot list resource "pods"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You are in the wrong namespace, or you were never granted access to this one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;KeyError: 'DATABASE_URL'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The secret for this environment was never created.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;x509: certificate signed by unknown authority&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The internal CA is not installed in your base image.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Error: no matches for kind "Ingress" in version "extensions/v1beta1"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The template you copied is three years old.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row in the right column is something the platform team could have printed instead. None of them did, because the message came from a tool underneath the platform and nobody caught it on the way up.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. An error has three jobs
&lt;/h2&gt;

&lt;p&gt;A useful error message answers three questions, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What broke?&lt;/strong&gt; In one plain sentence, from the developer's point of view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why?&lt;/strong&gt; The most likely cause, stated as a fact if it is known and as a guess if it is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What do I do now?&lt;/strong&gt; A command, a link, or a name. Something the developer can act on in the next minute.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If a message does only the first job, the developer knows they are stuck. If it does all three, the developer is usually unstuck before they think of asking anyone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this looked like for me&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first error I rewrote was the image pull failure. The raw version was &lt;code&gt;ImagePullBackOff&lt;/code&gt; with a pod name. Developers would see it, check that their code compiled, re-run the deploy, see it again, and then ask in the channel.&lt;/p&gt;

&lt;p&gt;The rewritten version, printed by the deploy step, read something like: &lt;em&gt;"Deploy failed: image tag &lt;code&gt;abc1234&lt;/code&gt; does not exist in the registry. This usually means the build job for this commit failed or has not finished. Check the build job here: link."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first line was the same information. The second and third lines were the difference between a support request and a self-fix. Questions about that error mostly stopped after the change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Write for the person who did not build the system
&lt;/h2&gt;

&lt;p&gt;Most error messages are written, implicitly, for the person who wrote the code that raised them. They use internal names, assume knowledge of the architecture, and skip context that the author never needed because they already had it in their head.&lt;/p&gt;

&lt;p&gt;A developer on a product team does not have that context. They do not know that "reconciler" means the deploy controller, that "upstream" means the service behind the proxy, or that error code 4012 is the one about quotas.&lt;/p&gt;

&lt;p&gt;The test I use: would a competent engineer who joined last week and has never read our platform code understand this message? If not, it is not finished.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The message with an internal name in it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We had a validation step that rejected service configs with a message naming the internal component that did the checking. Something like &lt;em&gt;"rejected by policy-gate: rule R7 failed."&lt;/em&gt; Everyone on the platform team knew what R7 was. Nobody else did, and there was nothing in the message to search for.&lt;/p&gt;

&lt;p&gt;The fix was to make the rule print its own explanation: &lt;em&gt;"Service config rejected: &lt;code&gt;memory&lt;/code&gt; must be at most 4Gi for services without a capacity exception. Yours is 8Gi. To request an exception, see: link."&lt;/em&gt; Same rule, same check. The developer just got to read the reason instead of the rule number.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Put the fix in the message
&lt;/h2&gt;

&lt;p&gt;The most valuable thing an error can contain is the next command to run. Not a description of the fix. The fix.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A lint failure should print the command that auto-fixes it.&lt;/li&gt;
&lt;li&gt;A missing secret should print the command or link that creates it.&lt;/li&gt;
&lt;li&gt;A permissions error should name the group that grants the permission and where to request it.&lt;/li&gt;
&lt;li&gt;A deprecated config field should print the new field name and the one-line change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also the cheapest place to put documentation, because it is the only documentation that is guaranteed to be read at the moment it is needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Before
Error: validation failed for field 'healthcheck'

# After
Error: 'healthcheck' must be a path starting with '/', got 'healthz'.
Fix:   change it to '/healthz' in service.yaml.
Docs:  https://internal/docs/service-config#healthcheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The link that replaced a wiki page&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a while the most-linked page in our support channel was a wiki article explaining how to request access to a new environment. Someone would hit a permissions error, ask, and get the link. Dozens of times.&lt;/p&gt;

&lt;p&gt;We put the link in the error message. The wiki page did not change. The number of people who needed to be told about it dropped close to zero, because the tool told them at the exact moment they needed it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. Lead with the plain sentence, keep the raw detail
&lt;/h2&gt;

&lt;p&gt;The instinct when wrapping an error is to hide the original. That is a mistake. The original error is often the only thing that helps when the plain-language guess is wrong, and someone on the platform team will eventually need it.&lt;/p&gt;

&lt;p&gt;The structure that works: plain sentence first, cause and action next, raw detail last and clearly labelled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fplv8opb1jstou6u7odwz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fplv8opb1jstou6u7odwz.png" alt="Diagram" width="552" height="844"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The developer reads the top. The platform engineer, if it gets that far, reads the bottom. Nobody has to scroll through a stack trace to find out that a tag was missing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The wrapper that hid too much&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first attempt at friendlier errors over-corrected. The deploy tool caught every failure and printed a short summary, and dropped the original output entirely. It was clean until the summary was wrong. Then the developer had a confident sentence pointing at the wrong cause and no way to see what had actually happened.&lt;/p&gt;

&lt;p&gt;We put the raw output back, below a separator, under a heading that said it was for debugging. The summaries stayed. The dead ends went away.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Treat the most common errors as bugs
&lt;/h2&gt;

&lt;p&gt;Every platform has a small set of errors that account for most of the confusion. You can find them without any tooling: read a month of the support channel and count. The same five or six messages will come up over and over.&lt;/p&gt;

&lt;p&gt;Each of those is a bug in the platform, even if the underlying tool is behaving correctly. The bug is that the platform let a confusing message reach a developer. Put them on the backlog, in priority order by how often they appear, and fix them the way you would fix any other bug.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The list on the wall&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At one point I kept a short list of the errors that generated the most repeat questions, ordered by count. It had maybe eight entries. We worked down it during quiet weeks, one rewritten message at a time.&lt;/p&gt;

&lt;p&gt;It was some of the highest-return work the platform team did that year, and almost none of it involved changing what the platform actually did. It only changed what it said.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  6. Test error messages like features
&lt;/h2&gt;

&lt;p&gt;If a message matters, it deserves a test. Not a test that the error is raised, which most codebases already have, but a test that the message says what it should: names the field, includes the fix, links to the right place.&lt;/p&gt;

&lt;p&gt;This sounds excessive until the first time someone refactors the validation code and the helpful message quietly reverts to a generic one. Nobody notices, because errors are not on the happy path, until the support channel fills up again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The error message is the platform's real interface.&lt;/strong&gt; It is read more than any docs page, at the exact moment it matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three jobs:&lt;/strong&gt; what broke, why, what to do next. A message that does one is not finished.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write for someone who joined last week&lt;/strong&gt; and has never seen the platform code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the fix in the message.&lt;/strong&gt; A command or a link beats a description every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain sentence on top, raw error on the bottom.&lt;/strong&gt; Never hide the original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count the repeat errors and fix them as bugs.&lt;/strong&gt; It is cheap, and it compounds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I still have not worked out is errors from tools I do not own. When a managed cloud service or a third-party CLI returns something cryptic, the platform can wrap it, but the wrapping is a guess, and the guess goes stale every time the vendor changes their wording.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://vivek-itp.github.io/mydevexblog/blog/2026/09/09/error-messages-are-part-of-the-platform/" rel="noopener noreferrer"&gt;https://vivek-itp.github.io/mydevexblog/blog/2026/09/09/error-messages-are-part-of-the-platform/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devex</category>
      <category>errormessages</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Self-service on a slide vs self-service in practice</title>
      <dc:creator>vivek Itp</dc:creator>
      <pubDate>Wed, 09 Sep 2026 18:42:05 +0000</pubDate>
      <link>https://dev.to/vivek_itp/self-service-on-a-slide-vs-self-service-in-practice-1839</link>
      <guid>https://dev.to/vivek_itp/self-service-on-a-slide-vs-self-service-in-practice-1839</guid>
      <description>&lt;p&gt;Every internal platform I have seen described in a slide deck was self-service. Developers would "spin up a new service in minutes" with "no tickets" and "no waiting on the platform team". I have never seen one of those slides that was accurate on the day it was presented.&lt;/p&gt;

&lt;p&gt;That is not because the people writing the slides were lying. It is because self-service is a property of the whole path a developer walks, and the slide only describes the part the platform team built. The parts they did not build, the access request, the DNS entry, the secret that has to be created by someone else, the approval that lives in a different tool, are invisible from the platform side and completely visible from the developer's.&lt;/p&gt;

&lt;p&gt;This post is about measuring that gap honestly, and about what actually closes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only definition that matters
&lt;/h2&gt;

&lt;p&gt;Self-service means a developer with a normal amount of access can go from "I need a new service" to "it is running in production and I can see it" without asking a human for anything.&lt;/p&gt;

&lt;p&gt;Not "without asking the platform team". Without asking &lt;em&gt;anyone&lt;/em&gt;. The moment there is a ticket, a Slack message, or a form that someone else has to act on, the path is not self-service, no matter how good the tooling is on either side of that gap.&lt;/p&gt;

&lt;p&gt;That definition is strict on purpose. A path that is self-service except for one step is not ninety percent self-service. It is blocked at that step, and every developer will wait there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the path usually looks like
&lt;/h2&gt;

&lt;p&gt;Here is the shape of the path I have seen most often, regardless of company or tooling. The platform team owns the middle. The friction lives at the edges.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Who does it&lt;/th&gt;
&lt;th&gt;Self-service?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Create the repository from a template&lt;/td&gt;
&lt;td&gt;Developer&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get the pipeline running&lt;/td&gt;
&lt;td&gt;Developer, using the shared template&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get a namespace or environment&lt;/td&gt;
&lt;td&gt;Platform team, via ticket&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get a database or queue&lt;/td&gt;
&lt;td&gt;Another team, via a different ticket&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get secrets into the environment&lt;/td&gt;
&lt;td&gt;Security or platform, via request&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get a DNS entry and certificate&lt;/td&gt;
&lt;td&gt;Network team, via email&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get production deploy permission&lt;/td&gt;
&lt;td&gt;Manager approval, via form&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First production deploy&lt;/td&gt;
&lt;td&gt;Developer&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The three "yes" rows are what the slide describes. The five "no" rows are what the developer remembers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this looked like for me&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first time I actually walked the path myself, start to finish, as if I were a developer on a product team, it took the better part of a week. Not because any single step was slow. Because each step was a request to a different person, and each person had a queue.&lt;/p&gt;

&lt;p&gt;The platform tooling in the middle was genuinely good. Repository template, pipeline, deploy, all worked first time. I still spent most of the week waiting. Nobody on the platform team had ever counted the waiting, because none of it happened in our tools.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Count tickets, not features
&lt;/h2&gt;

&lt;p&gt;The most useful number a platform team can track is not how many features the platform has. It is how many times a developer has to ask another human for something on the way to a first production deploy.&lt;/p&gt;

&lt;p&gt;I call it the ticket count, even though half of those requests are never actual tickets. A Slack message asking for a namespace is a ticket. An email to the network team is a ticket. An approval form is a ticket. If a human has to act before the developer can continue, it counts.&lt;/p&gt;

&lt;p&gt;Then track the second number: how long each of those requests waits. Not how long it takes to fulfil, which is usually minutes. How long it waits in someone's queue before anyone looks at it, which is usually days.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The number that changed the conversation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I first put the ticket count in front of the people who owned the platform roadmap, it was somewhere around six human touch points for a new service, with a median wait of a day or two each. That was the first time the roadmap conversation shifted from "what feature should we add" to "which of these requests can we make disappear".&lt;/p&gt;

&lt;p&gt;It also surfaced something uncomfortable: the platform team was not the bottleneck. We were the fastest queue in the chain. The slow ones belonged to teams that had never been told they were part of the developer experience at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Most tickets exist because of a default nobody set
&lt;/h2&gt;

&lt;p&gt;When you look at why each request exists, a pattern shows up. Most of them are not there for safety. They are there because nobody decided what the default should be, so a human decides it every time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A namespace request exists because nobody decided that every repository gets a namespace automatically.&lt;/li&gt;
&lt;li&gt;A resource quota request exists because nobody decided what a reasonable starting quota is.&lt;/li&gt;
&lt;li&gt;A database request exists because nobody decided that a service can provision its own small database within limits.&lt;/li&gt;
&lt;li&gt;A production access request exists because nobody decided that the team that owns a service can deploy it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is a policy question that a human answers by hand, dozens of times, usually the same way. Writing the answer down once and letting the tooling apply it is the whole job.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The request that was always approved&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the requests in our chain was for a new namespace with a default quota. When I looked at the history, every single one had been approved, almost always with the same quota. The approval step existed because at some point someone had worried about cluster capacity, and the worry had turned into a permanent human gate.&lt;/p&gt;

&lt;p&gt;We replaced it with a rule: every repository created from the template gets a namespace and a standard quota at creation time, with a documented way to ask for more. The approval disappeared, the ticket disappeared, and the cluster did not run out of capacity. The worry had been real. The gate had never been the right answer to it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Pre-approve the safe path, and only the safe path
&lt;/h2&gt;

&lt;p&gt;The objection to removing gates is always the same: what stops someone doing something dangerous? The answer is not a human. It is a narrow path that is safe by construction, with the gates kept only for leaving that path.&lt;/p&gt;

&lt;p&gt;In practice that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The template produces a service that is safe to deploy as-is: no public exposure, conservative resources, no elevated permissions.&lt;/li&gt;
&lt;li&gt;Everything on that path is pre-approved. Nobody reviews it, because the review already happened when the template was written.&lt;/li&gt;
&lt;li&gt;Anything off the path, a public endpoint, a privileged container, a larger quota, still needs a human. But that human is now reviewing exceptions, not routine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the part that makes security and platform teams comfortable with removing gates. They are not removing review. They are moving it from every request to the template, where it happens once and applies everywhere.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Where the pre-approval broke down&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The thing I got wrong the first time was making the safe path too narrow. The template covered a stateless HTTP service and nothing else. Anything with a queue consumer, a scheduled job, or a database needed the exception route, which meant most real services went through the exception route, which meant we had rebuilt the ticket queue with extra steps.&lt;/p&gt;

&lt;p&gt;The fix was widening the path until it covered what most teams actually built, and treating each new exception request as a signal that the path was still too narrow. The exception route is supposed to be rare. If it is not, the template is wrong, not the developers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. Own the whole path, or at least measure it
&lt;/h2&gt;

&lt;p&gt;The hardest part is that most of the tickets belong to teams the platform team does not control. Networking, security, database operations, a change board. Telling those teams to remove their gates does not work. Showing them where they sit in the developer's week sometimes does.&lt;/p&gt;

&lt;p&gt;What I have seen work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publish the path as a single diagram&lt;/strong&gt;, with every human touch point marked and the median wait for each. Put it somewhere visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask each owning team one question:&lt;/strong&gt; what would need to be true for this step to be automatic? Usually the answer is a policy nobody has written down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offer to build the automation&lt;/strong&gt; for them. Most teams are not against self-service. They do not have time to build it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz113exnovc6kd8up736a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz113exnovc6kd8up736a.png" alt="Diagram" width="795" height="74"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each green node was, at one point, a ticket to a different team. None of them stopped being someone's responsibility. They stopped needing a human in the loop for the routine case.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The team that was never asked&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The slowest step on our path was a DNS and certificate request that went to a networking team by email. From the platform side it looked like an immovable external dependency. When I finally talked to that team, they had no idea their queue was on the critical path for every new service. They had automation for internal zones already. It just was not connected to anything developers could reach.&lt;/p&gt;

&lt;p&gt;Connecting it took a couple of weeks and one conversation. The step went from a multi-day wait to a couple of minutes, and the platform team did not build most of it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Measure it the way a developer feels it
&lt;/h2&gt;

&lt;p&gt;If the goal is honest self-service, the metric has to be something a developer would recognise. The ones I keep coming back to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time from repository creation to first production deploy.&lt;/strong&gt; Wall clock, not effort. This is the number that captures the waiting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human touch points on that path.&lt;/strong&gt; The ticket count from above. Target: zero for the standard path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exception rate.&lt;/strong&gt; What fraction of new services need the off-path route. If it is high, the path is too narrow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support requests per new service.&lt;/strong&gt; How many times the developer had to ask a question that the tooling should have answered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these need a new tool. They need someone to walk the path occasionally, as a developer would, and write down what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-service means no human in the loop&lt;/strong&gt;, not just no platform engineer in the loop. One gate blocks the whole path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count the tickets, including the ones that are not tickets.&lt;/strong&gt; Slack messages, emails, and approval forms all count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most gates are undecided defaults.&lt;/strong&gt; Decide the default once, encode it, and the gate disappears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-approve the safe path and keep review for exceptions.&lt;/strong&gt; Then widen the path until exceptions are rare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The slow steps usually belong to someone else.&lt;/strong&gt; Show them the path. Offer to build the automation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I still do not have a good answer for is the last step on most paths: the production approval that exists for compliance reasons rather than technical ones. I have made it faster and made it clearer, but I have not yet made it go away, and I am not sure it should.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://vivek-itp.github.io/mydevexblog/blog/2026/09/07/self-service-on-a-slide-vs-in-practice/" rel="noopener noreferrer"&gt;https://vivek-itp.github.io/mydevexblog/blog/2026/09/07/self-service-on-a-slide-vs-in-practice/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>platformengineering</category>
      <category>selfservice</category>
      <category>internaldeveloperplatform</category>
    </item>
    <item>
      <title>Why this blog exists</title>
      <dc:creator>vivek Itp</dc:creator>
      <pubDate>Wed, 09 Sep 2026 18:42:01 +0000</pubDate>
      <link>https://dev.to/vivek_itp/why-this-blog-exists-1hf7</link>
      <guid>https://dev.to/vivek_itp/why-this-blog-exists-1hf7</guid>
      <description>&lt;p&gt;After more than seven years in DevOps and platform engineering, I have a backlog of small lessons that never made it into a document. This blog is where they go.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "small" problems
&lt;/h2&gt;

&lt;p&gt;Most of the work I have done was not about big architecture decisions. It was about the things in between: a pipeline that takes twelve minutes when it should take four, a Kubernetes error that means nothing to the developer reading it, a deploy process that everyone has quietly learned to work around.&lt;/p&gt;

&lt;p&gt;None of these are outages. Nobody writes a postmortem for them. But they are what a developer's day actually feels like, and they decide whether a team ships confidently or nervously.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I plan to write about
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt; – what makes a pipeline something developers trust, and what quietly breaks that trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes&lt;/strong&gt; – the parts that are hard to explain to people who just want to deploy their service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform engineering&lt;/strong&gt; – what "self-service" really requires, and where internal platforms fall short of the slide deck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer experience in general&lt;/strong&gt; – the friction that is too small to name and too expensive to ignore.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to expect
&lt;/h2&gt;

&lt;p&gt;Short posts, written from experience, with the failures left in. I will update posts when I learn something new, and the last-updated date will show that.&lt;/p&gt;

&lt;p&gt;If you disagree with something, or have seen the same problem solved better, I would genuinely like to hear about it. The &lt;a href="https://github.com/vivek-Itp/mydevexblog" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; for this site accepts issues.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://vivek-itp.github.io/mydevexblog/blog/2026/09/05/why-this-blog-exists/" rel="noopener noreferrer"&gt;https://vivek-itp.github.io/mydevexblog/blog/2026/09/05/why-this-blog-exists/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>meta</category>
      <category>devex</category>
    </item>
    <item>
      <title>Kubernetes for people who did not ask for Kubernetes</title>
      <dc:creator>vivek Itp</dc:creator>
      <pubDate>Wed, 09 Sep 2026 18:33:24 +0000</pubDate>
      <link>https://dev.to/vivek_itp/kubernetes-for-people-who-did-not-ask-for-kubernetes-20kc</link>
      <guid>https://dev.to/vivek_itp/kubernetes-for-people-who-did-not-ask-for-kubernetes-20kc</guid>
      <description>&lt;p&gt;Nobody on an application team wakes up wanting to learn Kubernetes. They want to ship a service, see it running, and fix it when it breaks. Kubernetes was a decision the platform team made, usually for good reasons, and then handed to people who never asked for it.&lt;/p&gt;

&lt;p&gt;I have been on the side that made that decision. I still think it was right. But I have also watched what it costs a developer the first time they see &lt;code&gt;CrashLoopBackOff&lt;/code&gt; in a terminal and have no idea what it means, who to ask, or whether it is their fault. That moment is where a lot of developer experience quietly goes to die.&lt;/p&gt;

&lt;p&gt;This post is about closing the gap between what developers need from a cluster and what the cluster asks of them in return.&lt;/p&gt;

&lt;h2&gt;
  
  
  What developers asked for, and what they got
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;They asked for&lt;/th&gt;
&lt;th&gt;What the cluster asked of them&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deploy my service&lt;/td&gt;
&lt;td&gt;Write a Deployment, a Service, an Ingress, maybe a ConfigMap and a Secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;See my logs&lt;/td&gt;
&lt;td&gt;Learn &lt;code&gt;kubectl&lt;/code&gt;, find the right namespace, find the right pod, pick the right container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Roll back&lt;/td&gt;
&lt;td&gt;Know that rollouts have history, know the command, have permission to run it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Know if it is healthy&lt;/td&gt;
&lt;td&gt;Understand readiness versus liveness probes and what happens when each fails&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Give it enough memory&lt;/td&gt;
&lt;td&gt;Understand requests versus limits, and what &lt;code&gt;OOMKilled&lt;/code&gt; means at 2 AM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of the right-hand column is unreasonable for a platform engineer. All of it is unreasonable to expect of someone whose job is the left-hand column.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start from the three things developers actually do
&lt;/h2&gt;

&lt;p&gt;Strip away the tooling and a developer interacts with the runtime in three ways: deploy a version, look at what it is doing, and go back to the previous version when something is wrong. Everything else is either rare or someone else's job.&lt;/p&gt;

&lt;p&gt;That means the first question about any Kubernetes platform is not "which ingress controller" or "which service mesh". It is: &lt;em&gt;how many steps does it take a developer to do each of those three things, and how many of those steps require knowing a Kubernetes concept?&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this looked like for me&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I first mapped the deploy path for a typical service, I counted the Kubernetes concepts a developer had to touch to get from a merged pull request to a running pod they could see. It was somewhere around ten: namespace, deployment, service, ingress, image pull secret, config map, resource requests, probes, labels that had to match exactly, and the &lt;code&gt;kubectl&lt;/code&gt; context to even look at any of it.&lt;/p&gt;

&lt;p&gt;Most developers had learned exactly enough to copy the previous service's YAML and change the name. Which worked, until it did not, and then nobody could explain why.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Every concept you expose becomes a support ticket
&lt;/h2&gt;

&lt;p&gt;Kubernetes leaks. Not in a technical sense, but in the sense that every abstraction it offers eventually shows up in a developer's terminal as an error they cannot decode.&lt;/p&gt;

&lt;p&gt;Some of these leak more than others. In my experience the ones that generate the most confusion, in rough order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resource limits and &lt;code&gt;OOMKilled&lt;/code&gt;.&lt;/strong&gt; The pod dies, restarts, dies again. The application logs show nothing because the process was killed from outside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probes.&lt;/strong&gt; A service that is fine but slow to start gets restarted forever because its liveness probe fires too early.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Labels and selectors.&lt;/strong&gt; A Service pointing at nothing because one label has a typo. No error. Just no traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Namespaces and contexts.&lt;/strong&gt; "My pod is not there." It is there. They are looking in the wrong namespace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image pull errors.&lt;/strong&gt; A tag that was never pushed, or a registry credential that expired.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The one that came up every week&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The single most repeated question I dealt with was some version of &lt;em&gt;"my pod keeps restarting and the logs are empty."&lt;/em&gt; Nine times out of ten it was &lt;code&gt;OOMKilled&lt;/code&gt;. The developer had copied a memory limit from another service, their service needed more, and the kernel killed it before it could log anything.&lt;/p&gt;

&lt;p&gt;The fix that finally reduced the questions was not documentation. It was making the deploy tooling print the pod's last termination reason next to the logs, in plain words: &lt;em&gt;"This container was killed for exceeding its memory limit of 256Mi. Raise &lt;code&gt;memory&lt;/code&gt; in your service config or investigate memory usage."&lt;/em&gt; The developer never needed to know what a limit was. They needed to know what to change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Put a thin layer in front, not a whole new platform
&lt;/h2&gt;

&lt;p&gt;The tempting answer to all of this is to build an internal platform that hides Kubernetes completely. I have seen that go wrong more often than it goes right. The layer becomes its own thing to learn, it lags behind the features teams need, and when it breaks the developer is now two abstractions away from the actual problem.&lt;/p&gt;

&lt;p&gt;The approach I have had the most success with is thinner: a single, opinionated template that turns a small config file into all the Kubernetes objects a service needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# service.yaml, the only file a developer edits&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders-api&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.internal/orders-api&lt;/span&gt;
&lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512Mi&lt;/span&gt;
&lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;250m&lt;/span&gt;
&lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/healthz&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind that file is a Helm chart, or a Kustomize base, or a small script. It does not matter which. What matters is that the developer never writes a Deployment by hand and never sees a label selector, but the objects that get created are ordinary Kubernetes objects that anyone on the platform team can inspect with standard tools.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The template that replaced the copy-paste&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We replaced per-service YAML with one shared chart and a values file of roughly ten keys. Migrating existing services was mostly deleting things. The chart set sensible defaults for everything the values file did not mention, so a brand new service needed four lines to deploy.&lt;/p&gt;

&lt;p&gt;The trade-off was real. A handful of services needed something the chart did not support, like an extra sidecar or a specific affinity rule. We added an escape hatch that let a service ship raw Kubernetes manifests alongside the values file. Almost nobody used it. The few who did were the people who actually understood Kubernetes, which is exactly who should be writing raw manifests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. Do not hide the things they will need at 2 AM
&lt;/h2&gt;

&lt;p&gt;Hiding complexity during the happy path is good. Hiding it during an incident is dangerous. A developer being paged about their own service needs to get to logs, recent events, and a rollback in under a minute, without a platform engineer in the loop.&lt;/p&gt;

&lt;p&gt;What that requires, concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A direct link to logs&lt;/strong&gt; for the service, ideally posted on the pull request or in the deploy notification, not a &lt;code&gt;kubectl&lt;/code&gt; incantation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events in plain language.&lt;/strong&gt; "Pod restarted 4 times in 10 minutes, last reason: OOMKilled" beats a raw &lt;code&gt;kubectl describe&lt;/code&gt; dump.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read access by default.&lt;/strong&gt; Developers should be able to look at anything about their own service without asking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback as a button or a single command.&lt;/strong&gt; Not a runbook.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyt0psq79sn6n9mun267q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyt0psq79sn6n9mun267q.png" alt="Diagram" width="800" height="76"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The incident that changed my mind about access&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Early on, developers did not have &lt;code&gt;kubectl&lt;/code&gt; access to production at all. Everything went through the platform team. The reasoning was safety. The effect was that every incident, however small, needed a platform engineer awake before anyone could even look at what was happening.&lt;/p&gt;

&lt;p&gt;After one late-night incident that was a five-minute fix once someone with access finally looked, we gave every team read-only access to their own namespace and a rollback command they could run themselves. The number of incidents did not go up. The time to resolve them went down noticeably, and the platform team stopped being the bottleneck for problems that were never theirs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Defaults are decisions, so make them good ones
&lt;/h2&gt;

&lt;p&gt;Every field a developer does not set is a decision the platform made for them. In Kubernetes, most of the defaults are wrong for a production service: no resource requests, no probes, one replica, no pod disruption budget.&lt;/p&gt;

&lt;p&gt;A good template treats defaults as the main product. A service that specifies nothing should still get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resource requests and limits that are conservative but not tiny.&lt;/li&gt;
&lt;li&gt;A readiness probe on the health endpoint, with a start-up grace period long enough for a slow JVM or a database migration.&lt;/li&gt;
&lt;li&gt;Two replicas, spread across nodes.&lt;/li&gt;
&lt;li&gt;Rolling updates that keep the old version serving until the new one is ready.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The default that bit us&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a while our template set no resource requests unless the developer added them. Most did not, because they did not know they should. The result was a cluster where the scheduler had no idea how much anything needed, nodes got overcommitted, and the noisiest service on a node would starve its neighbours.&lt;/p&gt;

&lt;p&gt;Setting a modest default request for every service, and making the template refuse to render without one, fixed most of the scheduling problems in one change. Nobody on an application team noticed, which was the point.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  6. Rollback must be as easy as deploy, and faster
&lt;/h2&gt;

&lt;p&gt;If deploying is a merge and rolling back is a ticket, developers will not roll back. They will try to fix forward under pressure, which is how a small incident becomes a large one.&lt;/p&gt;

&lt;p&gt;Rollback should be the same mechanism as deploy, pointed at the previous version. In Kubernetes that can be as simple as &lt;code&gt;kubectl rollout undo&lt;/code&gt;, but the developer should not need to know that. They need a command or a button in the same place they deploy from, and it needs to complete in seconds, not minutes.&lt;/p&gt;

&lt;p&gt;The test I use: can a developer who joined last week roll back their own service during an incident without asking anyone? If the answer is no, the platform is not done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Count the concepts.&lt;/strong&gt; For deploy, logs, and rollback, count how many Kubernetes ideas a developer must understand. Aim for zero on the happy path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translate the errors.&lt;/strong&gt; &lt;code&gt;OOMKilled&lt;/code&gt;, failed probes, and empty selectors should surface as plain sentences that say what to change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thin template, ordinary objects.&lt;/strong&gt; A small config file in, standard Kubernetes manifests out, with an escape hatch for the few who need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never hide the incident path.&lt;/strong&gt; Logs, events, and rollback must be one click away for the service owner, with no platform engineer required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good defaults are the product.&lt;/strong&gt; Requests, probes, replicas, and rolling updates should be right when the developer says nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part I still have not solved is what happens when the thin layer needs to change. Every template update is a change to every service at once, and I do not yet have a way to roll that out that feels as safe as rolling out a single service.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://vivek-itp.github.io/mydevexblog/blog/2026/09/05/kubernetes-for-people-who-did-not-ask-for-it/" rel="noopener noreferrer"&gt;https://vivek-itp.github.io/mydevexblog/blog/2026/09/05/kubernetes-for-people-who-did-not-ask-for-it/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>onboarding</category>
      <category>devex</category>
    </item>
    <item>
      <title>CI/CD should be designed around the developer, not the pipeline</title>
      <dc:creator>vivek Itp</dc:creator>
      <pubDate>Wed, 09 Sep 2026 18:33:19 +0000</pubDate>
      <link>https://dev.to/vivek_itp/cicd-should-be-designed-around-the-developer-not-the-pipeline-459</link>
      <guid>https://dev.to/vivek_itp/cicd-should-be-designed-around-the-developer-not-the-pipeline-459</guid>
      <description>&lt;p&gt;Ask a developer which tool they touch most after their editor, and the honest answer is usually the CI pipeline. Every branch, every pull request, every deploy goes through it. Yet it is almost always designed by people who rarely have to wait on it.&lt;/p&gt;

&lt;p&gt;That gap shows. Pipelines get optimised for what is easy to run, easy to secure, and easy for the platform team to maintain. Developer time spent waiting is invisible on every dashboard, so it never gets fixed.&lt;/p&gt;

&lt;p&gt;I have been on the platform side of this for most of my career, and I noticed the gap the way most platform engineers do: through the support channel. The same three questions kept coming back. &lt;em&gt;Why is it slow? Why is it red? Can someone re-run it?&lt;/em&gt; None of them were about the product. All of them were about the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What developers actually want from CI
&lt;/h2&gt;

&lt;p&gt;Strip away the tooling and developers want four things from a pipeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;They want&lt;/th&gt;
&lt;th&gt;Which means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fast feedback&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Know within minutes whether the change is fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Clear failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;When it is red, know why without opening five logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trust&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Red means broken. Green means safe to merge. No exceptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No ceremony&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Merging and deploying should not need a meeting, a ticket, or a favour&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything below is about delivering those four things. Every pipeline feature that does not serve one of them is a cost, not a benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Feedback in under ten minutes, or split the pipeline
&lt;/h2&gt;

&lt;p&gt;A pull request check that takes thirty minutes trains developers to context-switch. They open another task, forget the PR, and come back an hour later. The real cost of a slow pipeline is not the thirty minutes. It is the hour of lost focus on both sides of the review.&lt;/p&gt;

&lt;p&gt;The fix is rarely a faster machine. It is deciding what must run &lt;em&gt;before&lt;/em&gt; merge and what can run &lt;em&gt;after&lt;/em&gt;. Lint, unit tests, and a build belong in the PR check. Full integration suites, long security scans, and multi-platform builds can run on the main branch after merge, with a fast path to revert.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this looked like for me&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The slowest pipeline I inherited ran the full integration suite against a real database on every pull request. It took over half an hour on a good day, and a good day meant nobody else was pushing.&lt;/p&gt;

&lt;p&gt;We moved integration tests to run on &lt;code&gt;main&lt;/code&gt; after merge, and kept lint, unit tests and the build in the PR check. The PR check dropped to under ten minutes. The integration suite still ran on every merge, just not in the developer's way.&lt;/p&gt;

&lt;p&gt;One thing did slip through: a database migration that passed unit tests broke the integration run on &lt;code&gt;main&lt;/code&gt;. It was caught within minutes and reverted with one click. That was the whole point of the split. A failure on &lt;code&gt;main&lt;/code&gt; costs one revert. A slow PR check costs every developer, every day.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. A failure should tell you what broke in the first screen
&lt;/h2&gt;

&lt;p&gt;The most expensive kind of pipeline failure is the one where the developer has to scroll through two thousand lines of log to find a single &lt;code&gt;AssertionError&lt;/code&gt;. Every minute spent hunting for the failure is a minute the pipeline has actively wasted.&lt;/p&gt;

&lt;p&gt;What helps, in order of impact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fail the job with a one-line summary&lt;/strong&gt; at the top of the output, not buried at the bottom.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Annotate the PR&lt;/strong&gt; with the failing test, file, and line. Most CI systems support this. Most pipelines never bother.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name jobs after what they check&lt;/strong&gt;, not after the tool. &lt;code&gt;unit-tests&lt;/code&gt; beats &lt;code&gt;pytest-run-3&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Link to the fix&lt;/strong&gt; when the failure is a known category. A lint failure should point to the command that fixes it locally.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The error message that generated the most support requests&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a long stretch, the single most common message in our platform channel was some version of &lt;em&gt;"my tests are failing with exit code 137 and I did not change anything."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Exit code 137 means the process was killed, almost always because the runner ran out of memory. The tests were fine. The test &lt;em&gt;runner&lt;/em&gt; was being killed mid-suite because the job was sharing a small machine with a build step that ate all the RAM.&lt;/p&gt;

&lt;p&gt;The real fix was giving test jobs their own memory limit. But the cheaper fix, which we shipped the same day, was a wrapper step that caught exit code 137 and printed one line: &lt;em&gt;"Job was killed for exceeding memory. This is a runner problem, not your code. See link."&lt;/em&gt; Support requests for that error stopped almost entirely. The developers had never needed the fix. They had needed the explanation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Flaky tests are a platform problem, not a developer problem
&lt;/h2&gt;

&lt;p&gt;Nothing destroys trust in a pipeline faster than a test that fails one run in ten for no reason. Developers learn to click &lt;em&gt;re-run&lt;/em&gt; on red without reading. Once that habit sets in, real failures get re-run too, and the pipeline stops meaning anything.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is that the platform team usually knows which tests are flaky and leaves them in, because removing a test feels like lowering the bar. It is the opposite. A test nobody believes is not raising the bar. It is noise with a green tick.&lt;/p&gt;

&lt;p&gt;What I think works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Track the re-run rate per job.&lt;/strong&gt; It is the most honest developer-experience metric a pipeline has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give every flaky test an owner and a deadline.&lt;/strong&gt; Fix it or move it out of the merge path until it is fixed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never retry silently.&lt;/strong&gt; Automatic retries hide the problem and make the pipeline slower for everyone.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How flakiness actually showed up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We did not find our flaky tests by looking at test reports. We found them by counting re-runs. Roughly one in five pull requests was being re-run at least once before it went green, and when we looked at &lt;em&gt;why&lt;/em&gt;, the same handful of tests appeared every time.&lt;/p&gt;

&lt;p&gt;Most of them had the same root cause: they assumed something about the machine they ran on. A fixed port that was sometimes taken. A timing assertion that held on a laptop and failed on a busy shared runner. A test that depended on the order another test left the database in.&lt;/p&gt;

&lt;p&gt;The ones we could fix, we fixed. The ones we could not fix quickly moved to a separate nightly job with a named owner and a date. They still ran. They just stopped blocking merges. The re-run rate fell to a level where a red PR check became worth reading again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. The difference between running code locally and in CI should be almost zero
&lt;/h2&gt;

&lt;p&gt;Every gap between a developer's machine and the pipeline is a place where &lt;em&gt;"it works locally"&lt;/em&gt; becomes a true statement and a useless one. The developer did nothing wrong. The pipeline did nothing wrong. They were simply running two different things.&lt;/p&gt;

&lt;p&gt;Those gaps are rarely dramatic. A different Python version. A dependency resolved differently. An environment variable that exists on the laptop and not on the runner. A file that is present locally and ignored by git. Each one costs a developer an hour of confusion and a support message to the platform team.&lt;/p&gt;

&lt;p&gt;The target should be that a developer can run one command locally and get the same result the pipeline will give. Not similar. &lt;strong&gt;The same.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The one that took an afternoon to find&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A developer pushed a change that used newer language syntax. It ran fine on their laptop. The pipeline went red with a syntax error on a line that was obviously valid. They re-ran it. Red again. They pushed a no-op commit. Red again. Then they asked in the channel.&lt;/p&gt;

&lt;p&gt;It took most of an afternoon to find the cause: the CI image was pinned to a runtime two minor versions older than the one on the laptop. Nobody had changed anything. The runtime version was defined in the pipeline config and nowhere in the repository, so local and CI had simply drifted apart over time without anyone noticing.&lt;/p&gt;

&lt;p&gt;The fix was three lines. The runtime version went into a version file in the repo that both the developer tooling and the pipeline read. The dependency lockfile was committed and installed with an exact-match flag on both sides. The CI job was changed to run inside the same container image developers could run locally.&lt;/p&gt;

&lt;p&gt;The lesson generalises: &lt;strong&gt;anything that defines the environment belongs in the repository, versioned, and read by both sides.&lt;/strong&gt; Anything that lives only on a laptop or only in a CI setting is a bug waiting for its moment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;How to keep the gap closed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One entry point in the repo, a &lt;code&gt;Makefile&lt;/code&gt;, &lt;code&gt;justfile&lt;/code&gt;, or task runner, that defines &lt;code&gt;lint&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;, and &lt;code&gt;build&lt;/code&gt;. The pipeline calls those targets and nothing else.&lt;/li&gt;
&lt;li&gt;Pin the runtime version in the repo, not in the pipeline config.&lt;/li&gt;
&lt;li&gt;Commit lockfiles and install from them with exact matching on both sides.&lt;/li&gt;
&lt;li&gt;Keep secrets and configuration out of the test path. If a test needs a token to pass, it is not a unit test.&lt;/li&gt;
&lt;li&gt;When the pipeline fails, the reproduction step is always the same: run the target locally. If that does not reproduce it, the gap &lt;em&gt;is&lt;/em&gt; the bug, and it goes to the platform team, not the developer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Shared templates, not copied YAML
&lt;/h2&gt;

&lt;p&gt;In any organisation with more than a handful of repos, pipeline definitions get copied. Then one copy gets a fix and the others do not. Six months later there are eleven slightly different versions of the same deploy job and nobody knows which one is right.&lt;/p&gt;

&lt;p&gt;The alternative is a small set of reusable pipeline templates owned by the platform team, versioned, with a changelog. Application repos call the template and pass a few parameters. When the template improves, every repo gets it on the next run.&lt;/p&gt;

&lt;p&gt;The trade-off is real: templates centralise control, and a bad template change breaks everyone at once. That is a reason to test templates properly, not a reason to avoid them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The migration was easy. The customisations were not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When we consolidated, we had dozens of repos, and almost every one had started life as a copy of another repo's pipeline. Moving the ones that had never been touched was straightforward. Point them at the shared template, run it, done.&lt;/p&gt;

&lt;p&gt;The hard part was the repos with &lt;em&gt;small customisations&lt;/em&gt;. An extra step here, a different flag there, added by someone who had since left, for a reason nobody remembered. Each one needed a conversation: is this still needed, and if so, should the template support it? Most of them were not needed. A few were, and they made the template better.&lt;/p&gt;

&lt;p&gt;What I would do differently is version the templates from day one and publish a changelog. The first time a template change broke a downstream repo, the question was not &lt;em&gt;what changed&lt;/em&gt; but &lt;em&gt;when&lt;/em&gt;, and we had no clean answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  6. Deploying should be boring, and rolling back should be more boring
&lt;/h2&gt;

&lt;p&gt;A deploy that needs a runbook is a deploy people avoid. A deploy that happens on merge, with automatic rollback if health checks fail, is one people do ten times a day without thinking.&lt;/p&gt;

&lt;p&gt;The signs a deploy process is hurting developer experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;People batch changes to avoid deploying often.&lt;/li&gt;
&lt;li&gt;Deploys happen at fixed times, usually chosen for the platform team's comfort.&lt;/li&gt;
&lt;li&gt;Rollback is a different, harder process than deploy.&lt;/li&gt;
&lt;li&gt;Someone has to be pinged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is a design choice, and every one of them can be reversed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;From a scheduled deploy window to deploy-on-merge&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At its worst, production deploys happened in a window a few times a week, with a platform engineer watching. Developers batched changes to hit the window, which made every deploy bigger, which made every deploy riskier, which was the argument for keeping the window. The process was defending itself.&lt;/p&gt;

&lt;p&gt;At its best, a merge to &lt;code&gt;main&lt;/code&gt; built the image, ran the post-merge suite, and rolled it out to Kubernetes progressively. Health checks and error rate decided whether the rollout continued. If they failed, the rollout reversed itself and posted a message with the failing check. Nobody watched. Nobody needed to.&lt;/p&gt;

&lt;p&gt;The single change that made rollback boring was making it &lt;em&gt;the same operation as deploy&lt;/em&gt;. Rolling back meant deploying the previous image tag through the same pipeline. No special runbook, no separate permissions, no person on call to run it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  7. Remove the gates that do not add information
&lt;/h2&gt;

&lt;p&gt;Manual approvals feel like safety. Most of them are theatre. If the approver clicks &lt;em&gt;approve&lt;/em&gt; without reading, because they always click &lt;em&gt;approve&lt;/em&gt;, the gate is adding delay and no protection.&lt;/p&gt;

&lt;p&gt;A gate earns its place when the person approving knows something the pipeline cannot check. Most of the time, what they are checking can be automated: a security scan, a policy check, a required reviewer on the code itself. Automate it, put the result in the PR, and remove the manual step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The approval that approved everything&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every production deploy needed a sign-off from a change manager. When we looked at the history, the approval rate was effectively one hundred percent, and the median time between request and approval was measured in hours. Not because anyone was reviewing. Because the request had to wait for someone to notice it.&lt;/p&gt;

&lt;p&gt;We replaced it with the checks the approver was supposed to be doing: tests passed, security scan passed, code was reviewed by someone on the owning team. All three were already visible in the pull request. The manual gate stayed for exactly one category, database migrations, where a human genuinely did know something the pipeline could not.&lt;/p&gt;

&lt;p&gt;The argument for keeping the gate was audit. The answer was that a pipeline log with named checks and a timestamp is a better audit trail than a click.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What this looks like from the developer's chair
&lt;/h2&gt;

&lt;p&gt;Put together, a pipeline built for developer experience looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fij6pvk98wj9jhnc3667e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fij6pvk98wj9jhnc3667e.png" alt="Diagram" width="800" height="146"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push a branch. Within minutes, lint and unit results are annotated on the PR.&lt;/li&gt;
&lt;li&gt;If something is red, the annotation says what and where, and the fix command is one copy-paste away.&lt;/li&gt;
&lt;li&gt;Green means safe to merge. Nobody re-runs on principle.&lt;/li&gt;
&lt;li&gt;Merge. The main branch builds, runs the slow suite, and deploys to production with health checks.&lt;/li&gt;
&lt;li&gt;If health checks fail, it rolls back on its own and posts a message with the failing check.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is exotic. Every piece exists in every major CI system today. The reason most pipelines do not look like this is that nobody made the developer's experience the goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Measure the pipeline from the developer's side:&lt;/strong&gt; time to feedback, re-run rate, time from merge to production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything slow goes after merge.&lt;/strong&gt; Anything flaky goes out of the merge path until it is fixed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local and CI must run the same thing.&lt;/strong&gt; Pin the runtime, commit the lockfile, share the container image, keep the YAML thin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploys happen on merge.&lt;/strong&gt; Rollback is the same operation, run backwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every manual gate has to justify itself&lt;/strong&gt; with information the pipeline cannot get on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part I am still working out is how to keep a pipeline this way once it exists. Every one of the problems above crept back in slowly, one reasonable-sounding exception at a time. Keeping the developer's experience as the design goal is not a project. It is maintenance.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://vivek-itp.github.io/mydevexblog/blog/2026/09/05/cicd-designed-for-developers/" rel="noopener noreferrer"&gt;https://vivek-itp.github.io/mydevexblog/blog/2026/09/05/cicd-designed-for-developers/&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ci</category>
      <category>pipelines</category>
      <category>devex</category>
    </item>
  </channel>
</rss>
