<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="atom.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://docs.victor.onl/blog</id>
    <title>Platform Engineering Blog</title>
    <updated>2026-05-10T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://docs.victor.onl/blog"/>
    <subtitle>Platform Engineering Blog</subtitle>
    <icon>https://docs.victor.onl/img/favicon.ico</icon>
    <entry>
        <title type="html"><![CDATA[Application Repositories as Golden Paths: Dockerfile, CI, and .k8s in Every Repo]]></title>
        <id>https://docs.victor.onl/blog/application-repositories-golden-paths</id>
        <link href="https://docs.victor.onl/blog/application-repositories-golden-paths"/>
        <updated>2026-05-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How scaffolded application repositories with Dockerfile, CI pipeline, and .k8s folder give developers autonomy over workload configuration while maintaining platform control.]]></summary>
        <content type="html"><![CDATA[<p>Our platform manages <em>where</em> and <em>how</em> services are deployed through GitOps repositories. But what about the application itself — the code, the build, the container? Until now, every team set up their own Dockerfile, wrote their own CI pipeline, and figured out their own image tagging strategy. The result was predictable: 10 teams, 10 different CI workflows, 10 different container build approaches, and zero consistency.</p>
<p>The Application Repository convention changes this. When a new service is scaffolded via Backstage, it delivers a complete, opinionated repository with source code, Dockerfile, CI pipeline, and a <code>.k8s</code> folder — giving the application team autonomy over their workload configuration while keeping the platform in control of the deployment model.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-architecture-two-repos-one-deployment">The architecture: two repos, one deployment<a href="https://docs.victor.onl/blog/application-repositories-golden-paths#the-architecture-two-repos-one-deployment" class="hash-link" aria-label="Direct link to The architecture: two repos, one deployment" title="Direct link to The architecture: two repos, one deployment" translate="no">​</a></h2>
<p>Our GitOps model uses a clear separation between <strong>deployment configuration</strong> and <strong>application configuration</strong>:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">{domain}-gitops/k8s/{env}/{service}/    ← Platform-managed base manifests</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">{app}-repo/.k8s/values.yaml             ← Developer-managed overrides</span><br></div></code></pre></div></div>
<p>The domain GitOps repository contains the foundational Kubernetes configuration — Deployments, Services, NetworkPolicies, HPA, PDB — generated by the <code>create-service</code> template and maintained by the domain team's platform conventions.</p>
<p>The application repository contains a <code>.k8s</code> folder where the development team manages values that change frequently with application releases.</p>
<p><strong>ArgoCD's Multiple Sources</strong> pattern merges both sources at sync time. The base manifests from the GitOps repo provide structure and safety. The application overrides provide flexibility and speed.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-developers-can-override">What developers can override<a href="https://docs.victor.onl/blog/application-repositories-golden-paths#what-developers-can-override" class="hash-link" aria-label="Direct link to What developers can override" title="Direct link to What developers can override" translate="no">​</a></h2>
<p>The <code>.k8s/values.yaml</code> in the application repository gives developers control over:</p>
<ul>
<li class=""><strong>Image tag</strong>: Updated automatically by CI on every successful build</li>
<li class=""><strong>Replicas</strong>: Override min/max HPA thresholds for specific workload needs</li>
<li class=""><strong>CPU and memory</strong>: Adjust requests and limits as the application's profile evolves</li>
<li class=""><strong>Environment variables</strong>: Application-specific configuration that changes with releases</li>
</ul>
<p>These are the values that change most frequently and are most directly tied to the application code. By keeping them in the app repo, developers don't need a PR on the GitOps repo every time they push a release.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-developers-dont-touch">What developers don't touch<a href="https://docs.victor.onl/blog/application-repositories-golden-paths#what-developers-dont-touch" class="hash-link" aria-label="Direct link to What developers don't touch" title="Direct link to What developers don't touch" translate="no">​</a></h2>
<p>The base manifests in <code>{domain}-gitops/k8s/</code> remain under platform convention control:</p>
<ul>
<li class=""><strong>Namespace definition</strong> with all 9 required labels</li>
<li class=""><strong>Security context</strong>: <code>runAsNonRoot</code>, <code>readOnlyRootFilesystem</code>, <code>capabilities: drop: [ALL]</code></li>
<li class=""><strong>NetworkPolicy</strong>: project-boundary isolation</li>
<li class=""><strong>PodDisruptionBudget</strong>: prod-only safety net</li>
<li class=""><strong>ResourceQuota</strong>: env-sized limits</li>
</ul>
<p>This separation ensures that security and compliance guarantees are never accidentally overridden by a developer who "just needs to change the memory limit."</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-ci-pipeline-from-commit-to-deploy">The CI pipeline: from commit to deploy<a href="https://docs.victor.onl/blog/application-repositories-golden-paths#the-ci-pipeline-from-commit-to-deploy" class="hash-link" aria-label="Direct link to The CI pipeline: from commit to deploy" title="Direct link to The CI pipeline: from commit to deploy" translate="no">​</a></h2>
<p>Every application repository scaffolded by <code>create-service</code> includes a pre-configured GitHub Actions workflow. The pipeline is not something the team needs to write — it arrives ready:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Developer pushes code</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        │</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ▼</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">┌─── CI Pipeline ────────────────────────────────────┐</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│                                                     │</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│  1. Test &amp; Lint      Run unit tests, lint checks   │</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│  2. Build            Build container from Dockerfile│</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│  3. Publish          Push image to container registry│</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│  4. Tag Update       Update .k8s/values.yaml with  │</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│                      new image tag, commit back     │</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│                                                     │</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">└─────────────────────────────────────────────────────┘</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        │</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ▼</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ArgoCD detects change in app repo (secondary source)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        │</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        ▼</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Deployment syncs with new image tag</span><br></div></code></pre></div></div>
<p>The key step is <strong>Tag Update</strong>. After publishing the container image, the pipeline automatically updates <code>image.tag</code> in <code>.k8s/values.yaml</code> and commits the change back to the application repository. This commit is what ArgoCD watches.</p>
<p>Because ArgoCD is configured with Multiple Sources — one pointing at the GitOps repo for base manifests, one pointing at the app repo for overrides — the new tag triggers an automatic sync to the target environment.</p>
<p><strong>No manual image tag updates. No separate "deploy" step. Push code, get deployment.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-dockerfile-secure-by-default">The Dockerfile: secure by default<a href="https://docs.victor.onl/blog/application-repositories-golden-paths#the-dockerfile-secure-by-default" class="hash-link" aria-label="Direct link to The Dockerfile: secure by default" title="Direct link to The Dockerfile: secure by default" translate="no">​</a></h2>
<p>The scaffolded Dockerfile follows platform security standards:</p>
<ul>
<li class=""><strong>Multi-stage build</strong>: Separate build and runtime stages to minimize image size</li>
<li class=""><strong>Non-root user</strong>: <code>USER 1000</code> — matches the pod security context (<code>runAsNonRoot: true</code>)</li>
<li class=""><strong>Read-only filesystem compatible</strong>: Application writes go to <code>/tmp</code> (mounted as <code>emptyDir</code>)</li>
<li class=""><strong>Language-specific optimization</strong>: Node.js, .NET, and Python each get a tailored Dockerfile with appropriate base images and dependency caching</li>
</ul>
<p>The developer does not need to know about pod security contexts or read-only filesystems. The Dockerfile and the Kubernetes manifests are designed to work together from day one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-full-picture">The full picture<a href="https://docs.victor.onl/blog/application-repositories-golden-paths#the-full-picture" class="hash-link" aria-label="Direct link to The full picture" title="Direct link to The full picture" translate="no">​</a></h2>
<p>When <code>create-service</code> runs, the developer gets:</p>
<table><thead><tr><th>What</th><th>Where</th><th>Who maintains it</th></tr></thead><tbody><tr><td>Source code boilerplate</td><td><code>{app}-repo/src/</code></td><td>Application team</td></tr><tr><td>Dockerfile</td><td><code>{app}-repo/Dockerfile</code></td><td>Application team</td></tr><tr><td>CI pipeline</td><td><code>{app}-repo/.github/workflows/ci.yaml</code></td><td>Platform team (generated), application team (extended)</td></tr><tr><td>Workload overrides</td><td><code>{app}-repo/.k8s/values.yaml</code></td><td>Application team</td></tr><tr><td>TechDocs</td><td><code>{app}-repo/docs/</code></td><td>Application team</td></tr><tr><td>Base K8s manifests</td><td><code>{domain}-gitops/k8s/{env}/{service}/</code></td><td>Domain team</td></tr><tr><td>ArgoCD routing</td><td><code>platform-gitops/argocd/applicationsets/</code></td><td>Platform team</td></tr></tbody></table>
<p>Three repositories, three ownership boundaries, one deployment that works without manual coordination.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-not-everything-in-the-app-repo">Why not everything in the app repo?<a href="https://docs.victor.onl/blog/application-repositories-golden-paths#why-not-everything-in-the-app-repo" class="hash-link" aria-label="Direct link to Why not everything in the app repo?" title="Direct link to Why not everything in the app repo?" translate="no">​</a></h2>
<p>A common question: "Why not put all Kubernetes manifests in the application repository?"</p>
<p>Because it breaks the ownership model:</p>
<ol>
<li class=""><strong>Security context and network policies are platform concerns</strong>, not application concerns. Developers should not need to (or be able to) modify them.</li>
<li class=""><strong>Multiple services share the same ApplicationSet and domain structure.</strong> Centralizing this in the GitOps repo avoids duplication and ensures consistency.</li>
<li class=""><strong>Convention validation runs on the GitOps repo.</strong> CI checks for naming, labels, and security defaults happen where those definitions live.</li>
</ol>
<p>The <code>.k8s</code> folder gives developers the autonomy they need — image tags, scaling, resources — without giving them access to the things they should not change.</p>
<hr>
<p>The Application Repositories convention is documented in the <a class="" href="https://docs.victor.onl/docs/platform-convention/application-repositories">Platform Convention — Application Repositories</a>. The ArgoCD Multiple Sources pattern is in the <a class="" href="https://docs.victor.onl/docs/platform-convention/argocd">ArgoCD Convention</a>.</p>]]></content>
        <author>
            <name>Platform Engineering</name>
        </author>
        <category label="GitOps" term="GitOps"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="IDP" term="IDP"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Golden Paths: How Backstage Templates Guarantee Correct-by-Default Services]]></title>
        <id>https://docs.victor.onl/blog/golden-paths-backstage-templates</id>
        <link href="https://docs.victor.onl/blog/golden-paths-backstage-templates"/>
        <updated>2026-05-09T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Deep dive into how Backstage Scaffolder templates enforce platform conventions by design, making correct-by-default services the only option for product engineers.]]></summary>
        <content type="html"><![CDATA[<p>Documentation gets ignored. Runbooks get outdated. Wiki pages accumulate conflicting instructions from three different authors. When the "right way" to create a service exists only in documentation, every new service is a chance to deviate from convention.</p>
<p>Backstage Scaffolder templates change this equation. Instead of documenting the correct path and hoping engineers follow it, we encode the correct path into a form that produces correct output every time. This post is about how our template chain works, why the dependency order matters, and what happens when you click <strong>Create</strong>.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem-with-documentation-driven-standards">The problem with documentation-driven standards<a href="https://docs.victor.onl/blog/golden-paths-backstage-templates#the-problem-with-documentation-driven-standards" class="hash-link" aria-label="Direct link to The problem with documentation-driven standards" title="Direct link to The problem with documentation-driven standards" translate="no">​</a></h2>
<p>Our platform convention defines a precise naming pattern, 9 required labels, security defaults, and a specific repository structure. Before templates, applying these standards meant:</p>
<ol>
<li class="">Reading 3 different wiki pages</li>
<li class="">Copy-pasting YAML from examples (often outdated)</li>
<li class="">Hoping the reviewer catches what you missed</li>
<li class="">Waiting for the platform team to fix what the reviewer missed</li>
</ol>
<p>The result: 80% of new services needed at least one correction PR after the initial creation. Convention compliance was aspirational.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="templates-as-enforcement">Templates as enforcement<a href="https://docs.victor.onl/blog/golden-paths-backstage-templates#templates-as-enforcement" class="hash-link" aria-label="Direct link to Templates as enforcement" title="Direct link to Templates as enforcement" translate="no">​</a></h2>
<p>A Backstage Scaffolder template is a YAML definition that combines a user-facing form with a sequence of automated actions. The developer fills in the form. The template generates all the files, opens the PRs, and registers the entity in the catalog.</p>
<p>The template does not document the convention — it <strong>implements</strong> the convention. There is no way to create a service with missing labels, wrong naming, or incorrect security context, because the template generates all of those.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-template-chain">The template chain<a href="https://docs.victor.onl/blog/golden-paths-backstage-templates#the-template-chain" class="hash-link" aria-label="Direct link to The template chain" title="Direct link to The template chain" translate="no">​</a></h2>
<p>Templates have dependencies. You cannot create a service without a system. You cannot create a system without a domain. The chain enforces this order:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">People layer (run any time)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">──────────────────────────────────────────────────────</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> create-group → Group entity + RBAC + ArgoCD roles</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> create-user  → User entity  + RBAC + ArgoCD access</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Platform layer (run in order)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">──────────────────────────────────────────────────────</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> create-domain</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      │  Domain entity, {domain}-gitops repo, AppProject</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      ▼</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> create-system</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      │  System entity, ApplicationSet</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      ▼</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      ├─────────────────────────┬─────────────────────┐</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">      ▼                         ▼                     ▼</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> create-service          create-resource         create-secret</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> Component entity,       Resource entity,        SealedSecret</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> App Repo (CI/CD,        Crossplane Claim/env,   manifest/env</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> TechDocs, .k8s),        infra namespace</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"> k8s manifests/env</span><br></div></code></pre></div></div>
<p>Each template uses <code>EntityPicker</code> to select its parent. You pick the domain from a dropdown that only shows existing domains. You pick the system from a dropdown filtered to that domain's systems. <strong>Referential integrity is enforced by the UI itself.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-create-service-actually-does">What <code>create-service</code> actually does<a href="https://docs.victor.onl/blog/golden-paths-backstage-templates#what-create-service-actually-does" class="hash-link" aria-label="Direct link to what-create-service-actually-does" title="Direct link to what-create-service-actually-does" translate="no">​</a></h2>
<p>When a product engineer runs <code>create-service</code>, they fill in:</p>
<ul>
<li class=""><strong>System</strong>: selected from catalog (domain, repo, AppProject resolve automatically)</li>
<li class=""><strong>Service name</strong>: e.g., <code>api</code>, <code>worker</code>, <code>frontend</code></li>
<li class=""><strong>Service type</strong>: api / worker / frontend / grpc / cronjob</li>
<li class=""><strong>Container image</strong>: registry path</li>
<li class=""><strong>Resource profile</strong>: which CPU/memory tier</li>
<li class=""><strong>Target environments</strong>: which envs to deploy to</li>
</ul>
<p>Then the template executes:</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1--generate-the-application-repository">Step 1 — Generate the Application Repository<a href="https://docs.victor.onl/blog/golden-paths-backstage-templates#step-1--generate-the-application-repository" class="hash-link" aria-label="Direct link to Step 1 — Generate the Application Repository" title="Direct link to Step 1 — Generate the Application Repository" translate="no">​</a></h3>
<p>A new repository is created with:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">{app}-repo/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── src/                    ← boilerplate source code</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── Dockerfile              ← optimized, non-root, language-specific</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── .github/workflows/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│   └── ci.yaml             ← lint, test, build, publish, tag update</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── .k8s/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│   └── values.yaml         ← overrides (image tag, replicas, resources)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">├── docs/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│   └── index.md            ← TechDocs base</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">└── catalog-info.yaml       ← Backstage Component entity</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2--generate-kubernetes-manifests-in-the-domain-repo">Step 2 — Generate Kubernetes manifests in the domain repo<a href="https://docs.victor.onl/blog/golden-paths-backstage-templates#step-2--generate-kubernetes-manifests-in-the-domain-repo" class="hash-link" aria-label="Direct link to Step 2 — Generate Kubernetes manifests in the domain repo" title="Direct link to Step 2 — Generate Kubernetes manifests in the domain repo" translate="no">​</a></h3>
<p>In <code>{domain}-gitops/k8s/{env}/{service}/</code>, the template creates:</p>
<ul>
<li class=""><strong>Namespace</strong> with all 9 required labels</li>
<li class=""><strong>Deployment</strong> with security context, resource limits, readiness/liveness probes</li>
<li class=""><strong>Service</strong> exposing the correct port</li>
<li class=""><strong>HPA</strong> with env-appropriate scaling (dev: 1→2, staging: 2→5, prod: 3→10)</li>
<li class=""><strong>PDB</strong> (prod only, <code>minAvailable: 1</code>)</li>
<li class=""><strong>NetworkPolicy</strong> isolating by project boundary</li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3--update-the-applicationset">Step 3 — Update the ApplicationSet<a href="https://docs.victor.onl/blog/golden-paths-backstage-templates#step-3--update-the-applicationset" class="hash-link" aria-label="Direct link to Step 3 — Update the ApplicationSet" title="Direct link to Step 3 — Update the ApplicationSet" translate="no">​</a></h3>
<p>In <code>platform-gitops</code>, the template adds the new service as an element in the existing ApplicationSet:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">elements</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">service</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> api      </span><span class="token comment" style="color:#999988;font-style:italic"># ← existing</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">service</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> worker   </span><span class="token comment" style="color:#999988;font-style:italic"># ← added by template</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4--register-and-open-prs">Step 4 — Register and open PRs<a href="https://docs.victor.onl/blog/golden-paths-backstage-templates#step-4--register-and-open-prs" class="hash-link" aria-label="Direct link to Step 4 — Register and open PRs" title="Direct link to Step 4 — Register and open PRs" translate="no">​</a></h3>
<p>The template opens two PRs:</p>
<ol>
<li class=""><strong>Domain repo PR</strong>: k8s manifests + Component entity</li>
<li class=""><strong>Platform repo PR</strong>: ApplicationSet element</li>
</ol>
<p>It also registers the new <code>catalog-info.yaml</code> in the Backstage catalog.</p>
<p><strong>Merge order matters</strong>: Platform PR first (ApplicationSet must exist before ArgoCD can sync), then domain PR. Within minutes of both merging, the service is running on the dev cluster.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-two-pr-workflow">The two-PR workflow<a href="https://docs.victor.onl/blog/golden-paths-backstage-templates#the-two-pr-workflow" class="hash-link" aria-label="Direct link to The two-PR workflow" title="Direct link to The two-PR workflow" translate="no">​</a></h2>
<p>Every template follows the same pattern: one PR on the domain/catalog repo, one PR on <code>platform-gitops</code>. This separation exists because:</p>
<ul>
<li class=""><strong>Domain teams own their manifests</strong> — they review and merge what they deploy</li>
<li class=""><strong>Platform team owns the routing</strong> — they review changes to ArgoCD config and RBAC</li>
<li class=""><strong>CODEOWNERS enforce this</strong> — wrong-repo changes get blocked automatically</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="results">Results<a href="https://docs.victor.onl/blog/golden-paths-backstage-templates#results" class="hash-link" aria-label="Direct link to Results" title="Direct link to Results" translate="no">​</a></h2>
<p>Since adopting templates:</p>
<ul>
<li class=""><strong>100% label compliance</strong> on all new services (was &lt; 20%)</li>
<li class=""><strong>&lt; 3 minutes</strong> from clicking Create to PRs opened (was hours of manual YAML)</li>
<li class=""><strong>Zero convention violations</strong> on template-generated services</li>
<li class=""><strong>Service running in dev in &lt; 30 minutes</strong> (was 2–5 days)</li>
</ul>
<p>The templates did not just make service creation faster — they made convention violations structurally impossible for new services. The "correct way" is the only way.</p>
<hr>
<p>The full template chain and output summary are documented in the <a class="" href="https://docs.victor.onl/docs/platform-convention/templates">Platform Convention — Templates</a>. The Backstage entity model and catalog design are in the <a class="" href="https://docs.victor.onl/docs/platform-convention/backstage">Backstage Convention</a>.</p>]]></content>
        <author>
            <name>Platform Engineering</name>
        </author>
        <category label="Backstage" term="Backstage"/>
        <category label="IDP" term="IDP"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[The ROI of Naming Things: How a 3-Segment Key Eliminated Cross-Team Confusion]]></title>
        <id>https://docs.victor.onl/blog/roi-of-naming-things</id>
        <link href="https://docs.victor.onl/blog/roi-of-naming-things"/>
        <updated>2026-05-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How a single 3-segment naming convention across Kubernetes, ArgoCD, Backstage, and Crossplane eliminated cross-team confusion and saved hours of manual mapping every week.]]></summary>
        <content type="html"><![CDATA[<p>Naming things is famously the hardest problem in computer science. It is also, quietly, one of the most expensive. When four different teams name their namespaces four different ways, the cost shows up in incidents that take longer to diagnose, in onboarding sessions that turn into tribal knowledge transfers, and in a platform team that spends its weeks translating between systems instead of building.</p>
<p>This post is about how a single naming convention — three segments, one key — unified Kubernetes, ArgoCD, Backstage, and Crossplane. And why the return on investment was measured in days saved per week.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-cost-of-naming-drift">The cost of naming drift<a href="https://docs.victor.onl/blog/roi-of-naming-things#the-cost-of-naming-drift" class="hash-link" aria-label="Direct link to The cost of naming drift" title="Direct link to The cost of naming drift" translate="no">​</a></h2>
<p>Before the convention, every team had its own style:</p>
<ul>
<li class="">Team A named namespaces <code>payments-api-prod</code></li>
<li class="">Team B used <code>prod-orders-backend</code></li>
<li class="">Team C went with <code>frontend_staging</code></li>
</ul>
<p>None of these are wrong. All of them are incompatible. When an incident hit <code>payments-api-prod</code>, the first question was always: <em>"What ArgoCD application manages this?"</em> Nobody knew without digging. The answer lived in someone's head, or in a spreadsheet last updated three months ago.</p>
<p>The real cost was not the naming itself — it was the <strong>manual mapping</strong> required every time a human or a tool needed to cross-reference systems:</p>
<table><thead><tr><th>Question</th><th>Without convention</th><th>With convention</th></tr></thead><tbody><tr><td>Which ArgoCD app manages this namespace?</td><td>Ask someone, check spreadsheet</td><td>Read the <code>argocd/app</code> label</td></tr><tr><td>Which team owns this?</td><td>Check JIRA, Slack, or git blame</td><td>Read the <code>team</code> label</td></tr><tr><td>What pods belong to this service across all envs?</td><td>Manual kubectl on each cluster</td><td><code>kubectl get ns -l backstage.io/component=gateway-api</code></td></tr><tr><td>Is this service healthy in prod?</td><td>Open Grafana, ArgoCD, kubectl separately</td><td>Open one Backstage Component page</td></tr></tbody></table>
<p>Each of these questions cost 5–15 minutes per occurrence. Multiply by the number of engineers asking them daily, and naming drift was consuming <strong>hours per week</strong> across the organization.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-semantic-key">The semantic key<a href="https://docs.victor.onl/blog/roi-of-naming-things#the-semantic-key" class="hash-link" aria-label="Direct link to The semantic key" title="Direct link to The semantic key" translate="no">​</a></h2>
<p>The convention is simple. Every resource in the platform is addressed by three segments:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">{project}-{env}-{service}</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">payments-prod-api</span><br></div></code></pre></div></div>
<table><thead><tr><th>Segment</th><th>What it means</th><th>Where it appears</th></tr></thead><tbody><tr><td><code>project</code></td><td>Ownership boundary (domain)</td><td>Backstage Domain, ArgoCD AppProject, namespace prefix</td></tr><tr><td><code>env</code></td><td>Deployment target</td><td>Cluster label, namespace middle segment</td></tr><tr><td><code>service</code></td><td>Individual workload</td><td>Namespace suffix, Backstage Component suffix</td></tr></tbody></table>
<p>The key insight is that this is not just a naming pattern — it is a <strong>contract</strong>. When the naming is consistent, tools can talk to each other without a human in the middle.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-one-key-connects-four-systems">How one key connects four systems<a href="https://docs.victor.onl/blog/roi-of-naming-things#how-one-key-connects-four-systems" class="hash-link" aria-label="Direct link to How one key connects four systems" title="Direct link to How one key connects four systems" translate="no">​</a></h2>
<p>Here is the full mapping for a single service:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">Backstage Domain:    payments          ← project</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ArgoCD AppProject:   payments          ← project</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Backstage System:    gateway           ← logical grouping</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ArgoCD ApplicationSet: gateway        ← logical grouping</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Backstage Component: gateway-api      ← system + service</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">ArgoCD Application:  gateway-api-prod ← system + service + env</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Kubernetes Namespace: payments-prod-api ← project + env + service</span><br></div></code></pre></div></div>
<p>When a developer opens the Backstage Component page for <code>gateway-api</code>, the Kubernetes plugin finds pods across <strong>all clusters</strong> using a single label selector: <code>project=payments,service=api</code>. No configuration per environment. No manual registration of each cluster's namespace. The label does the work.</p>
<p>The ArgoCD plugin surfaces sync status per environment using the same label approach. The dependency graph shows which databases and queues this service connects to.</p>
<p><strong>One name, one query, one page, one source of truth.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-9-labels-that-make-it-work">The 9 labels that make it work<a href="https://docs.victor.onl/blog/roi-of-naming-things#the-9-labels-that-make-it-work" class="hash-link" aria-label="Direct link to The 9 labels that make it work" title="Direct link to The 9 labels that make it work" translate="no">​</a></h2>
<p>Every namespace in the platform carries 9 required labels:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token key atrule" style="color:#00a4db">labels</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">project</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> payments               </span><span class="token comment" style="color:#999988;font-style:italic"># domain</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">env</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> prod                       </span><span class="token comment" style="color:#999988;font-style:italic"># environment</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">service</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> api                    </span><span class="token comment" style="color:#999988;font-style:italic"># workload</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">team</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> team</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">payments             </span><span class="token comment" style="color:#999988;font-style:italic"># owning team</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">backstage.io/domain</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> payments   </span><span class="token comment" style="color:#999988;font-style:italic"># → Backstage Domain</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">backstage.io/system</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> gateway    </span><span class="token comment" style="color:#999988;font-style:italic"># → Backstage System</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">backstage.io/component</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> gateway</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">api  </span><span class="token comment" style="color:#999988;font-style:italic"># → Backstage Component</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">argocd/app</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> gateway</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">api</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">prod    </span><span class="token comment" style="color:#999988;font-style:italic"># → ArgoCD Application</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">argocd/app-set</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> gateway         </span><span class="token comment" style="color:#999988;font-style:italic"># → ArgoCD ApplicationSet</span><br></div></code></pre></div></div>
<p>Missing any of these labels fails the PR in CI. There is no exception path. This is not enforced by documentation — it is enforced by code.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-business-impact">The business impact<a href="https://docs.victor.onl/blog/roi-of-naming-things#the-business-impact" class="hash-link" aria-label="Direct link to The business impact" title="Direct link to The business impact" translate="no">​</a></h2>
<p>After rolling out the convention to the first domain team:</p>
<table><thead><tr><th>Metric</th><th>Before</th><th>After</th></tr></thead><tbody><tr><td>Time to answer "who owns this namespace?"</td><td>5–15 min (ask around)</td><td>0 sec (read label)</td></tr><tr><td>Incident cross-referencing between tools</td><td>Manual, error-prone</td><td>Automatic via labels</td></tr><tr><td>New service bootstrapping</td><td>8–15 manual steps</td><td>1 template, 2 PRs</td></tr><tr><td>Namespaces with all required labels</td><td>&lt; 20%</td><td>100% (new services)</td></tr></tbody></table>
<p>The convention is the cheapest thing we built and the highest ROI investment in the entire platform. Every tool integration, every automation, every dashboard depends on it. Without consistent naming, the platform is just a collection of tools. With it, the platform is a product.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-lesson">The lesson<a href="https://docs.victor.onl/blog/roi-of-naming-things#the-lesson" class="hash-link" aria-label="Direct link to The lesson" title="Direct link to The lesson" translate="no">​</a></h2>
<p><strong>Invest in naming before investing in tooling.</strong> A perfect ArgoCD setup with inconsistent namespace names is worse than a basic setup with consistent ones. The naming convention is not a document to write after the architecture is done — it is the foundation the architecture is built on.</p>
<p>Every hour we spent getting the three-segment key right saved ten hours of debugging tool integrations later.</p>
<hr>
<p>The full naming convention and cross-system mapping are documented in the <a class="" href="https://docs.victor.onl/docs/platform-convention/naming-convention">Platform Convention</a>. The business goals and success metrics are in the <a class="" href="https://docs.victor.onl/docs/platform-prd/executive-summary">Platform PRD</a>.</p>]]></content>
        <author>
            <name>Platform Engineering</name>
        </author>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="IDP" term="IDP"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Building an Internal Developer Platform: From Ticket Hell to Self-Service in 30 Minutes]]></title>
        <id>https://docs.victor.onl/blog/building-an-idp</id>
        <link href="https://docs.victor.onl/blog/building-an-idp"/>
        <updated>2026-05-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How we built an Internal Developer Platform with Backstage, ArgoCD, Kubernetes, and Crossplane to replace ticket-driven provisioning with self-service in under 30 minutes.]]></summary>
        <content type="html"><![CDATA[<p>Every platform team reaches the same inflection point. The ticket queue grows faster than the team. Engineers wait days for a namespace, a database, or an ArgoCD application — things that should take minutes. The platform team becomes the bottleneck for every team trying to ship.</p>
<p>We hit that wall. This post is about how we got out of it.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem">The problem<a href="https://docs.victor.onl/blog/building-an-idp#the-problem" class="hash-link" aria-label="Direct link to The problem" title="Direct link to The problem" translate="no">​</a></h2>
<p>Before the IDP, onboarding a new service looked like this:</p>
<ol>
<li class="">Open a JIRA ticket for namespace creation</li>
<li class="">Wait 2–5 days for the platform team to review it</li>
<li class="">Receive the namespace — with the wrong labels, or missing resource limits</li>
<li class="">Open another ticket for the ArgoCD application</li>
<li class="">Open another ticket for the database</li>
</ol>
<p>Eight to fifteen manual steps across multiple tools. Every step required a human from the platform team. Every error required another round-trip.</p>
<p>The root cause was not a lack of automation. We had Terraform, we had Helm, we had ArgoCD. The problem was that <strong>none of it was self-service</strong>. The automation only ran when the platform team ran it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-built">What we built<a href="https://docs.victor.onl/blog/building-an-idp#what-we-built" class="hash-link" aria-label="Direct link to What we built" title="Direct link to What we built" translate="no">​</a></h2>
<p>The IDP is four tools working together:</p>
<table><thead><tr><th>Pillar</th><th>Tool</th><th>Role</th></tr></thead><tbody><tr><td>Developer portal</td><td>Backstage</td><td>Templates, catalog, observability</td></tr><tr><td>GitOps delivery</td><td>ArgoCD</td><td>Continuous delivery from Git to Kubernetes</td></tr><tr><td>Container runtime</td><td>Kubernetes</td><td>Workload scheduling and isolation</td></tr><tr><td>Cloud IaC</td><td>Crossplane</td><td>Cloud resources as Kubernetes objects</td></tr></tbody></table>
<p>The key insight was that <strong>conventions are the product</strong>. A naming convention is not documentation — it is the contract that lets every tool talk to every other tool without a human in the middle.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-semantic-key">The semantic key<a href="https://docs.victor.onl/blog/building-an-idp#the-semantic-key" class="hash-link" aria-label="Direct link to The semantic key" title="Direct link to The semantic key" translate="no">​</a></h2>
<p>Every resource in the platform — namespace, ArgoCD application, Backstage entity, Crossplane claim — is addressed by the same three-segment key:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token plain">{project}-{env}-{service}</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">payments-prod-api</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│         │     └── Kubernetes namespace: payments-prod-api</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│         │         Backstage Component: gateway-api</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">│         └── ArgoCD Application: gateway-api-prod</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">└── ArgoCD AppProject: payments</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    Backstage Domain: payments</span><br></div></code></pre></div></div>
<p>When the naming is consistent, the Backstage Kubernetes plugin can find all pods for a service across every cluster with a single label selector. The ArgoCD plugin can surface sync status per environment. No manual mapping. No spreadsheets.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="backstage-templates-as-golden-paths">Backstage templates as golden paths<a href="https://docs.victor.onl/blog/building-an-idp#backstage-templates-as-golden-paths" class="hash-link" aria-label="Direct link to Backstage templates as golden paths" title="Direct link to Backstage templates as golden paths" translate="no">​</a></h2>
<p>The self-service layer is a set of Backstage Scaffolder templates — one per platform operation:</p>
<ul>
<li class=""><strong><code>create-domain</code></strong> — creates the ownership boundary, the GitOps repo, and the ArgoCD AppProject</li>
<li class=""><strong><code>create-system</code></strong> — creates a product grouping, its ApplicationSet, and TechDocs base</li>
<li class=""><strong><code>create-service</code></strong> — scaffolds an Application Repository (with Dockerfile, CI workflow, and TechDocs base), generates Kubernetes manifests per environment, registers the catalog entry, and opens the PRs</li>
<li class=""><strong><code>create-resource</code></strong> — creates Crossplane Claims for cloud infrastructure (databases, queues, buckets)</li>
<li class=""><strong><code>create-secret</code></strong> — provides self-service secure secrets encryption using Sealed Secrets</li>
<li class=""><strong><code>create-group</code></strong> / <strong><code>create-user</code></strong> — onboards teams and engineers with correct RBAC from day one</li>
</ul>
<p>A product engineer runs <code>create-service</code>, selects their system, names the service, picks a resource profile, and clicks <strong>Create</strong>. Two PRs are opened automatically. When both are merged, ArgoCD detects the new ApplicationSet element and syncs the service to the dev cluster. <strong>The service is running in under 30 minutes with zero platform team involvement.</strong></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="convention-validation-in-ci">Convention validation in CI<a href="https://docs.victor.onl/blog/building-an-idp#convention-validation-in-ci" class="hash-link" aria-label="Direct link to Convention validation in CI" title="Direct link to Convention validation in CI" translate="no">​</a></h2>
<p>Templates guarantee correct output at creation time. CI validation keeps repos correct over time.</p>
<p>Every domain GitOps repository includes a GitHub Actions workflow (<code>validate-conventions.yaml</code>) generated by <code>create-domain</code>. On every PR it runs <code>validate-namespaces.sh</code> and checks:</p>
<ul>
<li class="">Namespace naming matches <code>{project}-{env}-{service}</code></li>
<li class="">All 9 required labels are present</li>
<li class="">Every container has resource requests and limits</li>
<li class="">Kubernetes manifests pass schema validation (kubeconform)</li>
<li class="">ArgoCD dry-run diff against the dev cluster passes</li>
</ul>
<p>Convention violations block the PR merge. There is no exception path.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="cloud-infrastructure-as-code--without-terraform-expertise">Cloud infrastructure as code — without Terraform expertise<a href="https://docs.victor.onl/blog/building-an-idp#cloud-infrastructure-as-code--without-terraform-expertise" class="hash-link" aria-label="Direct link to Cloud infrastructure as code — without Terraform expertise" title="Direct link to Cloud infrastructure as code — without Terraform expertise" translate="no">​</a></h2>
<p>Cloud resources (databases, queues, storage) are declared as Crossplane Claims committed to Git. ArgoCD syncs them to the cluster. Crossplane provisions the actual resource on GCP, AWS, Azure, or IBM.</p>
<p>A developer requesting a Cloud SQL instance does not write Terraform. They run <code>create-resource</code>, select GCP → Cloud SQL, fill in a form, and merge the PR. The Claim lands in <code>crossplane/claims/prod/cloudsql-main.yaml</code>. Crossplane reconciles continuously — drift is corrected automatically. <code>deletionPolicy: Orphan</code> on production Claims means an accidental <code>kubectl delete</code> cannot destroy a database.</p>
<p>The Backstage Resource page for that database shows <code>READY: True</code> and <code>SYNCED: True</code> once provisioning completes. No digging through cloud console.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="rbac-without-the-maintenance-burden">RBAC without the maintenance burden<a href="https://docs.victor.onl/blog/building-an-idp#rbac-without-the-maintenance-burden" class="hash-link" aria-label="Direct link to RBAC without the maintenance burden" title="Direct link to RBAC without the maintenance burden" translate="no">​</a></h2>
<p>Access is granted via identity provider groups, never individual users. Removing someone from the GitHub or Okta group immediately revokes all Kubernetes access — no manual RoleBinding cleanup.</p>
<p>The <code>developer</code> role has no RoleBinding created for production namespaces. Not by convention. Not by documentation. By the fact that the binding does not exist. Production access requires explicit role elevation.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="secure-secrets-management">Secure secrets management<a href="https://docs.victor.onl/blog/building-an-idp#secure-secrets-management" class="hash-link" aria-label="Direct link to Secure secrets management" title="Direct link to Secure secrets management" translate="no">​</a></h2>
<p>Access to production environments is strictly controlled, and the platform enforces a strict "no plain-text secrets" rule.
Instead of opening tickets to manage secrets, developers use the <strong><code>create-secret</code></strong> Backstage template. They encrypt their secrets locally using <code>kubeseal</code> and fill out the template. The platform automatically opens a PR with the <code>SealedSecret</code> manifest directly into their domain's GitOps repository. The platform service <code>sealed-secrets</code> runs in the cluster and handles the decryption, ensuring secure secrets management remains entirely self-service.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="results-so-far">Results so far<a href="https://docs.victor.onl/blog/building-an-idp#results-so-far" class="hash-link" aria-label="Direct link to Results so far" title="Direct link to Results so far" translate="no">​</a></h2>
<p>We are in Phase 1 of the rollout. The first domain team is fully self-sufficient:</p>
<ul>
<li class="">Service creation: <strong>&lt; 30 minutes</strong> (was 2–5 days)</li>
<li class="">Platform team tickets: <strong>trending down</strong></li>
<li class="">Namespaces with all required labels: <strong>100%</strong> (for new services)</li>
<li class="">Engineer onboarding time: <strong>&lt; 2 hours</strong> (was 1–3 days)</li>
</ul>
<p>Phase 2 will extend self-service to all product teams and bring Backstage full-stack observability to every service. Phase 3 migrates existing services and cloud resources into the catalog.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-learned">What we learned<a href="https://docs.victor.onl/blog/building-an-idp#what-we-learned" class="hash-link" aria-label="Direct link to What we learned" title="Direct link to What we learned" translate="no">​</a></h2>
<p><strong>Conventions first.</strong> No tool integration works without consistent naming. We spent time on the convention before building templates, and every hour there saved ten hours of debugging tool integrations.</p>
<p><strong>Templates as the enforcement mechanism.</strong> Documentation gets ignored. Templates make the correct path the only path. If you can run <code>create-service</code> and get a working service, you have no reason to do it manually.</p>
<p><strong>GitOps scales.</strong> ApplicationSet matrix generators mean adding a new service to a new cluster is one line in a YAML file. ArgoCD handles the rest. We are not creating Applications by hand.</p>
<p><strong>Crossplane for cloud IaC belongs in Kubernetes.</strong> Keeping cloud resources in the same reconciliation loop as application workloads means one place to look for drift, one place to look for status, one RBAC model for access.</p>
<hr>
<p>The full requirements, architecture, and roadmap are documented in the <a class="" href="https://docs.victor.onl/docs/platform-prd/executive-summary">Platform PRD</a>. The naming convention and operational standards are in the <a class="" href="https://docs.victor.onl/docs/platform-convention/intro">Platform Convention</a>.</p>]]></content>
        <author>
            <name>Platform Engineering</name>
        </author>
        <category label="IDP" term="IDP"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="Backstage" term="Backstage"/>
        <category label="GitOps" term="GitOps"/>
        <category label="Kubernetes" term="Kubernetes"/>
    </entry>
</feed>