<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://docs.kuberocketci.io/blog</id>
    <title>KubeRocketCI Blog</title>
    <updated>2026-07-14T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://docs.kuberocketci.io/blog"/>
    <subtitle>KubeRocketCI Blog</subtitle>
    <icon>https://docs.kuberocketci.io/img/favicon.ico</icon>
    <rights>Copyright © 2026 KubeRocketCI.</rights>
    <entry>
        <title type="html"><![CDATA[Cancel In-Progress Tekton Pipelines: Stop Wasting CI on Outdated Commits]]></title>
        <id>https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci</id>
        <link href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci"/>
        <updated>2026-07-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[KubeRocketCI can now cancel in-progress review PipelineRuns the moment a new commit supersedes them - gracefully, with git status still reported. Here is how.]]></summary>
        <content type="html"><![CDATA[<p>Push a commit to a merge request and your review pipeline starts. Spot a typo, push again thirty seconds later - and now <em>two</em> pipelines are running, one of them validating a commit that no longer matters. GitHub Actions solved this years ago with <code>concurrency.cancel-in-progress</code>; Tekton has no built-in equivalent, so on a Tekton-based platform those superseded runs keep burning CPU until they finish, producing results nobody will read.</p>
<p>KubeRocketCI 3.14 closes that gap with a single chart flag: <code>pipelines.cancelInProgress</code>. When enabled, the KRCI <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">Tekton interceptor</a> cancels still-running review PipelineRuns for the same pull request before triggering the replacement. The cancellation is graceful (<code>CancelledRunFinally</code>): the superseded run still executes its <code>finally</code> tasks, so your git provider gets a status for the old commit instead of an eternally "pending" check. This post shows the whole flow live on the local <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">try-kuberocketci</a> testbed - every screenshot and command output is from a real run.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-problem-superseded-review-runs-pile-up">The Problem: Superseded Review Runs Pile Up<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#the-problem-superseded-review-runs-pile-up" class="hash-link" aria-label="Direct link to The Problem: Superseded Review Runs Pile Up" title="Direct link to The Problem: Superseded Review Runs Pile Up" translate="no">​</a></h2>
<p>A review (pull request) pipeline exists to answer one question: <em>is this commit mergeable?</em> The moment a newer commit lands on the same merge request, that question changes - and every pipeline still validating the old commit is wasted work. The pattern is worst exactly where CI matters most: active MRs with quick fix-up pushes, <code>/recheck</code> comments, rebases. Each event spawns a full pipeline - clone, build, lint, SonarQube scan, image verification - and on a shared cluster those zombie runs compete with useful work for CPU, memory, and runner capacity.</p>
<p>GitHub Actions users express the fix in two lines of YAML:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_OeMC">GitHub Actions equivalent (for comparison)</div><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">concurrency</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">group</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> review</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">$</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> github.ref </span><span class="token punctuation" style="color:#393A34">}</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">cancel-in-progress</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><br></div></code></pre></div></div>
<p>Tekton, by design, has no such primitive - a <code>PipelineRun</code> is a standalone Kubernetes resource with no concept of "group" or "supersedes". The <a href="https://github.com/tektoncd/community/pull/716" target="_blank" rel="noopener noreferrer" class="">Tekton community has discussed pipeline concurrency for years</a> without a shipped core feature. So the platform layer has to provide it - and in KubeRocketCI, the natural place is the <strong>krci interceptor</strong>, the component that already sits between every git webhook and every triggered pipeline.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-cancel-in-progress-works">How Cancel-In-Progress Works<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#how-cancel-in-progress-works" class="hash-link" aria-label="Direct link to How Cancel-In-Progress Works" title="Direct link to How Cancel-In-Progress Works" translate="no">​</a></h2>
<p>The interceptor enriches webhook payloads for all four supported git providers (GitHub, GitLab, Bitbucket, Gerrit) before Tekton's EventListener instantiates a PipelineRun. That position makes it the perfect referee: it sees the <em>new</em> event before the new run exists, and it can find every <em>old</em> run for the same change because review PipelineRuns are now stamped with a <code>app.edp.epam.com/git-change-number</code> label at creation time.</p>
<!-- -->
<p>The design choices matter as much as the mechanism:</p>
<ul>
<li class=""><strong>Graceful, not brutal.</strong> The interceptor patches <code>spec.status: CancelledRunFinally</code> - Tekton stops scheduling new tasks and cancels running ones, but the pipeline's <code>finally</code> section still executes. In KubeRocketCI's pipelines-library, that is where commit status is reported back to the git provider - so the superseded commit ends up marked <em>failed</em>, never stuck <em>pending</em>.</li>
<li class=""><strong>Review pipelines only.</strong> The <code>cancelInProgress</code> parameter is injected into review Triggers exclusively. Build pipelines - the ones producing versioned artifacts after a merge - are never cancelled.</li>
<li class=""><strong>Best-effort by contract.</strong> If listing or patching fails, the error is logged and the new pipeline triggers anyway. Cancellation can never break your CI.</li>
<li class=""><strong>Scoped to one change.</strong> The label selector matches codebase + pipeline type + change number, so parallel MRs on the same repository never interfere with each other.</li>
<li class=""><strong>RBAC follows the flag.</strong> The interceptor's Role gains <code>patch</code> on <code>pipelineruns</code> only when the feature is enabled.</li>
</ul>
<p>Enabling it is one Helm value on the pipelines-library chart:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_OeMC">values.yaml (pipelines-library)</div><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">pipelines</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 comment" style="color:#999988;font-style:italic"># Cancel in-progress review PipelineRuns when a new commit is pushed to the same</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># Pull Request / Merge Request / Gerrit change. Cancellation is graceful</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#999988;font-style:italic"># (spec.status: CancelledRunFinally), so finally tasks of the superseded run still execute.</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">cancelInProgress</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="walkthrough-two-commits-one-surviving-pipeline">Walkthrough: Two Commits, One Surviving Pipeline<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#walkthrough-two-commits-one-surviving-pipeline" class="hash-link" aria-label="Direct link to Walkthrough: Two Commits, One Surviving Pipeline" title="Direct link to Walkthrough: Two Commits, One Surviving Pipeline" translate="no">​</a></h2>
<p>The environment is the same local testbed as my previous posts: a <a href="https://kind.sigs.k8s.io/" target="_blank" rel="noopener noreferrer" class="">kind</a> cluster with KubeRocketCI, Tekton, and self-hosted GitLab, with the Go sample application <code>test-go-app</code> onboarded and <code>pipelines.cancelInProgress: true</code> set.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1-open-a-merge-request">Step 1: Open a Merge Request<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#step-1-open-a-merge-request" class="hash-link" aria-label="Direct link to Step 1: Open a Merge Request" title="Direct link to Step 1: Open a Merge Request" translate="no">​</a></h3>
<p>I create a branch, add a commit, and open a merge request through the GitLab API - the same thing a developer does from the terminal:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ </span><span class="token function" style="color:#d73a49">git</span><span class="token plain"> checkout </span><span class="token parameter variable" style="color:#36acaa">-b</span><span class="token plain"> feature-cancel-demo </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">git</span><span class="token plain"> commit </span><span class="token parameter variable" style="color:#36acaa">-m</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"feat: add feature (attempt 1)"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">git</span><span class="token plain"> push</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># MR !2 "Add feature" opened against main</span><br></div></code></pre></div></div>
<p>The webhook fires, and the review pipeline starts. Note the change-number label - this is the handle the interceptor will use later:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get pipelinerun --show-labels </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> review</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">review-test-go-app-main-2p446   Unknown   Running   2s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  app.edp.epam.com/codebase</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">test-go-app,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  app.edp.epam.com/pipelinetype</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">review,</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  app.edp.epam.com/git-change-number</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">2</span><span class="token plain">, </span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">.</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2-push-a-fix-while-the-pipeline-is-still-running">Step 2: Push a Fix While the Pipeline Is Still Running<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#step-2-push-a-fix-while-the-pipeline-is-still-running" class="hash-link" aria-label="Direct link to Step 2: Push a Fix While the Pipeline Is Still Running" title="Direct link to Step 2: Push a Fix While the Pipeline Is Still Running" translate="no">​</a></h3>
<p>Ninety seconds in - the pipeline is mid-flight, fetch, build and sonar already done, image verification in progress - I notice the typo and push the fix to the same branch:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ </span><span class="token function" style="color:#d73a49">git</span><span class="token plain"> commit </span><span class="token parameter variable" style="color:#36acaa">-m</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"feat: add feature (attempt 2, fixes review)"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;&amp;</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">git</span><span class="token plain"> push</span><br></div></code></pre></div></div>
<p>GitLab sends the <code>update</code> webhook. The interceptor finds the still-running pipeline for change 2, cancels it, and lets the new one trigger. Twenty seconds later the picture is exactly what you want:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get pipelinerun</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                            SUCCEEDED   REASON      STARTTIME   COMPLETIONTIME</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">review-test-go-app-main-2p446   False       Cancelled   104s        15s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">review-test-go-app-main-8th5r   Unknown     Running     22s</span><br></div></code></pre></div></div>
<p>One log line in the interceptor tells the story:</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">Canceled in-progress PipelineRun review-test-go-app-main-2p446 superseded</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">by a new event for codebase test-go-app change 2</span><br></div></code></pre></div></div>
<p>The Portal shows both runs side by side - the superseded run <strong>Cancelled</strong> at 1m 29s instead of running to completion, the replacement already underway:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal pipeline runs list for test-go-app showing the superseded review run Cancelled and the replacement review run Completed for the same pull request" src="https://docs.kuberocketci.io/assets/images/pipeline-runs-cancelled-and-completed-9a72a6f6fddf59707ff275a1e323a437.png" title="The superseded run is cancelled; only the run for the latest commit survives" width="1600" height="1000" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3-verify-the-cancellation-was-graceful">Step 3: Verify the Cancellation Was Graceful<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#step-3-verify-the-cancellation-was-graceful" class="hash-link" aria-label="Direct link to Step 3: Verify the Cancellation Was Graceful" title="Direct link to Step 3: Verify the Cancellation Was Graceful" translate="no">​</a></h3>
<p>Open the cancelled run in the Portal. The status is <code>Cancelled</code>, the spec says <code>CancelledRunFinally</code>, and the task list is the interesting part:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal PipelineRun details of the cancelled review run, showing completed tasks, the cancelled dockerbuild-verify task and the executed finally tasks" src="https://docs.kuberocketci.io/assets/images/cancelled-run-details-a771177fa2a95939348cb2bb7aa81426.png" title="Inside the cancelled run: finished tasks stay finished, the in-flight task is cancelled" width="1600" height="1000" class="img_ev3q"></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get taskrun </span><span class="token parameter variable" style="color:#36acaa">-l</span><span class="token plain"> tekton.dev/pipelineRun</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">review-test-go-app-main-2p446</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                                       OK      REASON</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">.-fetch-repository                       True    Succeeded</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">.-build                                  True    Succeeded</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">.-sonar                                  True    Succeeded</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">.-dockerbuild-verify                     False   TaskRunCancelled   </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">- was </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> flight</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">.-gitlab-set-failure-status              True    Succeeded          </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">- finally task ran</span><br></div></code></pre></div></div>
<p><code>dockerbuild-verify</code> - the task that was running when the second commit arrived - was cancelled. But the <code>finally</code> task <code>gitlab-set-failure-status</code> executed <em>after</em> the cancellation and reported the outcome to GitLab:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal showing the gitlab-set-failure-status finally task of the cancelled run succeeded, reporting the failed state for the superseded commit to GitLab" src="https://docs.kuberocketci.io/assets/images/cancelled-run-finally-task-0aad0605eef1b8c96cc7f2f7de6a16fb.png" title="The finally task still runs: GitLab gets a status for the superseded commit" width="1600" height="1000" class="img_ev3q"></p>
<p>The git provider's view confirms it - no dangling "pending" check anywhere:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># commit #1 (superseded)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ glab api projects/krci%2Ftest-go-app/repository/commits/7fb8e40/statuses</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Review Pipeline -</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> failed</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"></span><span class="token comment" style="color:#999988;font-style:italic"># commit #2 (current)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ glab api projects/krci%2Ftest-go-app/repository/commits/5ffb585/statuses</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Review Pipeline -</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> success</span><br></div></code></pre></div></div>
<p>The replacement run finished green, the MR shows a passing pipeline for the latest commit, and the cluster spent zero extra minutes on the outdated one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-this-saves">What This Saves<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#what-this-saves" class="hash-link" aria-label="Direct link to What This Saves" title="Direct link to What This Saves" translate="no">​</a></h2>
<p>Back-of-the-envelope for a real team: the review pipeline above takes about 4 minutes of pod time (clone, build, lint, SonarQube, image verification). A ten-developer team pushing an average of one fix-up commit per MR wastes one full pipeline per MR - dozens of pipeline-hours per month on a busy repository, all competing with useful runs for cluster capacity. Concurrency limits and cluster autoscaling treat the symptom; cancelling superseded runs removes the work itself. It is the same reasoning that makes <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">ephemeral environments</a> cheaper than permanent staging: the cheapest workload is the one that stops existing when it stops being useful.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="configuration-reference">Configuration Reference<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#configuration-reference" class="hash-link" aria-label="Direct link to Configuration Reference" title="Direct link to Configuration Reference" translate="no">​</a></h2>
<table><thead><tr><th>Setting</th><th>Where</th><th>Default</th><th>Effect</th></tr></thead><tbody><tr><td><code>pipelines.cancelInProgress</code></td><td>pipelines-library Helm values</td><td><code>false</code></td><td>Cancel in-progress review PipelineRuns superseded by a new event for the same change</td></tr><tr><td><code>app.edp.epam.com/git-change-number</code></td><td>Label stamped on review PipelineRuns</td><td>-</td><td>Selector the interceptor uses to find superseded runs</td></tr></tbody></table>
<p>Behavior summary:</p>
<ul>
<li class=""><strong>Triggers cancellation:</strong> a new commit pushed to the same PR/MR/Gerrit change, and re-trigger comments (<code>/recheck</code>, <code>/ok-to-test</code>).</li>
<li class=""><strong>Cancels:</strong> review PipelineRuns for the same codebase and change number that are not already done or cancelled.</li>
<li class=""><strong>Never touches:</strong> build pipelines, runs for other changes, runs for other codebases.</li>
<li class=""><strong>On failure:</strong> logs the error and proceeds - the new pipeline always triggers.</li>
</ul>
<p>Handy one-liner to see every run for a given pull request, newest first:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get pipelinerun </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 parameter variable" style="color:#36acaa">-l</span><span class="token plain"> app.edp.epam.com/git-change-number</span><span class="token operator" style="color:#393A34">=</span><span class="token number" style="color:#36acaa">2</span><span class="token plain">,app.edp.epam.com/codebase</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">test-go-app </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">  --sort-by</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">.metadata.creationTimestamp</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="frequently-asked-questions">Frequently Asked Questions<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#frequently-asked-questions" class="hash-link" aria-label="Direct link to Frequently Asked Questions" title="Direct link to Frequently Asked Questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-tekton-have-a-built-in-equivalent-of-github-actions-cancel-in-progress">Does Tekton have a built-in equivalent of GitHub Actions cancel-in-progress?<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#does-tekton-have-a-built-in-equivalent-of-github-actions-cancel-in-progress" class="hash-link" aria-label="Direct link to Does Tekton have a built-in equivalent of GitHub Actions cancel-in-progress?" title="Direct link to Does Tekton have a built-in equivalent of GitHub Actions cancel-in-progress?" translate="no">​</a></h3>
<p>No. Tekton PipelineRuns are independent Kubernetes resources with no native concurrency groups; pipeline concurrency has been a long-running community discussion without a shipped core feature. KubeRocketCI implements the pattern at the platform layer, in the interceptor that already processes every git webhook.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-cancelledrunfinally-instead-of-just-deleting-the-pipelinerun">Why CancelledRunFinally instead of just deleting the PipelineRun?<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#why-cancelledrunfinally-instead-of-just-deleting-the-pipelinerun" class="hash-link" aria-label="Direct link to Why CancelledRunFinally instead of just deleting the PipelineRun?" title="Direct link to Why CancelledRunFinally instead of just deleting the PipelineRun?" translate="no">​</a></h3>
<p>Deleting (or hard-cancelling) a run would leave the superseded commit with a forever-pending status in GitLab or GitHub, and would skip cleanup logic. <code>CancelledRunFinally</code> stops the useless work but still executes the pipeline's <code>finally</code> tasks - in KubeRocketCI's pipeline library that is where the commit status is reported, so the old commit is cleanly marked as failed.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="are-build-pipelines-ever-cancelled">Are build pipelines ever cancelled?<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#are-build-pipelines-ever-cancelled" class="hash-link" aria-label="Direct link to Are build pipelines ever cancelled?" title="Direct link to Are build pipelines ever cancelled?" translate="no">​</a></h3>
<p>No. The <code>cancelInProgress</code> interceptor parameter is injected into review Triggers only. Build pipelines - triggered by merges and producing versioned artifacts - always run to completion.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-git-providers-are-supported">Which git providers are supported?<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#which-git-providers-are-supported" class="hash-link" aria-label="Direct link to Which git providers are supported?" title="Direct link to Which git providers are supported?" translate="no">​</a></h3>
<p>All four KubeRocketCI providers: GitHub, GitLab, Bitbucket, and Gerrit. The change number is parsed from each provider's webhook payload (including Gerrit's, which sends it as either an integer or a string), and the same label-based cancellation logic applies everywhere.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-happens-if-the-cancellation-itself-fails">What happens if the cancellation itself fails?<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#what-happens-if-the-cancellation-itself-fails" class="hash-link" aria-label="Direct link to What happens if the cancellation itself fails?" title="Direct link to What happens if the cancellation itself fails?" translate="no">​</a></h3>
<p>Nothing visible to the developer. Cancellation is best-effort: a failure to list or patch PipelineRuns is logged by the interceptor and the new pipeline is triggered regardless. The worst case is the old behavior - two runs in parallel - never a blocked pipeline.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="can-two-different-merge-requests-cancel-each-others-pipelines">Can two different merge requests cancel each other's pipelines?<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#can-two-different-merge-requests-cancel-each-others-pipelines" class="hash-link" aria-label="Direct link to Can two different merge requests cancel each other's pipelines?" title="Direct link to Can two different merge requests cancel each other's pipelines?" translate="no">​</a></h3>
<p>No. The interceptor matches on codebase <em>and</em> pipeline type <em>and</em> change number. Parallel MRs on the same repository each have their own change number, so their pipelines coexist untouched.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-i-enable-this-on-an-existing-kuberocketci-installation">How do I enable this on an existing KubeRocketCI installation?<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#how-do-i-enable-this-on-an-existing-kuberocketci-installation" class="hash-link" aria-label="Direct link to How do I enable this on an existing KubeRocketCI installation?" title="Direct link to How do I enable this on an existing KubeRocketCI installation?" translate="no">​</a></h3>
<p>Set <code>pipelines.cancelInProgress: true</code> in the pipelines-library chart values and upgrade the release. The chart adds the interceptor parameter to review Triggers, stamps the change-number label on review runs, and extends the interceptor's RBAC with <code>patch</code> on <code>pipelineruns</code> - all gated by the same flag, nothing else changes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://docs.kuberocketci.io/blog/cancel-in-progress-pipelines-tekton-kuberocketci#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<p>Every push to an active merge request used to leave a zombie review pipeline burning cluster resources on a commit nobody cares about anymore. With <code>pipelines.cancelInProgress: true</code>, the KRCI interceptor now cancels superseded review runs the instant the replacing event arrives - gracefully, so <code>finally</code> tasks still report a status for the old commit, scoped by a change-number label so parallel MRs never collide, and best-effort so CI can never be blocked by its own cleanup.</p>
<p>In this run, the second commit landed 90 seconds into the first commit's pipeline: the in-flight task was cancelled on the spot, GitLab received a <em>failed</em> status for the superseded commit and a <em>success</em> for the new one, and only one pipeline ran to completion. The whole feature is one Helm value.</p>
<p>Next steps from here:</p>
<ul>
<li class="">Stand up the same environment with <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">Try KubeRocketCI Locally</a> and replay the flow.</li>
<li class="">Read <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">Kubernetes-Native CI/CD with Tekton</a> for the architecture this builds on - the interceptor, triggers, and the pipelines library.</li>
<li class="">See <a class="" href="https://docs.kuberocketci.io/docs/user-guide/tekton-pipelines">Manage Tekton Pipelines</a> for the day-to-day pipeline workflow in the Portal.</li>
<li class="">Explore the implementation in <a href="https://github.com/epam/edp-tekton" target="_blank" rel="noopener noreferrer" class="">edp-tekton on GitHub</a> - the cancellation logic lives in <code>pkg/interceptor/cancel_pipelineruns.go</code>.</li>
</ul>
<p>KubeRocketCI is open source under Apache License 2.0. The platform, Helm charts, and the local testbed are all on <a href="https://github.com/KubeRocketCI/try-kuberocketci" target="_blank" rel="noopener noreferrer" class="">GitHub</a>.</p>
]]></content>
        <author>
            <name>Sergiy Kulanov</name>
            <uri>https://github.com/sergk</uri>
        </author>
        <category label="KubeRocketCI" term="KubeRocketCI"/>
        <category label="Tekton" term="Tekton"/>
        <category label="Kubernetes" term="Kubernetes"/>
        <category label="CI/CD" term="CI/CD"/>
        <category label="Pipelines" term="Pipelines"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="GitOps" term="GitOps"/>
        <category label="Open Source" term="Open Source"/>
        <category label="GitLab" term="GitLab"/>
        <category label="Cost Optimization" term="Cost Optimization"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Stale Branch Cleanup on Kubernetes: Keep Your Platform in Sync with Git]]></title>
        <id>https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci</id>
        <link href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci"/>
        <updated>2026-07-13T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[KubeRocketCI now detects git branches deleted from the repository, marks the matching Kubernetes resources as stale, and can clean them up safely - automatically.]]></summary>
        <content type="html"><![CDATA[<p>Every feature branch you create in KubeRocketCI becomes a <code>CodebaseBranch</code> resource in Kubernetes - the object that drives its CI pipelines, records its built images, and feeds <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">ephemeral preview environments</a>. But when that branch merges and someone deletes it in GitLab or GitHub, the Kubernetes side never hears about it. The <code>CodebaseBranch</code> lives on: it clutters the Portal, offers a Build button for a branch that no longer exists, and on a busy platform these orphans accumulate by the hundreds.</p>
<p>KubeRocketCI 3.14 closes that gap. The codebase-operator now periodically verifies every branch against the real git repository, marks missing ones with a <strong>Stale</strong> condition (and a badge in the Portal), and - if you opt in - deletes them automatically, while refusing to touch any branch that is still wired into a deployment. This post shows the whole lifecycle live on the local <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">try-kuberocketci</a> testbed: every screenshot and command output below is from a real run.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-stale-branches-accumulate">Why Stale Branches Accumulate<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#why-stale-branches-accumulate" class="hash-link" aria-label="Direct link to Why Stale Branches Accumulate" title="Direct link to Why Stale Branches Accumulate" translate="no">​</a></h2>
<p>The branch-per-feature workflow that makes <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">preview environments</a> so effective is also what produces the litter. The lifecycle of a healthy feature branch ends with a merge and a <code>git branch -d</code> - usually a checkbox in the merge request. That deletion happens <em>in git</em>. The platform objects created for the branch - the <code>CodebaseBranch</code> resource, its <code>CodebaseImageStream</code> (the record of images built from it) - are Kubernetes resources, and nothing in the git provider tells Kubernetes to remove them.</p>
<p>The result is drift between two sources of truth:</p>
<ul>
<li class=""><strong>The Portal shows branches that do not exist.</strong> New team members trigger builds against them and get confusing clone failures.</li>
<li class=""><strong>Resources pile up.</strong> Platforms with dozens of codebases and a branch-per-ticket habit accumulate hundreds of dead <code>CodebaseBranch</code> and <code>CodebaseImageStream</code> objects.</li>
<li class=""><strong>Nobody knows which ones are safe to delete.</strong> A <code>CodebaseBranch</code> may look abandoned and still be the input of a live deployment - deleting the wrong one breaks an environment.</li>
</ul>
<p>Manual cleanup does not scale, and a naive cron that deletes anything old is dangerous. What you want is exactly what a Kubernetes operator is for: continuously observe external state (git), record it on the resource, and act on it with guardrails.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-stale-branch-detection-works">How Stale Branch Detection Works<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#how-stale-branch-detection-works" class="hash-link" aria-label="Direct link to How Stale Branch Detection Works" title="Direct link to How Stale Branch Detection Works" translate="no">​</a></h2>
<p>The codebase-operator runs a <strong>stale branch checker</strong> - a leader-elected background loop, not a webhook - because branch deletion in git produces no Kubernetes event to react to. On every tick it makes one cheap <code>git ls-remote</code> call per repository (no cloning, no working tree) and compares the listed branches against the <code>CodebaseBranch</code> resources in the namespace:</p>
<!-- -->
<p>The verdict is recorded in three places, so both humans and tooling can consume it:</p>
<ul>
<li class=""><strong>A <code>Stale</code> status condition</strong> on the <code>CodebaseBranch</code> - the source of truth, with a reason (<code>BranchNotFoundInGit</code>) and a human-readable message.</li>
<li class=""><strong>The <code>app.edp.epam.com/stale</code> label</strong>, mirroring the condition so you can filter with a plain label selector.</li>
<li class=""><strong>Kubernetes events</strong> (<code>BranchStale</code>, <code>StaleBranchDeleted</code>, <code>StaleBranchRetained</code>, <code>BranchStaleResolved</code>) for audit trails and alerting.</li>
</ul>
<p>Just as important is what the checker will <em>not</em> do:</p>
<ul>
<li class=""><strong>No marking on infrastructure errors.</strong> If the git server is unreachable or credentials are rejected, the sweep skips that repository entirely - a GitLab outage will never mass-mark your branches.</li>
<li class=""><strong>Default branches are never checked.</strong> <code>main</code> cannot go stale.</li>
<li class=""><strong>Branches not yet pushed or already terminating are skipped.</strong></li>
<li class=""><strong>A branch that reappears in git is un-marked</strong> on the next sweep (<code>BranchStaleResolved</code>).</li>
</ul>
<p>Detection is always on and needs no configuration - the interval defaults to 24 hours (<code>branchStaleCheckInterval</code> Helm value; <code>0</code> disables the checker). Cleanup is a separate, opt-in decision per codebase, which we will get to below.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="walkthrough-marking-stale-branches-on-a-live-cluster">Walkthrough: Marking Stale Branches on a Live Cluster<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#walkthrough-marking-stale-branches-on-a-live-cluster" class="hash-link" aria-label="Direct link to Walkthrough: Marking Stale Branches on a Live Cluster" title="Direct link to Walkthrough: Marking Stale Branches on a Live Cluster" translate="no">​</a></h2>
<p>The setup is the same local testbed as the <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">preview environments post</a>: a <a href="https://kind.sigs.k8s.io/" target="_blank" rel="noopener noreferrer" class="">kind</a> cluster with KubeRocketCI, Tekton, Argo CD, and a self-hosted GitLab, with the sample Go application <code>test-go-app</code> already onboarded. The only deviation from defaults: I set the check interval to <code>2m</code> so the demo does not wait a day between sweeps.</p>
<p>The operator confirms the checker is running on startup:</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">INFO  stale-branch-checker  Starting stale branch checker  {"interval": "2m0s"}</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">INFO  stale-branch-checker  Listing remote branches  {"repository": "git@gitlab.127.0.0.1.nip.io:krci/test-go-app.git"}</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">INFO  stale-branch-checker  Remote branches listed successfully  {"count": 2}</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">INFO  stale-branch-checker  Codebase branches staleness check finished  {"codebases": 2}</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1-create-two-feature-branches">Step 1: Create Two Feature Branches<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#step-1-create-two-feature-branches" class="hash-link" aria-label="Direct link to Step 1: Create Two Feature Branches" title="Direct link to Step 1: Create Two Feature Branches" translate="no">​</a></h3>
<p>In the <code>test-go-app</code> component: <strong>Branches → Create Branch</strong>. I create <code>feature-tt-123</code> and <code>feature-tt-456</code> from <code>main</code>, keeping the default pipelines.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal Create Branch dialog for test-go-app, creating feature-tt-123 from main" src="https://docs.kuberocketci.io/assets/images/create-feature-branch-bcba13adb92396d300dfac18e134cc2a.png" title="Creating a feature branch in the KubeRocketCI Portal" width="1600" height="1000" class="img_ev3q"></p>
<p>Each click creates a <code>CodebaseBranch</code> resource and pushes the branch to GitLab. Note the new <strong>STALE</strong> column on the resource - empty for now, because every branch exists in git:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebasebranch</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                               RESULT    STATUS    CODEBASE NAME   BRANCH           STALE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci-gitops-main                   success   created   krci-gitops     main</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-feature-tt-123-02e67   success   created   test-go-app     feature-tt-123</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-feature-tt-456-045fe   success   created   test-go-app     feature-tt-456</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-main                   success   created   test-go-app     main</span><br></div></code></pre></div></div>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal branch list for test-go-app showing main, feature-tt-456 and feature-tt-123, all healthy" src="https://docs.kuberocketci.io/assets/images/branches-created-f97fbd679f6170be05afeb04d28e394e.png" title="Both feature branches created and healthy" width="1600" height="1000" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2-delete-a-branch-in-git---and-watch-it-get-marked">Step 2: Delete a Branch in Git - and Watch It Get Marked<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#step-2-delete-a-branch-in-git---and-watch-it-get-marked" class="hash-link" aria-label="Direct link to Step 2: Delete a Branch in Git - and Watch It Get Marked" title="Direct link to Step 2: Delete a Branch in Git - and Watch It Get Marked" translate="no">​</a></h3>
<p>Now the everyday event this feature exists for: the branch is merged and removed <em>in the git provider</em> - the "delete source branch" checkbox on a merge request, or an explicit API call. I delete <code>feature-tt-123</code> straight in GitLab:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ </span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-X</span><span class="token plain"> DELETE </span><span class="token parameter variable" style="color:#36acaa">-H</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"PRIVATE-TOKEN: </span><span class="token string variable" style="color:#36acaa">$PAT</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </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 string" style="color:#e3116c">"https://gitlab.127.0.0.1.nip.io/api/v4/projects/krci%2Ftest-go-app/repository/branches/feature-tt-123"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># HTTP 204</span><br></div></code></pre></div></div>
<p>Kubernetes knows nothing about this - until the next sweep. Within two minutes:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebasebranch</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                               RESULT    STATUS    CODEBASE NAME   BRANCH           STALE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci-gitops-main                   success   created   krci-gitops     main</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-feature-tt-123-02e67   success   created   test-go-app     feature-tt-123   True</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-feature-tt-456-045fe   success   created   test-go-app     feature-tt-456   False</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-main                   success   created   test-go-app     main</span><br></div></code></pre></div></div>
<p>The condition carries the full story:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><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 property" style="color:#36acaa">"type"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Stale"</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 property" style="color:#36acaa">"status"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"True"</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 property" style="color:#36acaa">"reason"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"BranchNotFoundInGit"</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 property" style="color:#36acaa">"message"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Branch was not found in the git repository"</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 property" style="color:#36acaa">"lastTransitionTime"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"2026-07-13T10:33:09Z"</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><br></div></code></pre></div></div>
<p>An event is emitted for the audit trail, and the mirrored label makes fleet-wide queries trivial:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get events --field-selector </span><span class="token assign-left variable" style="color:#36acaa">involvedObject.name</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">test-go-app-feature-tt-123-02e67</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">LAST SEEN   TYPE      REASON        MESSAGE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">86s         Warning   BranchStale   Branch feature-tt-123 was not found </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> the </span><span class="token function" style="color:#d73a49">git</span><span class="token plain"> repository and is marked as stale</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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebasebranch </span><span class="token parameter variable" style="color:#36acaa">-l</span><span class="token plain"> app.edp.epam.com/stale</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">true</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                               </span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">.   BRANCH           STALE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-feature-tt-123-02e67   </span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">.   feature-tt-123   True</span><br></div></code></pre></div></div>
<p>The Portal surfaces the same information as a warning badge in the branch list - hover it and the tooltip shows the condition message:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal branch list showing an orange Stale badge next to branches deleted from git, with a tooltip explaining the branch was not found in the git repository" src="https://docs.kuberocketci.io/assets/images/stale-branches-marked-ee689a7826949e8ba9b4a67d4a2c1471.png" title="Stale badges in the Portal branch list" width="1600" height="1000" class="img_ev3q"></p>
<p>At this point nothing has been deleted - the default cleanup strategy is <strong>mark</strong>: make staleness visible, let a human decide. For many teams that is already the win. But we can go further.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="auto-cleanup---with-a-deployment-aware-safety-net">Auto Cleanup - With a Deployment-Aware Safety Net<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#auto-cleanup---with-a-deployment-aware-safety-net" class="hash-link" aria-label="Direct link to Auto Cleanup - With a Deployment-Aware Safety Net" title="Direct link to Auto Cleanup - With a Deployment-Aware Safety Net" translate="no">​</a></h2>
<p>Before turning on automatic deletion, let's build the exact scenario that makes naive cleanup dangerous: a stale branch that is <em>still deployed</em>.</p>
<p>Following the <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">preview environment flow</a>, I build <code>feature-tt-456</code> and create a Deployment named <code>preview</code> with an Auto-triggered <code>dev</code> environment consuming that branch's image stream:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI deployment wizard with the test-go-app application selected and branch feature-tt-456 chosen as the deployment input" src="https://docs.kuberocketci.io/assets/images/deployment-select-app-branch-44826f8ffce42db6ae2ddd046f6e97a7.png" title="Selecting the feature branch as the deployment input" width="1600" height="1000" class="img_ev3q"></p>
<p>After the deploy pipeline finishes, the preview environment is live in its own namespace, running the image built from <code>feature-tt-456</code>:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI environment applications view showing test-go-app Healthy and Synced with deployed version feature-tt-456-20260713-103618" src="https://docs.kuberocketci.io/assets/images/preview-environment-deployed-8d0b126ab651f86d65a57850575db300.png" title="The preview environment running the feature branch image" width="1600" height="1000" class="img_ev3q"></p>
<p>Now I delete <code>feature-tt-456</code> in GitLab too. Next sweep: <strong>both</strong> branches are stale - but one of them is backing a running environment.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="guardrail-1-the-admission-webhook">Guardrail 1: The Admission Webhook<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#guardrail-1-the-admission-webhook" class="hash-link" aria-label="Direct link to Guardrail 1: The Admission Webhook" title="Direct link to Guardrail 1: The Admission Webhook" translate="no">​</a></h3>
<p>Even a well-meaning human with <code>kubectl</code> cannot break the deployment by "cleaning up" the stale branch. The codebase-operator's admission webhook now rejects deleting any <code>CodebaseBranch</code> that participates in a deployment (a rule the Portal used to enforce only client-side):</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci delete codebasebranch test-go-app-feature-tt-456-045fe</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Error from server </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">Forbidden</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">: admission webhook </span><span class="token string" style="color:#e3116c">"codebasebranch.epam.com"</span><span class="token plain"> denied the request:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">CodebaseBranch test-go-app-feature-tt-456-045fe cannot be deleted because it is used by</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">CDPipeline preview </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">inputDockerStreams</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> remove it from the deployment first</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="guardrail-2-retention-in-auto-cleanup">Guardrail 2: Retention in Auto Cleanup<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#guardrail-2-retention-in-auto-cleanup" class="hash-link" aria-label="Direct link to Guardrail 2: Retention in Auto Cleanup" title="Direct link to Guardrail 2: Retention in Auto Cleanup" translate="no">​</a></h3>
<p>Automatic cleanup is opt-in <strong>per codebase</strong>, via a single annotation:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci annotate codebase test-go-app app.edp.epam.com/branch-cleanup-strategy</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">auto</span><br></div></code></pre></div></div>
<p>With <code>auto</code>, the next sweep deletes stale branches that nothing references - and retains the ones that a <code>CDPipeline</code> (or a Stage autotest quality gate) still uses. One sweep later, the two stale branches got exactly opposite treatment:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get events --sort-by</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">.lastTimestamp </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> Stale</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Warning   BranchStale          Branch feature-tt-456 was not found </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> the </span><span class="token function" style="color:#d73a49">git</span><span class="token plain"> repository and is marked as stale</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Normal    StaleBranchDeleted   Branch feature-tt-123 was not found </span><span class="token keyword" style="color:#00009f">in</span><span class="token plain"> the </span><span class="token function" style="color:#d73a49">git</span><span class="token plain"> repository and is not used by any deployment, deleting</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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebasebranch</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                               RESULT    STATUS    CODEBASE NAME   BRANCH           STALE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci-gitops-main                   success   created   krci-gitops     main</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-feature-tt-456-045fe   success   created   test-go-app     feature-tt-456   True</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-main                   success   created   test-go-app     main</span><br></div></code></pre></div></div>
<p><code>feature-tt-123</code> is gone, and because the <code>CodebaseBranch</code> owns its <code>CodebaseImageStream</code>, the image stream was garbage-collected with it - no orphaned children. <code>feature-tt-456</code> stays, and its condition message names the exact resource that is keeping it alive:</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">Branch was not found in the git repository; retained because it is used by CDPipeline preview (inputDockerStreams)</span><br></div></code></pre></div></div>
<p>The Portal tooltip shows the same reason, so the "why is this still here?" question answers itself:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal stale badge tooltip stating the branch was not found in the git repository and is retained because it is used by CDPipeline preview" src="https://docs.kuberocketci.io/assets/images/stale-branch-retained-tooltip-c9c8dc9ae1519966d89c2b323db7aeef.png" title="A retained stale branch names the deployment that references it" width="1600" height="1000" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="releasing-the-hold">Releasing the Hold<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#releasing-the-hold" class="hash-link" aria-label="Direct link to Releasing the Hold" title="Direct link to Releasing the Hold" translate="no">​</a></h3>
<p>The retained branch is cleaned up the moment it stops being needed. I remove the preview Deployment in the Portal (<strong>Actions → Delete</strong>, type the name to confirm) - which, as covered in the <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">previous post</a>, tears down the environment namespace and the Argo CD Application:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI confirm deletion dialog for the preview deployment, requiring the deployment name to be typed" src="https://docs.kuberocketci.io/assets/images/delete-deployment-confirm-b01b8e0ec50b10a9ff2d51e0b54ddc0d.png" title="Deleting the preview deployment releases the stale branch" width="1600" height="1000" class="img_ev3q"></p>
<p>Next sweep, the last stale branch goes too:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get events --sort-by</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">.lastTimestamp </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> StaleBranchDeleted</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Normal   StaleBranchDeleted   Branch feature-tt-123 </span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">. not used by any deployment, deleting</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Normal   StaleBranchDeleted   Branch feature-tt-456 </span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">. not used by any deployment, deleting</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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebasebranch</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME               RESULT    STATUS    CODEBASE NAME   BRANCH   STALE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci-gitops-main   success   created   krci-gitops     main</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-main   success   created   test-go-app     main</span><br></div></code></pre></div></div>
<p>The platform is back to exactly what git says it should be - no manual bookkeeping, and at no point could the cleanup have taken down a running environment.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="configuration-reference">Configuration Reference<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#configuration-reference" class="hash-link" aria-label="Direct link to Configuration Reference" title="Direct link to Configuration Reference" translate="no">​</a></h2>
<table><thead><tr><th>Setting</th><th>Where</th><th>Default</th><th>Effect</th></tr></thead><tbody><tr><td><code>branchStaleCheckInterval</code></td><td>codebase-operator Helm values (env <code>BRANCH_STALE_CHECK_INTERVAL</code>)</td><td><code>24h</code></td><td>Sweep interval; <code>0</code> disables detection entirely</td></tr><tr><td><code>app.edp.epam.com/branch-cleanup-strategy</code></td><td>Annotation on a <code>Codebase</code></td><td><code>mark</code></td><td><code>mark</code> = only flag stale branches; <code>auto</code> = delete unreferenced stale branches</td></tr><tr><td><code>app.edp.epam.com/stale</code> label</td><td>Set by the operator on <code>CodebaseBranch</code></td><td>-</td><td>Mirrors the <code>Stale</code> condition for label-selector queries</td></tr></tbody></table>
<p>Events emitted by the checker:</p>
<table><thead><tr><th>Event reason</th><th>Type</th><th>Meaning</th></tr></thead><tbody><tr><td><code>BranchStale</code></td><td>Warning</td><td>Branch missing in git, marked as stale</td></tr><tr><td><code>BranchStaleResolved</code></td><td>Normal</td><td>Branch reappeared in git, mark removed</td></tr><tr><td><code>StaleBranchDeleted</code></td><td>Normal</td><td>Auto cleanup deleted an unreferenced stale branch</td></tr><tr><td><code>StaleBranchRetained</code></td><td>Warning</td><td>Stale branch kept because a deployment references it</td></tr></tbody></table>
<p>Handy one-liners:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># All stale branches across the platform namespace</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebasebranch </span><span class="token parameter variable" style="color:#36acaa">-l</span><span class="token plain"> app.edp.epam.com/stale</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">true</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"></span><span class="token comment" style="color:#999988;font-style:italic"># Why is this branch still around?</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebasebranch </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">name</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-o</span><span class="token plain"> </span><span class="token assign-left variable" style="color:#36acaa">jsonpath</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">'{.status.conditions[?(@.type=="Stale")].message}'</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="frequently-asked-questions">Frequently Asked Questions<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#frequently-asked-questions" class="hash-link" aria-label="Direct link to Frequently Asked Questions" title="Direct link to Frequently Asked Questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-marks-a-codebasebranch-as-stale-in-kuberocketci">What marks a CodebaseBranch as stale in KubeRocketCI?<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#what-marks-a-codebasebranch-as-stale-in-kuberocketci" class="hash-link" aria-label="Direct link to What marks a CodebaseBranch as stale in KubeRocketCI?" title="Direct link to What marks a CodebaseBranch as stale in KubeRocketCI?" translate="no">​</a></h3>
<p>A periodic checker in the codebase-operator lists the repository's branches with <code>git ls-remote</code> and compares them against the <code>CodebaseBranch</code> resources. If a branch no longer exists in the git repository, the resource gets a <code>Stale</code> status condition (reason <code>BranchNotFoundInGit</code>), the <code>app.edp.epam.com/stale</code> label, a Kubernetes event, and a warning badge in the Portal.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="can-the-default-branch-ever-be-marked-stale-or-deleted">Can the default branch ever be marked stale or deleted?<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#can-the-default-branch-ever-be-marked-stale-or-deleted" class="hash-link" aria-label="Direct link to Can the default branch ever be marked stale or deleted?" title="Direct link to Can the default branch ever be marked stale or deleted?" translate="no">​</a></h3>
<p>No. Default branches (for example <code>main</code>) are excluded from staleness checks entirely, as are branches that have not been pushed yet and branches already being deleted.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-happens-if-gitlab-or-github-is-down-during-a-sweep">What happens if GitLab or GitHub is down during a sweep?<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#what-happens-if-gitlab-or-github-is-down-during-a-sweep" class="hash-link" aria-label="Direct link to What happens if GitLab or GitHub is down during a sweep?" title="Direct link to What happens if GitLab or GitHub is down during a sweep?" translate="no">​</a></h3>
<p>Nothing. The checker never marks branches when the repository listing fails - connectivity problems, DNS errors, or rejected credentials cause the repository to be skipped for that sweep. Only a successful listing that positively lacks the branch produces a stale mark.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-stale-detection-delete-branches-automatically">Does stale detection delete branches automatically?<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#does-stale-detection-delete-branches-automatically" class="hash-link" aria-label="Direct link to Does stale detection delete branches automatically?" title="Direct link to Does stale detection delete branches automatically?" translate="no">​</a></h3>
<p>Not by default. Out of the box the strategy is <code>mark</code>: branches are flagged and left for a human. Automatic deletion is enabled per codebase with the <code>app.edp.epam.com/branch-cleanup-strategy: auto</code> annotation, and even then a stale branch referenced by a CDPipeline or a Stage quality gate is retained, with the retaining resource recorded in the condition message.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="can-a-stale-branch-that-is-still-deployed-be-deleted-by-mistake">Can a stale branch that is still deployed be deleted by mistake?<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#can-a-stale-branch-that-is-still-deployed-be-deleted-by-mistake" class="hash-link" aria-label="Direct link to Can a stale branch that is still deployed be deleted by mistake?" title="Direct link to Can a stale branch that is still deployed be deleted by mistake?" translate="no">​</a></h3>
<p>No - two independent guardrails prevent it. Auto cleanup skips any branch referenced by a deployment, and the codebase-operator's admission webhook rejects manual deletion (<code>kubectl delete</code>, API calls) of a <code>CodebaseBranch</code> that participates in a deployment, naming the CDPipeline in the error message.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-happens-to-the-images-built-from-a-deleted-branch">What happens to the images built from a deleted branch?<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#what-happens-to-the-images-built-from-a-deleted-branch" class="hash-link" aria-label="Direct link to What happens to the images built from a deleted branch?" title="Direct link to What happens to the images built from a deleted branch?" translate="no">​</a></h3>
<p>The <code>CodebaseBranch</code> owns its <code>CodebaseImageStream</code>, so when auto cleanup deletes the branch resource, Kubernetes garbage collection removes the image stream record with it. The container images themselves stay in your registry - registry retention is a separate concern.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-i-find-all-stale-branches-across-the-platform">How do I find all stale branches across the platform?<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#how-do-i-find-all-stale-branches-across-the-platform" class="hash-link" aria-label="Direct link to How do I find all stale branches across the platform?" title="Direct link to How do I find all stale branches across the platform?" translate="no">​</a></h3>
<p><code>kubectl get codebasebranch -l app.edp.epam.com/stale=true -A</code>. The label mirrors the status condition specifically so that plain label selectors - and any tooling built on them - can find stale branches without parsing conditions.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://docs.kuberocketci.io/blog/stale-branch-cleanup-kubernetes-kuberocketci#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<p>Feature branches are ephemeral; the Kubernetes resources behind them used to be forever. With KubeRocketCI 3.14, the codebase-operator closes the loop. One <code>git ls-remote</code> per repository per sweep detects branches deleted from git, and a <code>Stale</code> condition plus label plus Portal badge make them visible. An opt-in per-codebase <code>auto</code> strategy deletes the ones nothing depends on - while an admission webhook and a deployment-aware retention check make sure the cleanup can never take down a running environment.</p>
<p>In this run we created two branches, deleted them in GitLab at different points of their lifecycle, and watched the operator mark one, auto-delete it, retain the other while its <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">preview environment</a> was alive, and finish the job the moment the environment was gone - ending with a platform that matches git exactly.</p>
<p>Next steps from here:</p>
<ul>
<li class="">Spin up the same environment with <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">Try KubeRocketCI Locally</a> and replay the flow - the whole demo fits in a lunch break.</li>
<li class="">Read <a class="" href="https://docs.kuberocketci.io/docs/user-guide/manage-branches">Manage Branches</a> for the day-to-day branch workflow in the Portal.</li>
<li class="">See <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">Ephemeral Environments on Kubernetes</a> for the preview-environment lifecycle that produces these branches in the first place.</li>
<li class="">Explore the <a href="https://github.com/epam/edp-codebase-operator" target="_blank" rel="noopener noreferrer" class="">codebase-operator on GitHub</a> - the stale checker lives in <code>controllers/codebasebranch/stalecheck/</code>.</li>
</ul>
<p>KubeRocketCI is open source under Apache License 2.0. The platform, Helm charts, and the local testbed are all on <a href="https://github.com/KubeRocketCI/try-kuberocketci" target="_blank" rel="noopener noreferrer" class="">GitHub</a>.</p>
]]></content>
        <author>
            <name>Sergiy Kulanov</name>
            <uri>https://github.com/sergk</uri>
        </author>
        <category label="KubeRocketCI" term="KubeRocketCI"/>
        <category label="Kubernetes" term="Kubernetes"/>
        <category label="GitOps" term="GitOps"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="CI/CD" term="CI/CD"/>
        <category label="Branch Management" term="Branch Management"/>
        <category label="Operators" term="Operators"/>
        <category label="Open Source" term="Open Source"/>
        <category label="Tekton" term="Tekton"/>
        <category label="Argo CD" term="Argo CD"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Claude Code Telemetry: Token and Cost Metrics]]></title>
        <id>https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking</id>
        <link href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking"/>
        <updated>2026-07-12T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Claude Code emits OpenTelemetry metrics but ships no backend. Build a self-hosted Grafana stack that maps AI token spend to projects, Jira tickets, and teams.]]></summary>
        <content type="html"><![CDATA[<p>In the last 24 hours, one engineer's Claude Code sessions burned <strong>$166.97</strong>, <strong>833K input/output tokens</strong>, and <strong>959 API requests</strong> - and I can tell you exactly which Jira story got that money, which model spent it, which subagents did the heavy lifting, and that the prompt-cache hit ratio stayed at <strong>97.2%</strong> the whole time. Not from a vendor console: from a self-hosted Grafana stack fed by telemetry Claude Code already emits.</p>
<p>That last part is the point. Claude Code has native <a href="https://code.claude.com/docs/en/monitoring-usage" target="_blank" rel="noopener noreferrer" class="">OpenTelemetry support</a> - rich metrics and events for tokens, cost, sessions, tools, skills, and agents - but nothing to look at them with. As soon as AI-assisted development becomes a line item, every engineering leader asks the same questions: <em>what are we spending, on what, and is it worth it?</em> This post shows how we answer them at KubeRocketCI with a small OTel Collector → Prometheus + Loki → Grafana pipeline that runs identically on a laptop and on a team Kubernetes cluster - the <strong>claude-code-telemetry</strong> stack - without ever capturing a single prompt or line of your code.</p>
<p>Here's the path we'll take: why an "AI Factory" needs production-grade metering, what Claude Code emits out of the box, how to stand the stack up locally in minutes, how to attribute cost to projects and Jira tickets, how to deploy it for a whole team on Kubernetes (including CI/CD pipelines), the cardinality trap to avoid - and what a real day of data actually says.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-should-you-measure-claude-code-usage">Why Should You Measure Claude Code Usage?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#why-should-you-measure-claude-code-usage" class="hash-link" aria-label="Direct link to Why Should You Measure Claude Code Usage?" title="Direct link to Why Should You Measure Claude Code Usage?" translate="no">​</a></h2>
<p><strong>Because AI agents are production workers now, and production workers get metered.</strong> When developers and CI pipelines run Claude Code daily, token spend becomes real budget - and without attribution you can't answer which project, epic, or team consumed it, whether the spend produced code, or how adoption is trending.</p>
<p>Think of it as the observability layer of an <strong>AI Factory</strong>: the same discipline you apply to build minutes, cloud bills, and test flakiness, applied to AI-assisted engineering. Concretely, the questions that come up in every rollout:</p>
<ul>
<li class=""><strong>FinOps:</strong> cost by project, epic, story, user, and model - who spends, on what work.</li>
<li class=""><strong>Efficiency:</strong> cost per commit, cost per 1K lines changed, cache hit ratio - is the spend producing output.</li>
<li class=""><strong>Adoption:</strong> daily/weekly active users, sessions, active hours - is the investment being used.</li>
<li class=""><strong>Governance:</strong> which plugins, skills, and MCP servers run where, in which versions - what your fleet actually executes.</li>
</ul>
<p>And one hard constraint shapes everything: this must work <strong>without capturing prompts, responses, or file contents</strong>. Usage analytics is not surveillance - only aggregate counters and structured metadata should ever cross the wire.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-telemetry-does-claude-code-emit-out-of-the-box">What Telemetry Does Claude Code Emit Out of the Box?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#what-telemetry-does-claude-code-emit-out-of-the-box" class="hash-link" aria-label="Direct link to What Telemetry Does Claude Code Emit Out of the Box?" title="Direct link to What Telemetry Does Claude Code Emit Out of the Box?" translate="no">​</a></h2>
<p><strong>Claude Code natively exports OTLP metrics and events once you set a few environment variables</strong> - no wrappers, no scraping, no plugins. The metric names are fixed by the CLI, but every one carries dimensions you can slice by, and you can add your own.</p>
<p>The metrics that matter most for tokenomics:</p>
<table><thead><tr><th>Metric</th><th>What it answers</th></tr></thead><tbody><tr><td><code>claude_code.token.usage</code></td><td>tokens by <code>type</code> (input/output/cacheRead/cacheCreation), <code>model</code>, <code>agent.name</code>, <code>skill.name</code></td></tr><tr><td><code>claude_code.cost.usage</code></td><td>USD by the same dimensions, plus plugin and marketplace</td></tr><tr><td><code>claude_code.session.count</code> / <code>active_time.total</code></td><td>adoption and engagement</td></tr><tr><td><code>claude_code.lines_of_code.count</code>, <code>commit.count</code>, <code>pull_request.count</code></td><td>output to divide cost by</td></tr></tbody></table>
<p>Alongside metrics, Claude Code emits <strong>events</strong> - <code>api_request</code> (per-call cost and token detail), <code>skill_activated</code>, <code>tool_result</code>, <code>tool_decision</code> - which carry the high-cardinality forensics: per-session, per-prompt, per-tool detail.</p>
<p>Two facts drive the whole design. First, <strong>you cannot invent new metric names</strong>, but you <em>can</em> inject new <strong>dimensions</strong> and derive anything downstream with PromQL or LogQL. Second, content capture is opt-in and off by default in our setup: prompts, assistant responses, file contents, and raw API bodies stay dark - we enable only <code>OTEL_LOG_TOOL_DETAILS</code> so skill, agent, and tool <em>names</em> are visible.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="which-claude-products-support-opentelemetry">Which Claude Products Support OpenTelemetry?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#which-claude-products-support-opentelemetry" class="hash-link" aria-label="Direct link to Which Claude Products Support OpenTelemetry?" title="Direct link to Which Claude Products Support OpenTelemetry?" translate="no">​</a></h2>
<p><strong>Only Claude Code emits OTel today - the CLI, its IDE extensions, and the Agent SDK all share the same environment-variable configuration.</strong> Claude Desktop's chat has no telemetry export (only its Cowork feature offers an admin-configured OTLP endpoint on Team/Enterprise plans), Claude Code on the web currently emits nothing, and raw API usage is served by Anthropic's pull-based Usage and Cost APIs instead.</p>
<table><thead><tr><th>Product</th><th>OTel export</th><th>Mechanism</th></tr></thead><tbody><tr><td>Claude Code CLI</td><td>Yes</td><td><code>CLAUDE_CODE_ENABLE_TELEMETRY</code> + <code>OTEL_*</code> env vars</td></tr><tr><td>VS Code / JetBrains extensions</td><td>Yes</td><td>inherit the CLI's <code>settings.json</code></td></tr><tr><td>Agent SDK</td><td>Yes</td><td>same pipeline, runs as a child process</td></tr><tr><td>Claude Desktop (chat)</td><td>No</td><td>Cowork feature only, via admin portal</td></tr><tr><td>Claude Code on the web</td><td>Not yet</td><td>managed sandbox exposes no <code>OTEL_*</code> config</td></tr><tr><td>Claude API</td><td>No</td><td>pull-based <a href="https://code.claude.com/docs/en/monitoring-usage" target="_blank" rel="noopener noreferrer" class="">Usage &amp; Cost / Analytics APIs</a></td></tr></tbody></table>
<p>Two practical consequences for a rollout. First, the stack described here captures <strong>CLI, IDE-extension, and Agent SDK sessions</strong> - if part of your team works in Claude Code on the web, that spend is invisible to OTel until Anthropic closes the gap, so budget reconciliation still needs the Usage and Cost API as the financial source of truth. Second, if you later enable Cowork telemetry, note its content is <strong>not redacted by default</strong> - unlike Claude Code - so apply filtering in the collector before it shares a backend with this pipeline.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-you-build-a-self-hosted-telemetry-stack">How Do You Build a Self-Hosted Telemetry Stack?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#how-do-you-build-a-self-hosted-telemetry-stack" class="hash-link" aria-label="Direct link to How Do You Build a Self-Hosted Telemetry Stack?" title="Direct link to How Do You Build a Self-Hosted Telemetry Stack?" translate="no">​</a></h2>
<p><strong>With one OTel Collector fanning out to Prometheus for metrics and Loki for events, visualized in Grafana</strong> - the standard trio, packaged twice from the same config so a laptop and a Kubernetes cluster behave identically. That's the entire <a href="https://github.com/KubeRocketCI/claude-code-telemetry" target="_blank" rel="noopener noreferrer" class="">claude-code-telemetry</a> architecture:</p>
<!-- -->
<p>The Collector is the heart: it receives OTLP (gRPC/HTTP), stamps every datapoint with a <code>collector.env</code> attribute so laptop and cluster traffic never get confused, converts resource attributes into Prometheus labels (<code>resource_to_telemetry_conversion</code>), and ships full-fidelity events to Loki. On top sit six pre-built dashboards, organized into three Grafana folders by audience:</p>
<p><img decoding="async" loading="lazy" alt="Grafana dashboard folders for Claude Code telemetry: usage, governance, and operational scopes" src="https://docs.kuberocketci.io/assets/images/grafana-dashboards-overview-3ea9d73d40976e886dec9eb3286cd95f.png" width="2000" height="706" class="img_ev3q"></p>
<ul>
<li class=""><strong>Claude Code</strong> - Usage Audit, Efficiency &amp; ROI, Operations &amp; Adoption, Session Explorer: the daily-driver views.</li>
<li class=""><strong>Governance</strong> - Governance &amp; Security: plugins, skills, permission decisions across the fleet.</li>
<li class=""><strong>Operational</strong> - Collector Health: is the pipeline itself alive and ingesting.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-you-enable-it-for-local-development">How Do You Enable It for Local Development?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#how-do-you-enable-it-for-local-development" class="hash-link" aria-label="Direct link to How Do You Enable It for Local Development?" title="Direct link to How Do You Enable It for Local Development?" translate="no">​</a></h2>
<p><strong>Start the docker-compose testbed, merge one <code>env</code> block into your Claude Code settings, and data flows within a minute.</strong> The local stack is the source of truth we iterate on - same collector pipeline, same dashboards as the cluster deployment:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">git</span><span class="token plain"> clone https://github.com/KubeRocketCI/claude-code-telemetry.git</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> claude-code-telemetry/local</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">docker</span><span class="token plain"> compose up </span><span class="token parameter variable" style="color:#36acaa">-d</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># Grafana :3000 · Prometheus :9090 · Loki :3100 · OTLP :4317</span><br></div></code></pre></div></div>
<p>Then merge <code>local/claude-settings.snippet.json</code> into <code>~/.claude/settings.json</code> - the essentials:</p>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><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 property" style="color:#36acaa">"env"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </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 property" style="color:#36acaa">"CLAUDE_CODE_ENABLE_TELEMETRY"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"1"</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 property" style="color:#36acaa">"OTEL_METRICS_EXPORTER"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"otlp"</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 property" style="color:#36acaa">"OTEL_LOGS_EXPORTER"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"otlp"</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 property" style="color:#36acaa">"OTEL_EXPORTER_OTLP_PROTOCOL"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"grpc"</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 property" style="color:#36acaa">"OTEL_EXPORTER_OTLP_ENDPOINT"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"http://localhost:4317"</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 property" style="color:#36acaa">"OTEL_LOG_TOOL_DETAILS"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"1"</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 property" style="color:#36acaa">"OTEL_LOG_USER_PROMPTS"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"0"</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 property" style="color:#36acaa">"OTEL_LOG_ASSISTANT_RESPONSES"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"0"</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 property" style="color:#36acaa">"OTEL_LOG_TOOL_CONTENT"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"0"</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><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>Restart Claude Code, open Grafana, and the Usage Audit dashboard starts filling with your own traffic. This loop - real usage in, dashboard iteration out - is how every panel in this post was built.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-you-attribute-cost-to-projects-and-jira-tickets">How Do You Attribute Cost to Projects and Jira Tickets?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#how-do-you-attribute-cost-to-projects-and-jira-tickets" class="hash-link" aria-label="Direct link to How Do You Attribute Cost to Projects and Jira Tickets?" title="Direct link to How Do You Attribute Cost to Projects and Jira Tickets?" translate="no">​</a></h2>
<p><strong>By injecting business dimensions through <code>OTEL_RESOURCE_ATTRIBUTES</code> before each session starts.</strong> Claude Code reads the variable once at startup and stamps every metric and event with it - so the discipline is simple: <em>one Jira ticket ≈ one session</em>.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token builtin class-name">export</span><span class="token plain"> </span><span class="token assign-left variable" style="color:#36acaa">OTEL_RESOURCE_ATTRIBUTES</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">"project=krci-portal,jira.epic=EPMDEDP-15000,jira.story=EPMDEDP-17147"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">claude</span><br></div></code></pre></div></div>
<p>Three custom keys are enough to start: <code>project</code> (the git repository), <code>jira.epic</code>, and <code>jira.story</code>. User identity comes free - Claude Code natively attaches <code>user.email</code>, <code>user.id</code>, and <code>organization.id</code> to everything, so per-person breakdowns need no injection at all.</p>
<p>The Session Explorer dashboard shows what this buys you. Every session becomes a row with its epic, story, project, user, cost, and event count - sortable, filterable, drillable:</p>
<p><img decoding="async" loading="lazy" alt="Claude Code Session Explorer dashboard showing per-session cost attributed to Jira epics and stories" src="https://docs.kuberocketci.io/assets/images/session-explorer-dashboard-e89d346b2e13f647d3fe6b29b11969c4.png" width="2000" height="1255" class="img_ev3q"></p>
<p>Reading my own last 24 hours: <strong>$166.97 total</strong>, and the table names the culprits - two long <code>krci-workspace</code> sessions on story EPMDEDP-17188 at <strong>$67.68</strong> and <strong>$60.66</strong>, a <code>edp-keycloak-operator</code> triage on EPMDEDP-16043 at <strong>$24.93</strong>, and the session that built the telemetry stack itself at <strong>$1.99</strong>. When a manager asks "what did that epic cost in AI assistance?", this table <em>is</em> the answer - down to the session.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-you-deploy-it-for-a-team-on-kubernetes">How Do You Deploy It for a Team on Kubernetes?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#how-do-you-deploy-it-for-a-team-on-kubernetes" class="hash-link" aria-label="Direct link to How Do You Deploy It for a Team on Kubernetes?" title="Direct link to How Do You Deploy It for a Team on Kubernetes?" translate="no">​</a></h2>
<p><strong>With the repo's self-contained Helm bundle - one <code>helm install</code>, no operators, no CRDs, no cluster-monitoring prerequisites.</strong> Local PoC is for one laptop; team consumption means everyone's Claude Code - and your CI runners - pushing OTLP to a single in-cluster endpoint with shared dashboards:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">helm dependency update deploy-templates</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">helm </span><span class="token function" style="color:#d73a49">install</span><span class="token plain"> claude-code-telemetry deploy-templates </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 parameter variable" style="color:#36acaa">-n</span><span class="token plain"> claude-code-telemetry --create-namespace</span><br></div></code></pre></div></div>
<p>The bundle wraps the upstream OTel Collector, Prometheus, Loki, and Grafana charts, and it is <strong>deliberately isolated</strong>: its Prometheus scrapes exactly two static targets (the collector's data and self-telemetry ports), all cluster-wide discovery is disabled, no node-exporter or kube-state-metrics tag along, and RBAC is namespace-scoped. It won't fight the observability stack you already run - and if you have one, each component toggles off (<code>grafana.enabled: false</code>, <code>prometheus.enabled: false</code>, …) so you can wire just the collector into existing storage.</p>
<p>Three things change at team scale compared to the laptop:</p>
<ol>
<li class=""><strong>Distribution</strong> - ship the <code>env</code> block through a managed <code>settings.json</code> (MDM), so telemetry is on for everyone, pointed at the team endpoint, and can't be silently unset.</li>
<li class=""><strong>Authentication</strong> - expose the OTLP endpoint behind auth (<code>OTEL_EXPORTER_OTLP_HEADERS</code> with a bearer token, or Claude Code's <code>otelHeadersHelper</code> for short-lived tokens).</li>
<li class=""><strong>Enforcement</strong> - the Collector becomes the governance point users cannot bypass: inject defaults for missing attributes, validate <code>jira.*</code> values against <code>^[A-Z]+-[0-9]+$</code>, and drop or route telemetry that arrives without a <code>project</code>.</li>
</ol>
<p>Every datapoint's <code>collector.env</code> label (<code>local-poc</code> vs <code>k8s</code>) keeps sources distinguishable, so laptops and the cluster can even feed shared dashboards during a migration without mixing data.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-you-collect-metrics-from-cicd-pipelines">How Do You Collect Metrics from CI/CD Pipelines?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#how-do-you-collect-metrics-from-cicd-pipelines" class="hash-link" aria-label="Direct link to How Do You Collect Metrics from CI/CD Pipelines?" title="Direct link to How Do You Collect Metrics from CI/CD Pipelines?" translate="no">​</a></h2>
<p><strong>Exactly the same way - headless Claude Code in a pipeline step is still Claude Code, and the same OTLP env vars apply.</strong> This is where the AI Factory framing stops being a metaphor: automated agents doing code review, test generation, or triage inside <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">Tekton pipelines</a> spend tokens too, and they should land in the same ledger as human sessions.</p>
<p>In a pipeline task, set the same telemetry variables plus attribution that identifies the machine work:</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">env</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">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> CLAUDE_CODE_ENABLE_TELEMETRY</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"1"</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">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> OTEL_METRICS_EXPORTER</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"otlp"</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">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> OTEL_LOGS_EXPORTER</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"otlp"</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">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> OTEL_EXPORTER_OTLP_ENDPOINT</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"http://otel-collector.claude-code-telemetry:4317"</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">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> OTEL_RESOURCE_ATTRIBUTES</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"project=$(params.codebase),jira.epic=none,jira.story=$(params.ticket)"</span><br></div></code></pre></div></div>
<p>The pipeline populates <code>project</code> from the codebase it builds and the ticket from the change it processes - attribution comes from parameters you already have. One caveat worth knowing: Claude Code does <strong>not</strong> propagate <code>OTEL_*</code> variables to subprocesses, so only the CLI process itself is instrumented - which is exactly what you want in a build pod. The result: a single Grafana view where "cost of AI per merge request" and "cost of AI per developer day" sit side by side.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-the-cardinality-trap">What Is the Cardinality Trap?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#what-is-the-cardinality-trap" class="hash-link" aria-label="Direct link to What Is the Cardinality Trap?" title="Direct link to What Is the Cardinality Trap?" translate="no">​</a></h2>
<p><strong>Every unique label combination in Prometheus is a stored time series - so unbounded labels like Jira story IDs will eventually blow up your metrics store.</strong> This is the telemetry twin of the lesson from our <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what">Kubernetes audit trail post</a>: collect with a goal, not by default.</p>
<p>The rule we apply, enforced in the Collector:</p>
<ul>
<li class=""><strong>Bounded dimensions</strong> (<code>model</code>, <code>type</code>, <code>agent.name</code>, <code>skill.name</code>, <code>project</code>, <code>jira.epic</code>) → safe as <strong>metric labels</strong>, fast Grafana breakdowns.</li>
<li class=""><strong>Unbounded dimensions</strong> (<code>jira.story</code>, <code>session.id</code>, <code>prompt.id</code>) → <strong>events in Loki only</strong>; derive per-story cost with LogQL over <code>api_request</code> events.</li>
</ul>
<p>A single-user PoC can keep <code>jira.story</code> on metrics for convenience - ours does. At team scale, the Collector strips it from the metrics pipeline while Loki keeps the full detail. Same data, right store: Prometheus answers "how much, by which bounded dimension" in milliseconds; Loki answers "what exactly happened in that session" when you drill in.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-do-the-numbers-actually-say">What Do the Numbers Actually Say?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#what-do-the-numbers-actually-say" class="hash-link" aria-label="Direct link to What Do the Numbers Actually Say?" title="Direct link to What Do the Numbers Actually Say?" translate="no">​</a></h2>
<p><strong>That efficiency metrics are the fastest way to turn raw spend into decisions - including uncomfortable ones.</strong> The Efficiency &amp; ROI dashboard divides cost by output, and a real day of data is more instructive than any synthetic demo:</p>
<p><img decoding="async" loading="lazy" alt="Claude Code Efficiency and ROI Grafana dashboard with cache hit ratio, cost per commit, and cost per active hour" src="https://docs.kuberocketci.io/assets/images/efficiency-roi-dashboard-fcdc8658a4d2d81f710fe02cc431674f.png" width="2000" height="1247" class="img_ev3q"></p>
<ul>
<li class=""><strong>Cache hit ratio: 97.2%.</strong> Prompt caching is doing enormous work - at roughly a tenth of the input price, those cache reads are the difference between $167 and a number several times larger. If this ratio drops, something in your workflow broke; it's the first efficiency metric worth alerting on.</li>
<li class=""><strong>Cost per active hour: $31.10.</strong> A defensible, explainable unit price for AI pairing - the number to put next to an engineer's loaded hourly cost when someone asks about ROI.</li>
<li class=""><strong>Cost per 1K lines changed: $79.28.</strong> Noisy on any single day, meaningful as a trend across weeks and projects.</li>
<li class=""><strong>Cost per commit: $∞.</strong> Total cost divided by <code>claude_code.commit.count</code>, which counts only commits made by Claude Code itself. We review and commit by hand, so the counter stays at zero and the panel shows infinity. If you commit through the agent, this becomes a real spend-per-delivered-commit number.</li>
</ul>
<p>The Usage Audit dashboard tells you <em>where</em> the tokens went:</p>
<p><img decoding="async" loading="lazy" alt="Claude Code Usage Audit dashboard showing tokens by model, cost by project and epic, and subagent token usage" src="https://docs.kuberocketci.io/assets/images/usage-audit-dashboard-4e8ebd73302d7f7c8a04a1fc6caa3b0f.png" width="2000" height="1137" class="img_ev3q"></p>
<p>Over the two-day range: cost concentrates in <strong>krci-workspace / EPMDEDP-16757 ($97.6)</strong> and <strong>edp-keycloak-operator / EPMDEDP-16043 ($80.7)</strong>, with the telemetry stack's own development a modest <strong>$10.4</strong>. Token type over time is dominated by <code>cacheRead</code> - visual confirmation of that 97.2%. And the subagent panel is the sleeper hit: <strong>custom agents consumed 18.2M tokens</strong> against 4.08M for built-in Explore and 3.13M for general-purpose - the multi-agent workflows you build are where the budget actually goes. Tool usage (2,059 Bash invocations vs 475 Reads and 288 Edits in the window) rounds out the picture of what the agent fleet physically does all day.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-you-govern-plugins-and-skills-across-a-team">How Do You Govern Plugins and Skills Across a Team?<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#how-do-you-govern-plugins-and-skills-across-a-team" class="hash-link" aria-label="Direct link to How Do You Govern Plugins and Skills Across a Team?" title="Direct link to How Do You Govern Plugins and Skills Across a Team?" translate="no">​</a></h2>
<p><strong>The same event stream doubles as a software inventory - who runs which plugin, in which version, triggered how.</strong> The Governance &amp; Security dashboard breaks down skill activations by trigger, which quietly answers an adoption question every platform team has:</p>
<p><img decoding="async" loading="lazy" alt="Grafana panel showing Claude Code skill activations by source and plugin skill usage by trigger" src="https://docs.kuberocketci.io/assets/images/skill-plugin-usage-74e73a745fe7e911fe9c24422d1a8403.png" width="2000" height="430" class="img_ev3q"></p>
<p>In our data, most activations are <code>claude-proactive</code> or <code>nested-skill</code> rather than <code>user-slash</code> - the agent invokes the team's KubeRocketCI plugins on its own more often than humans type slash commands. That's exactly what you want from packaged expertise, and now it's measurable rather than anecdotal.</p>
<p>The plugin inventory closes the loop for fleet management:</p>
<p><img decoding="async" loading="lazy" alt="Claude Code plugin inventory panel listing installed plugins, versions, and scope per user" src="https://docs.kuberocketci.io/assets/images/plugin-inventory-governance-579c9d6834ed6811777677e51ca5c659.png" width="2000" height="532" class="img_ev3q"></p>
<p>Every user session reports its loaded plugins with marketplace, version, and install scope. When you ship a fixed <code>krci-godev</code> and need to know who's still loading 0.6.0 - the dashboard already knows.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-steps">Next Steps<a href="https://docs.kuberocketci.io/blog/claude-code-telemetry-opentelemetry-token-cost-tracking#next-steps" class="hash-link" aria-label="Direct link to Next Steps" title="Direct link to Next Steps" translate="no">​</a></h2>
<ul>
<li class=""><strong>Try it on your laptop:</strong> clone <a href="https://github.com/KubeRocketCI/claude-code-telemetry" target="_blank" rel="noopener noreferrer" class="">claude-code-telemetry</a>, run <code>docker compose up -d</code> in <code>local/</code>, merge the settings snippet, and watch your own session appear.</li>
<li class=""><strong>Deploy for your team:</strong> install the Helm bundle from <code>deploy-templates/</code> and distribute the <code>env</code> block via managed settings.</li>
<li class=""><strong>Go deeper on the design:</strong> the repo's <code>docs/analytics.md</code> covers the attribution model, cardinality guidance, and rollout plan; <code>spec/</code> catalogs the full Claude Code OTEL vocabulary.</li>
<li class=""><strong>See the platform context:</strong> Claude Code is one worker in the factory - <a class="" href="https://docs.kuberocketci.io/docs/about-platform">KubeRocketCI</a> provides the CI/CD floor it works on, from <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">Tekton pipelines</a> to <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what">audit trails</a>. New here? <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">Try it locally</a>.</li>
</ul>
<p>Token spend is the new build minutes: invisible until the invoice arrives, obvious once it's on a dashboard. Claude Code already emits everything you need - stand up the backend, inject three attributes, and "what does AI development cost us?" becomes a Grafana query instead of a guess.</p>]]></content>
        <author>
            <name>Sergiy Kulanov</name>
            <uri>https://github.com/sergk</uri>
        </author>
        <category label="KubeRocketCI" term="KubeRocketCI"/>
        <category label="Claude Code" term="Claude Code"/>
        <category label="OpenTelemetry" term="OpenTelemetry"/>
        <category label="Observability" term="Observability"/>
        <category label="AI" term="AI"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="Grafana" term="Grafana"/>
        <category label="FinOps" term="FinOps"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[A Portable Kubernetes Audit Trail: Who Changed What]]></title>
        <id>https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what</id>
        <link href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what"/>
        <updated>2026-07-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Native EKS audit logs are great for SIEM but slow for quick answers. See how krci-audit gives KubeRocketCI a portable, queryable Kubernetes audit trail - and why you should scope it to specific events.]]></summary>
        <content type="html"><![CDATA[<p>You trigger pipelines. You update a <code>GitServer</code>, promote a deployment, adjust a <code>Codebase</code>. Much of that flows through GitOps and lands in your Git history - but not all of it. Some changes you make directly against the cluster, from the portal or <code>kubectl</code>, because that's the quickest way to get something done. That's normal, everyday platform work. What's harder is answering a fair question that comes up afterward: <em>what changed, when, and who made the change?</em> - especially for the changes that never went through a pull request.</p>
<p>On managed Kubernetes the raw answer exists - the API server writes an audit record for every request - but getting to it is another story. On Amazon EKS those records stream to CloudWatch. Answering a simple operational question then means writing a Logs Insights query, knowing the retention window, and having the right IAM. Great for a SIEM pipeline; heavy for a Tuesday-afternoon "what just changed?". KubeRocketCI ships a lighter, portable alternative called <strong>krci-audit</strong> - a self-contained, queryable audit trail that works the same on EKS, on-prem, or a local kind cluster. The catch, which most of this post is about: an audit trail is not free, so you should be deliberate about what you capture.</p>
<p>Here's the reasoning - why native audit logs leave a gap for day-to-day questions, how krci-audit fills it without a cloud dependency, the performance trap of auditing everything (with real numbers from a running cluster), and where this is going next.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-arent-native-kubernetes-audit-logs-enough-for-who-changed-what">Why Aren't Native Kubernetes Audit Logs Enough for "Who Changed What?"<a href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what#why-arent-native-kubernetes-audit-logs-enough-for-who-changed-what" class="hash-link" aria-label="Direct link to Why Aren't Native Kubernetes Audit Logs Enough for &quot;Who Changed What?&quot;" title="Direct link to Why Aren't Native Kubernetes Audit Logs Enough for &quot;Who Changed What?&quot;" translate="no">​</a></h2>
<p><strong>Short answer: they're built for security forensics and SIEM, not for a quick operational lookup.</strong> The data exists - every managed Kubernetes distribution can emit <a href="https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/" target="_blank" rel="noopener noreferrer" class="">API server audit logs</a>, a structured record of who called the API, what they changed, and when. On EKS you enable the <code>audit</code> log type and the control plane ships it to CloudWatch Logs. As a foundation that's exactly right: immutable, comprehensive, centrally retained, and easy to alert on.</p>
<p>But for <em>operational</em> questions it has friction:</p>
<ul>
<li class=""><strong>Quick access is a query, not a lookup.</strong> "Who created this PipelineRun?" becomes a CloudWatch Logs Insights query against a large, verbose stream - filtered by <code>objectRef</code>, <code>verb</code>, and time - not a one-line answer.</li>
<li class=""><strong>It's cloud-locked.</strong> The retrieval path on EKS (CloudWatch), on AKS (Azure Monitor), and on-prem (a file sink you wire up yourself) are all different. Your runbook doesn't port.</li>
<li class=""><strong>It's undifferentiated.</strong> The native stream captures <em>everything</em> the audit policy allows - most of which is control-plane noise you'll never ask about (more on that below).</li>
<li class=""><strong>It lives outside your platform.</strong> Developers using the KubeRocketCI portal can't see "who triggered this run" without leaving the tool and querying a cloud console they may not have access to.</li>
</ul>
<p>None of this makes native audit logging wrong - it makes it the <em>wrong tool for the quick, in-platform question</em>. That's the gap krci-audit targets.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-does-krci-audit-give-you-a-portable-audit-trail">How Does krci-audit Give You a Portable Audit Trail?<a href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what#how-does-krci-audit-give-you-a-portable-audit-trail" class="hash-link" aria-label="Direct link to How Does krci-audit Give You a Portable Audit Trail?" title="Direct link to How Does krci-audit Give You a Portable Audit Trail?" translate="no">​</a></h2>
<p><strong>It captures a scoped set of admission changes inside your cluster and makes them instantly queryable</strong> - no dependency on any cloud provider's logging service. krci-audit is a small, self-contained component that does exactly that.</p>
<!-- -->
<p>A resource change hits a validating admission webhook, which ships the record through Vector into an append-only PostgreSQL table; a read-only API then serves it to the portal and other consumers. The pieces:</p>
<table><thead><tr><th>Component</th><th>Role</th></tr></thead><tbody><tr><td><strong>Capture</strong></td><td>A <code>ValidatingWebhookConfiguration</code> (backed by <a href="https://github.com/RichardoC/kube-audit-rest" target="_blank" rel="noopener noreferrer" class="">kube-audit-rest</a>) receives the <code>AdmissionReview</code> for each matching change. It runs with <code>failurePolicy: Ignore</code> and a 1-second timeout, so it <strong>never blocks a platform operation</strong>.</td></tr><tr><td><strong>Ship</strong></td><td>A <a href="https://vector.dev/" target="_blank" rel="noopener noreferrer" class="">Vector</a> sidecar tails the capture log, keeps only the events you asked for, and writes them to PostgreSQL.</td></tr><tr><td><strong>Store</strong></td><td>A dedicated PostgreSQL database holds events in a monthly-partitioned, <strong>append-only</strong> table. Writes use a least-privilege role that cannot update or delete existing rows.</td></tr><tr><td><strong>Read API</strong></td><td>A separate, read-only service exposes initiator lookup and a filterable events query. It connects as a SELECT-only role, so the read path can never alter the trail.</td></tr></tbody></table>
<p>Because the whole thing runs <em>inside</em> your cluster, the experience is identical on EKS, AKS, GKE, OpenShift, or a laptop kind cluster - the same install, the same API, the same portal feature. It's an <a class="" href="https://docs.kuberocketci.io/docs/next/operator-guide/monitoring-and-observability/audit-trails-setup">add-on</a>, off by default, that you turn on when you have a reason to.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-shouldnt-you-audit-everything">Why Shouldn't You Audit Everything?<a href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what#why-shouldnt-you-audit-everything" class="hash-link" aria-label="Direct link to Why Shouldn't You Audit Everything?" title="Direct link to Why Shouldn't You Audit Everything?" translate="no">​</a></h2>
<p><strong>Because an admission webhook sits in the write path of the API server</strong> - so a catch-all policy taxes every write <em>and</em> buries the trail in noise. Every request matching the webhook's rules is paused, sent to the webhook, and awaited (within the timeout) before the write completes. Point it at <code>*</code> - all resources - and you pay that cost on everything.</p>
<p>How much noise? To put real numbers on it, I measured the <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">local <strong>try-kuberocketci</strong> testbed</a> - a single-node <a href="https://kind.sigs.k8s.io/" target="_blank" rel="noopener noreferrer" class="">kind</a> cluster (Kubernetes v1.36.1) running the full platform and otherwise idle. Comparing each <code>Lease</code> object's <code>resourceVersion</code> over a 30-second window shows the churn a catch-all audit policy would capture (reproduce it with <code>kubectl get leases -A</code>, twice, 30 seconds apart):</p>
<ul>
<li class=""><strong>31 <code>Lease</code> objects, and all 31 changed within those 30 seconds</strong> - pure leader-election heartbeat traffic. Extrapolated: <strong>~3,720 <code>Lease</code> UPDATE admissions per hour</strong>, none of which anyone will ever audit.</li>
<li class="">And that's before <code>Events</code> (13), <code>EndpointSlices</code> (50), <code>Pods</code> (54), and <code>ConfigMaps</code> (122) - all high-churn, all captured by a <code>*</code> policy.</li>
</ul>
<p>Now compare that to the <strong>scoped</strong> trail the same testbed actually recorded over ~36 hours, using krci-audit's default configuration (shown below):</p>
<table><thead><tr><th>Resource</th><th>Events</th></tr></thead><tbody><tr><td>PipelineRuns</td><td>97 (71 UPDATE, 18 DELETE, 8 CREATE)</td></tr><tr><td>CodebaseBranches</td><td>12</td></tr><tr><td>GitServers</td><td>4</td></tr><tr><td>CodebaseImageStreams</td><td>2</td></tr><tr><td>CDStageDeployments / Stages</td><td>3</td></tr></tbody></table>
<p>That's <strong>118 meaningful events over a day and a half</strong> - roughly <strong>80 a day</strong> - versus <strong>~3,720 an hour</strong> of lease noise from a single un-scoped resource type. A catch-all policy would be on the order of a thousand times larger, almost entirely with records no one wants.</p>
<p><strong>The design that avoids this:</strong> scope at the webhook <em>rules</em>, not downstream. When a resource isn't in the rules, the API server <strong>never calls the webhook at all</strong> - zero added latency, zero stored noise. krci-audit's default rules capture only what the platform cares about:</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">capture</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">failurePolicy</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> Ignore   </span><span class="token comment" style="color:#999988;font-style:italic"># never block a platform operation</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">timeoutSeconds</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">1</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">rules</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">apiGroups</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"v2.edp.epam.com"</span><span class="token punctuation" style="color:#393A34">]</span><span class="token plain">   </span><span class="token comment" style="color:#999988;font-style:italic"># KubeRocketCI CRDs</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">resources</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"*"</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">apiGroups</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"tekton.dev"</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">resources</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token string" style="color:#e3116c">"pipelineruns"</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">level</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> metadata          </span><span class="token comment" style="color:#999988;font-style:italic"># capture object metadata, not full bodies (bounds size and PII)</span><br></div></code></pre></div></div>
<p>The guidance is simple: <strong>audit with a goal, not by default.</strong> Decide what question you need the trail to answer - "who triggered pipelines", "who touched Git server config", "who deleted deployments" - and capture <em>those</em> resources. You can widen the filter later with a <code>helm upgrade</code>; you can't un-spend the latency budget you burned auditing lease renewals.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-you-see-who-triggered-a-pipeline">How Do You See Who Triggered a Pipeline?<a href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what#how-do-you-see-who-triggered-a-pipeline" class="hash-link" aria-label="Direct link to How Do You See Who Triggered a Pipeline?" title="Direct link to How Do You See Who Triggered a Pipeline?" translate="no">​</a></h2>
<p><strong>The KubeRocketCI portal shows a "Triggered By" field on the PipelineRun details page</strong> - the first thing krci-audit powers, answering a question every platform user eventually asks: <em>who started this run?</em></p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI PipelineRun details page showing the Triggered By field resolved to a service account" src="https://docs.kuberocketci.io/assets/images/krci-audit-triggered-by-field-6d622effb19e67ba72dc16bdf0f055d6.png" width="3200" height="2000" class="img_ev3q"></p>
<p>It resolves the run's creator from the audit trail and classifies them:</p>
<ul>
<li class="">A <strong>human</strong> user (for runs started from the portal), or</li>
<li class="">An <strong>automation</strong> identity - the service account behind a Git webhook or scheduled build (as shown above), or</li>
<li class=""><strong>N/A</strong>, when the initiator can't be resolved (for example, historical runs created before auditing was enabled).</li>
</ul>
<p>It's deliberately distinct from the existing <strong>Author</strong> column, which shows the Git commit author. The person who wrote the code is not always the identity that launched the run. Under the hood the portal calls the krci-audit read API with the run's <code>kind</code>, <code>namespace</code>, and <code>name</code>; no extra annotations, no changes to how pipelines are created.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI PipelineRuns list in the CI/CD Pipelines section" src="https://docs.kuberocketci.io/assets/images/krci-audit-pipelineruns-list-e03875f4eb4b31893274f4329f142bfa.png" width="3200" height="2000" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-you-query-the-audit-trail-directly">How Do You Query the Audit Trail Directly?<a href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what#how-do-you-query-the-audit-trail-directly" class="hash-link" aria-label="Direct link to How Do You Query the Audit Trail Directly?" title="Direct link to How Do You Query the Audit Trail Directly?" translate="no">​</a></h2>
<p><strong>Through a plain HTTP API</strong> - the same data the portal uses, handy for incident triage, scripts, and compliance exports. It's a <code>ClusterIP</code> service (in-cluster only), so port-forward it to try from your workstation:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci-audit port-forward svc/krci-audit-api </span><span class="token number" style="color:#36acaa">18080</span><span class="token plain">:8080</span><br></div></code></pre></div></div>
<p><strong>"Who created this resource?"</strong> - an initiator lookup by <code>kind</code>, <code>namespace</code>, and <code>name</code>:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"http://localhost:18080/api/v1/audit/initiator?kind=PipelineRun&amp;namespace=krci&amp;name=build-test-go-app-main-8a2c"</span><br></div></code></pre></div></div>
<div class="language-json codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-json codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><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 property" style="color:#36acaa">"actor"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"system:serviceaccount:krci:krci-admin"</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 property" style="color:#36acaa">"operation"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"CREATE"</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 property" style="color:#36acaa">"found"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean" style="color:#36acaa">true</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 property" style="color:#36acaa">"timestamp"</span><span class="token operator" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"2026-07-07T09:41:13Z"</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><br></div></code></pre></div></div>
<p><strong>"What happened to this kind of object?"</strong> - a filterable, paginated events query (by <code>kind</code>, <code>operation</code>, <code>actor</code>, time):</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"http://localhost:18080/api/v1/audit/events?kind=PipelineRun&amp;operation=DELETE&amp;limit=20"</span><br></div></code></pre></div></div>
<p>Real questions this answers without a cloud console:</p>
<ul>
<li class=""><em>"Who deleted my Codebase last night?"</em> - filter by <code>operation=DELETE</code> and the object.</li>
<li class=""><em>"Show me everything this service account did."</em> - filter by <code>actor</code>; the same query backs a user's own activity view.</li>
<li class=""><em>"Produce the change history for this resource for an audit."</em> - export the filtered result.</li>
</ul>
<p>Because the store is <strong>append-only</strong> and time-stamped, it doubles as compliance evidence: an immutable record of who changed what. It maps cleanly to <strong>SOC 2</strong>, <strong>ISO 27001</strong>, <strong>GDPR</strong>, and <strong>PCI DSS</strong> change-tracking controls, and it's retrievable through the API without handing anyone direct database access.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="when-should-you-still-use-native-audit-and-siem">When Should You Still Use Native Audit and SIEM?<a href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what#when-should-you-still-use-native-audit-and-siem" class="hash-link" aria-label="Direct link to When Should You Still Use Native Audit and SIEM?" title="Direct link to When Should You Still Use Native Audit and SIEM?" translate="no">​</a></h2>
<p><strong>When you need cluster-wide forensics, tamper-proof off-cluster retention, or cross-signal correlation - keep shipping the API server audit log to your SIEM.</strong> To be clear about scope: krci-audit <strong>complements</strong> native audit logging, it doesn't replace a security program. krci-audit is for the <strong>operational, in-platform</strong> question - fast, portable, developer-facing "who/what/when" - not the security org's system of record.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="future-work">Future Work<a href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what#future-work" class="hash-link" aria-label="Direct link to Future Work" title="Direct link to Future Work" translate="no">​</a></h2>
<p>This is the first slice of a broader capability. Next up:</p>
<ul>
<li class=""><strong>Authentication and authorization on the API.</strong> Today the read API is unprotected and reachable only in-cluster (ClusterIP, no Ingress), with network scope as the interim boundary. Role-based access - so the trail respects who's allowed to see what - is the next milestone.</li>
<li class=""><strong>"Who changed what" across every object, in the portal.</strong> The initiator today is surfaced for PipelineRuns; the same audit data can enrich <em>every</em> KubeRocketCI resource - a change history and last-modified-by on Codebases, environments, Git servers, and more - turning the portal into a single place to answer accountability questions for the whole platform.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-steps">Next Steps<a href="https://docs.kuberocketci.io/blog/kubernetes-audit-trail-who-changed-what#next-steps" class="hash-link" aria-label="Direct link to Next Steps" title="Direct link to Next Steps" translate="no">​</a></h2>
<ul>
<li class=""><strong>Operators:</strong> follow the <a class="" href="https://docs.kuberocketci.io/docs/next/operator-guide/monitoring-and-observability/audit-trails-setup">Audit Trails Setup guide</a> to enable the add-on, choose what to capture, and set retention.</li>
<li class=""><strong>Users:</strong> see the <a class="" href="https://docs.kuberocketci.io/docs/next/user-guide/pipelines">Pipelines Overview</a> for the Triggered By field in context.</li>
<li class=""><strong>New to the platform?</strong> Start with <a class="" href="https://docs.kuberocketci.io/docs/about-platform">What is KubeRocketCI</a> or <a class="" href="https://docs.kuberocketci.io/docs/quick-start/quick-start-overview">try it locally</a>.</li>
</ul>
<p>Audit trails are one of those features that's invisible until the moment you need it - and then it's the only thing that matters. Turn it on with a goal, keep it scoped, and it'll answer "who changed what?" in one call instead of one afternoon.</p>]]></content>
        <author>
            <name>Sergiy Kulanov</name>
            <uri>https://github.com/sergk</uri>
        </author>
        <category label="KubeRocketCI" term="KubeRocketCI"/>
        <category label="Kubernetes" term="Kubernetes"/>
        <category label="Audit" term="Audit"/>
        <category label="Security" term="Security"/>
        <category label="Compliance" term="Compliance"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="Observability" term="Observability"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[GitLab CI Integration in KubeRocketCI]]></title>
        <id>https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci</id>
        <link href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci"/>
        <updated>2026-06-08T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How KubeRocketCI runs CI in GitLab CI instead of Tekton: onboard the CI/CD components, create the ConfigMap, add a Runner. A hands-on multi-CI walkthrough.]]></summary>
        <content type="html"><![CDATA[<p><strong>GitLab CI integration</strong> in KubeRocketCI lets a single application run its CI pipeline in <strong>GitLab CI - on a GitLab Runner - instead of Tekton</strong>, while still being managed as a first-class Codebase on the platform. You set one field on the Codebase (<code>spec.ciTool: gitlab</code>), and KubeRocketCI generates a <strong><code>.gitlab-ci.yml</code></strong> in the repository; GitLab then runs the pipeline, with no Tekton involved. From then on, every merge request runs a review pipeline and every merge runs a build pipeline - all native GitLab CI, all on your own cluster.</p>
<p>This is part three of my hands-on series on the local <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">try-kuberocketci</a> testbed. In <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">part one</a> I stood up the full platform in two commands; in <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">part two</a> I built ephemeral preview environments from a feature branch. Both ran their CI in <strong>Tekton</strong>. This post takes the same <a href="https://kind.sigs.k8s.io/" target="_blank" rel="noopener noreferrer" class="">kind</a> cluster running <a class="" href="https://docs.kuberocketci.io/docs/about-platform">KubeRocketCI</a> 3.13 and shows the <strong>multi-CI</strong> path: how GitLab CI integration works, the three things you must set up <em>before</em> you enable it - a Runner, the onboarded <strong>CI/CD components</strong>, and a <strong>ConfigMap</strong> - and a full review-to-build run with real output.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-gitlab-ci-integration-means-in-kuberocketci">What "GitLab CI Integration" Means in KubeRocketCI<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#what-gitlab-ci-integration-means-in-kuberocketci" class="hash-link" aria-label="Direct link to What &quot;GitLab CI Integration&quot; Means in KubeRocketCI" title="Direct link to What &quot;GitLab CI Integration&quot; Means in KubeRocketCI" translate="no">​</a></h2>
<p>By default, KubeRocketCI runs your CI in Tekton. Version 3.13 added <strong>multi-CI</strong>: a per-Codebase choice of engine, set with one field - <code>spec.ciTool: tekton</code> (the default) or <code>spec.ciTool: gitlab</code> (see the <a class="" href="https://docs.kuberocketci.io/docs/operator-guide/upgrade/upgrade-krci-3.13">3.13 notes</a> and <a class="" href="https://docs.kuberocketci.io/docs/api/codebase">Codebase API</a>). Choosing <code>gitlab</code> is what this post is about.</p>
<p>When you set <code>ciTool: gitlab</code>, KubeRocketCI handles the wiring. Instead of the Tekton <code>EventListener</code> and webhook it would normally create, it <strong>generates a <code>.gitlab-ci.yml</code> and commits it to the repository</strong>; GitLab CI then runs the pipeline. The file is generated rather than written by hand, and no Tekton is involved - the build runs as native GitLab CI jobs on a GitLab Runner, and the logs reside in GitLab.</p>
<p>Other aspects are unchanged: the application remains a first-class <strong>Codebase</strong>, with the same Portal, branches, and GitOps deployment as a Tekton application. GitLab-CI and Tekton applications can run side by side on the same platform; only the CI execution moves to GitLab (see <a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#gitlab-ci-vs-tekton-when-to-use-which" class="">the comparison below</a>).</p>
<!-- -->
<p>On a real codebase the file lands in the repo, and there is no project webhook - GitLab CI does not need one:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebase java-gitlabci-app </span><span class="token parameter variable" style="color:#36acaa">-o</span><span class="token plain"> </span><span class="token assign-left variable" style="color:#36acaa">jsonpath</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">'{.spec.ciTool}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">gitlab</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># .gitlab-ci.yml is committed to the repo; project webhooks: 0</span><br></div></code></pre></div></div>
<p>The generated <code>.gitlab-ci.yml</code> is intentionally minimal: it contains no build logic of its own and instead references reusable GitLab CI/CD components. Those components must be in place first.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="before-you-enable-it-three-things-to-set-up-first">Before You Enable It: Three Things to Set Up First<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#before-you-enable-it-three-things-to-set-up-first" class="hash-link" aria-label="Direct link to Before You Enable It: Three Things to Set Up First" title="Direct link to Before You Enable It: Three Things to Set Up First" translate="no">​</a></h2>
<p>GitLab CI integration has three prerequisites that the Tekton path does not - a one-time platform setup, not something you repeat per app. Skip any one and the first pipeline fails to start or cannot resolve its components; complete them once, and subsequent GitLab-CI Codebases require no additional setup.</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="1-install-a-gitlab-runner-kuberocketci-does-not-bundle-one">1. Install a GitLab Runner (KubeRocketCI Does Not Bundle One)<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#1-install-a-gitlab-runner-kuberocketci-does-not-bundle-one" class="hash-link" aria-label="Direct link to 1. Install a GitLab Runner (KubeRocketCI Does Not Bundle One)" title="Direct link to 1. Install a GitLab Runner (KubeRocketCI Does Not Bundle One)" translate="no">​</a></h3>
<p>Tekton ships with KubeRocketCI; a <strong>GitLab Runner does not</strong>. GitLab CE already serves CI, but with no runner registered, jobs sit <code>pending</code> forever. On the testbed, <code>make gitlab-ci</code> installs the official <code>gitlab/gitlab-runner</code> Helm chart with the <strong>Kubernetes executor</strong> and registers it.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> gitlab-runner get pods</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                            READY   STATUS    RESTARTS   AGE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">gitlab-runner-f655c776c-xr9cm   </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">/1     Running   </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">          26m</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"></span><span class="token comment" style="color:#999988;font-style:italic"># the runner reports online as an instance runner</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">runner: krci-kubernetes   </span><span class="token assign-left variable" style="color:#36acaa">online</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">True   </span><span class="token assign-left variable" style="color:#36acaa">executor</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">kubernetes</span><br></div></code></pre></div></div>
<p>The Kubernetes executor runs each job as a pod in the cluster - the same model Tekton uses, so your CI capacity scales with the cluster. The testbed uses Helm for simplicity, but on a real cluster you install the runner the GitOps way, like every other platform component: KubeRocketCI ships a <a href="https://github.com/epam/edp-cluster-add-ons/tree/main/clusters/core/addons/gitlab-runner" target="_blank" rel="noopener noreferrer" class=""><code>gitlab-runner</code> add-on</a> in the <a href="https://github.com/epam/edp-cluster-add-ons" target="_blank" rel="noopener noreferrer" class="">edp-cluster-add-ons</a> repository that Argo CD reconciles, with the runner's registration token supplied through External Secrets. Either way you end up with one registered runner, sized to your job volume, ready to execute GitLab CI jobs.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="2-onboard-the-kuberocketci-cicd-components-into-your-instance">2. Onboard the KubeRocketCI CI/CD Components Into Your Instance<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#2-onboard-the-kuberocketci-cicd-components-into-your-instance" class="hash-link" aria-label="Direct link to 2. Onboard the KubeRocketCI CI/CD Components Into Your Instance" title="Direct link to 2. Onboard the KubeRocketCI CI/CD Components Into Your Instance" translate="no">​</a></h3>
<p>This prerequisite is easy to overlook. The injected <code>.gitlab-ci.yml</code> does not contain the build steps directly; it references a <strong>GitLab CI/CD component</strong> - the <a href="https://gitlab.com/kuberocketci/ci-java17-mvn" target="_blank" rel="noopener noreferrer" class=""><code>kuberocketci/ci-java17-mvn</code></a> library, which exposes <code>review</code> and <code>build</code> entry points over a 7-stage flow (<code>prepare → test → build → verify → package → publish → release</code>).</p>
<p>The catch: <strong>GitLab CI/CD components only resolve on the same GitLab instance</strong> (<code>$CI_SERVER_FQDN</code>). A component reference like <code>$CI_SERVER_FQDN/kuberocketci/ci-java17-mvn/build@0.1.1</code> will not reach out to <code>gitlab.com</code> - it resolves against <em>your</em> GitLab. So you must <strong>onboard (mirror) the components into your instance before enabling GitLab CI</strong>, and tag them at the version your template pins.</p>
<p><img decoding="async" loading="lazy" alt="Mirrored KubeRocketCI ci-java17-mvn CI/CD component in the self-hosted GitLab - review and build templates at tag 0.1.1" src="https://docs.kuberocketci.io/assets/images/gitlab-cicd-component-e6844f12e1f7b8c493f819b020300433.png" title="The onboarded KubeRocketCI CI/CD component in the local GitLab instance" width="3200" height="2000" class="img_ev3q"></p>
<p>On the testbed, <code>make gitlab-ci</code> mirrors the component into the local GitLab as <code>kuberocketci/ci-java17-mvn</code> at tag <code>0.1.1</code> (with two local-only patches: native arm64 builds and pushing to the in-cluster GitLab Container Registry instead of Docker Hub). In your own environment, this is where you publish the KubeRocketCI components - or your own forked components - to your GitLab CI/CD Catalog so every project can <code>include</code> them.</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>Onboard the components first, enable the Codebase second</div><div class="admonitionContent_BuS1"><p>If you set <code>ciTool: gitlab</code> before the component exists at the pinned tag on your instance, KubeRocketCI still injects <code>.gitlab-ci.yml</code>, but the very first pipeline fails at the <code>include:</code> stage with an unresolved-component error. Mirror and tag the components first.</p></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="3-create-the-gitlab-ciyml-template-configmap">3. Create the <code>.gitlab-ci.yml</code> Template ConfigMap<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#3-create-the-gitlab-ciyml-template-configmap" class="hash-link" aria-label="Direct link to 3-create-the-gitlab-ciyml-template-configmap" title="Direct link to 3-create-the-gitlab-ciyml-template-configmap" translate="no">​</a></h3>
<p>KubeRocketCI does not invent the pipeline - it fills in a template stored in a <strong>ConfigMap</strong>, one per tech stack. The Codebase points at the template with an annotation (or KubeRocketCI falls back to a <code>gitlab-ci-{lang}-{buildtool}</code> naming convention), and your codebase name is substituted in before the file is committed.</p>
<p>The ConfigMap for Java/Maven contains only orchestration - a <code>workflow</code> rule set, variables, and two <code>include</code>d component entry points (<code>review</code> and <code>build</code>) gated by rules:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_OeMC">manifests/gitlab-ci-java-maven-configmap.yaml (excerpt)</div><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">apiVersion</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">kind</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ConfigMap</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">metadata</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">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> gitlab</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">ci</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">java</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">maven</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">namespace</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> krci</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">data</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">.gitlab-ci.yml</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">|</span><span class="token scalar string" style="color:#e3116c"></span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">    workflow:</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">      rules:</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">        - if: $CI_PIPELINE_SOURCE == "merge_request_event"</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">        - if: $CI_COMMIT_REF_PROTECTED == "true"</span><br></div><div class="token-line" style="color:#393A34"><span class="token scalar string" style="color:#e3116c">        - if: $CI_COMMIT_TAG =~ /^\d+\.\d+\.\d+$/</span><span class="token plain"></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">    </span><span class="token key atrule" style="color:#00a4db">variables</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">CODEBASE_NAME</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"{{.CodebaseName}}"</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">CONTAINER_IMAGE</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"maven:3.9-eclipse-temurin-17"</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">IMAGE_REGISTRY</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"$CI_REGISTRY_IMAGE"</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">CHART_DIR</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"deploy-templates"</span><span class="token plain"></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">    </span><span class="token key atrule" style="color:#00a4db">include</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 comment" style="color:#999988;font-style:italic"># REVIEW - merge-request validation (test/build/lint/sonar/dockerbuild-verify; no push)</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">component</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $CI_SERVER_FQDN/kuberocketci/ci</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">java17</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">mvn/review@0.1.1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">inputs</span><span class="token punctuation" style="color:#393A34">:</span><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">codebase_name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">CODEBASE_NAME</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">container_image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">CONTAINER_IMAGE</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">chart_dir</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">CHART_DIR</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </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">rules</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">if</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $CI_PIPELINE_SOURCE == "merge_request_event"</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">      </span><span class="token comment" style="color:#999988;font-style:italic"># BUILD - protected/default branch: build, push image, deploy/tag</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">component</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $CI_SERVER_FQDN/kuberocketci/ci</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">java17</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">mvn/build@0.1.1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">        </span><span class="token key atrule" style="color:#00a4db">inputs</span><span class="token punctuation" style="color:#393A34">:</span><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">codebase_name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">CODEBASE_NAME</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">container_image</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">CONTAINER_IMAGE</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">image_registry</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">IMAGE_REGISTRY</span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">chart_dir</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain">CHART_DIR</span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"> </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">rules</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">if</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH </span><span class="token punctuation" style="color:#393A34">|</span><span class="token punctuation" style="color:#393A34">|</span><span class="token plain"> $CI_COMMIT_REF_PROTECTED == "true"</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">    </span><span class="token key atrule" style="color:#00a4db">stages</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">prepare</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> test</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> build</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> verify</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> package</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> publish</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> release</span><span class="token punctuation" style="color:#393A34">]</span><br></div></code></pre></div></div>
<p>Apply it once:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl apply </span><span class="token parameter variable" style="color:#36acaa">-f</span><span class="token plain"> manifests/gitlab-ci-java-maven-configmap.yaml</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">configmap/gitlab-ci-java-maven created</span><br></div></code></pre></div></div>
<p>These two rules are the entire control flow: the <code>review</code> component runs on <code>merge_request_event</code>, and the <code>build</code> component runs on the protected/default branch. That split produces the "MR → review, merge → build" behavior, with no webhook required.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="enable-gitlab-ci-on-a-codebase-speccitool-gitlab">Enable GitLab CI on a Codebase (<code>spec.ciTool: gitlab</code>)<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#enable-gitlab-ci-on-a-codebase-speccitool-gitlab" class="hash-link" aria-label="Direct link to enable-gitlab-ci-on-a-codebase-speccitool-gitlab" title="Direct link to enable-gitlab-ci-on-a-codebase-speccitool-gitlab" translate="no">​</a></h2>
<p>With the Runner online, the components onboarded, and the ConfigMap applied, enabling GitLab CI is one Codebase manifest. The <code>ciTool: gitlab</code> field selects the engine; the annotation selects the template:</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_OeMC">manifests/sample-gitlabci-codebase.yaml (excerpt)</div><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">apiVersion</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> v2.edp.epam.com/v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">kind</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> Codebase</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">metadata</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">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> java</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">gitlabci</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">app</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">namespace</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> krci</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">annotations</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">app.edp.epam.com/gitlab-ci-template</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> gitlab</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">ci</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">java</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">maven   </span><span class="token comment" style="color:#999988;font-style:italic"># pick the ConfigMap</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">spec</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">type</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> application</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">lang</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> java</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">framework</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> java17</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">buildTool</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> maven</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">ciTool</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> gitlab            </span><span class="token comment" style="color:#999988;font-style:italic"># &lt;-- run CI in GitLab CI, not Tekton</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">strategy</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> import</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">gitServer</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> gitlab</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">gitUrlPath</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> /krci/java</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">gitlabci</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">app</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">disablePutDeployTemplates</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</span><br></div></code></pre></div></div>
<p>In the KubeRocketCI Portal, the component is a normal Codebase - same lifecycle, same branches, same deployment story as a Tekton app - but its <strong>CI Tool reads <code>GitLab</code></strong>. That is the platform side of the integration: you manage the app on KubeRocketCI, even though its pipeline executes in GitLab.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal overview for java-gitlabci-app: CI Tool set to GitLab, language Java, build tool Maven" src="https://docs.kuberocketci.io/assets/images/krci-portal-gitlabci-codebase-c4310aba4c86c0678c7d73b96feb98bf.png" title="The GitLab-CI Codebase as seen in the KubeRocketCI Portal" width="3200" height="2000" class="img_ev3q"></p>
<p>Once KubeRocketCI finishes, the injected <code>.gitlab-ci.yml</code> is visible in the GitLab project - the <code>{{.CodebaseName}}</code> placeholder now reads <code>java-gitlabci-app</code>, and the two component includes are wired and ready:</p>
<p><img decoding="async" loading="lazy" alt=".gitlab-ci.yml injected by the KubeRocketCI codebase-operator in GitLab, including the review and build CI/CD components" src="https://docs.kuberocketci.io/assets/images/gitlab-ci-yml-injected-16c88b282bd1324356c745c6b31a4a37.png" title="The .gitlab-ci.yml injected by the codebase-operator, visible in GitLab" width="3200" height="2000" class="img_ev3q"></p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-pipeline-in-action-review-on-mr-build-on-merge">The Pipeline in Action: Review on MR, Build on Merge<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#the-pipeline-in-action-review-on-mr-build-on-merge" class="hash-link" aria-label="Direct link to The Pipeline in Action: Review on MR, Build on Merge" title="Direct link to The Pipeline in Action: Review on MR, Build on Merge" translate="no">​</a></h2>
<p>The flow mirrors the Tekton path from the <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">previous posts</a> - open a merge request to validate, merge to build and publish - but every job runs in GitLab CI on the Runner.</p>
<!-- -->
<p>In the GitLab project, both pipelines show green - one triggered by the merge request, one by the merge to <code>main</code>:</p>
<p><img decoding="async" loading="lazy" alt="GitLab Pipelines list for java-gitlabci-app: review (merge request) and build (push) pipelines passed on the GitLab Runner" src="https://docs.kuberocketci.io/assets/images/gitlab-pipelines-list-8698eba9d2b379480a3057bbec373981.png" title="Every GitLab CI pipeline passed - review on the MR, build on the merge" width="3200" height="2000" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="review-pipeline-merge-request">Review Pipeline (Merge Request)<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#review-pipeline-merge-request" class="hash-link" aria-label="Direct link to Review Pipeline (Merge Request)" title="Direct link to Review Pipeline (Merge Request)" translate="no">​</a></h3>
<p>Opening a merge request triggers a <code>merge_request_event</code> pipeline that runs the <code>review</code> component - validation only, <strong>no image push</strong>. On the live run it finished green in <strong>91 seconds</strong>, 10 jobs across four stages:</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">review pipeline #4 (merge_request_event) - 91s - GREEN</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  prepare   init-values</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  test      test            compile-check   lint</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  test      helm-docs       helm-lint       dockerfile-lint</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  build     build           sonar</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  verify    dockerbuild-verify        # buildkit builds the image but does NOT push</span><br></div></code></pre></div></div>
<p>The <code>sonar</code> job is a real SonarQube quality gate - the same engine the Tekton pipelines use - and <code>dockerbuild-verify</code> proves the image builds without publishing it. GitLab renders the MR with its pipeline result attached:</p>
<p><img decoding="async" loading="lazy" alt="GitLab merge request for java-gitlabci-app with the merge_request_event review pipeline passing before merge" src="https://docs.kuberocketci.io/assets/images/gitlab-merge-request-pipeline-20d220daf06a9e8c328e1d372469bf5f.png" title="The review pipeline attached to the merge request in GitLab" width="3200" height="2000" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="build-pipeline-protected-branch">Build Pipeline (Protected Branch)<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#build-pipeline-protected-branch" class="hash-link" aria-label="Direct link to Build Pipeline (Protected Branch)" title="Direct link to Build Pipeline (Protected Branch)" translate="no">​</a></h3>
<p>Merging the MR pushes to the protected <code>main</code> branch, which trips the second <code>workflow</code> rule and runs the <code>build</code> component. It is the review jobs <strong>plus</strong> the publishing stages - <code>buildkit-build</code> (build <em>and</em> push), <code>maven-deploy</code>, and <code>git-tag</code>. On the live run it finished green in <strong>100 seconds</strong>, 12 jobs:</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">build pipeline #5 (push, protected main) - 100s - GREEN</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  prepare   init-values</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  test      test            compile-check   lint</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  test      helm-docs       helm-lint       dockerfile-lint</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  build     build           sonar</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  package   buildkit-build  maven-deploy    # build+push image, publish artifact</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  publish   git-tag                         # tag the release</span><br></div></code></pre></div></div>
<p>The build pipeline graph in GitLab shows the stages fanning out - the same DAG model you get in Tekton, rendered by GitLab:</p>
<p><img decoding="async" loading="lazy" alt="GitLab CI build pipeline graph for java-gitlabci-app: prepare to publish stages, all 12 jobs green" src="https://docs.kuberocketci.io/assets/images/gitlab-build-pipeline-graph-e8063702a5d0b1e6665e72c865ec7f45.png" title="The build pipeline graph in GitLab - all jobs green" width="3200" height="2000" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-image-lands-in-gitlabs-container-registry">The Image Lands in GitLab's Container Registry<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#the-image-lands-in-gitlabs-container-registry" class="hash-link" aria-label="Direct link to The Image Lands in GitLab's Container Registry" title="Direct link to The Image Lands in GitLab's Container Registry" translate="no">​</a></h3>
<p>The <code>buildkit-build</code> job builds the container image and pushes it to GitLab's bundled <strong>Container Registry</strong> using the CI job token - no external registry, no Docker Hub credentials. After the build pipeline, the image is published with a content-addressed tag, and <code>git-tag</code> has created the matching release tag:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># image published by the build pipeline</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">image: krci/java-gitlabci-app   tags: </span><span class="token punctuation" style="color:#393A34">[</span><span class="token plain">723b97d8, cee1e98e</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" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># release tags created by the git-tag job</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tag: v0.1.0-cee1e98e</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tag: v0.1.0-723b97d8</span><br></div></code></pre></div></div>
<p><img decoding="async" loading="lazy" alt="GitLab Container Registry for java-gitlabci-app: image pushed by the buildkit-build job of the build pipeline" src="https://docs.kuberocketci.io/assets/images/gitlab-container-registry-f85c4c9627bfaf6ddfc5c607cf491511.png" title="The image published to GitLab&amp;#39;s Container Registry by the build pipeline" width="3200" height="2000" class="img_ev3q"></p>
<p>From here, the <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">GitOps delivery story</a> is identical to the Tekton path - the published image feeds the same CodebaseImageStream and Argo CD deployment flow covered in part two of this series.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="gitlab-ci-vs-tekton-when-to-use-which">GitLab CI vs Tekton: When to Use Which<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#gitlab-ci-vs-tekton-when-to-use-which" class="hash-link" aria-label="Direct link to GitLab CI vs Tekton: When to Use Which" title="Direct link to GitLab CI vs Tekton: When to Use Which" translate="no">​</a></h2>
<p>Both engines are first-class. The choice is about where your team already lives and what you want to own.</p>
<table><thead><tr><th>Dimension</th><th>Tekton (default)</th><th>GitLab CI (<code>ciTool: gitlab</code>)</th></tr></thead><tbody><tr><td>Pipeline definition</td><td>Tekton <code>PipelineRun</code> (KRCI-managed)</td><td><code>.gitlab-ci.yml</code> (operator-injected, GitLab-native)</td></tr><tr><td>Trigger mechanism</td><td>Tekton webhook</td><td>GitLab <code>workflow</code> rules (no webhook)</td></tr><tr><td>Where job logs live</td><td>KubeRocketCI Portal / Tekton</td><td>GitLab CI UI</td></tr><tr><td>Reusable build logic</td><td>Tekton tasks / pipeline library</td><td>GitLab CI/CD <strong>components</strong> (must be on your instance)</td></tr><tr><td>Image build</td><td>kaniko</td><td>buildkit</td></tr><tr><td>Runner needed</td><td>No (Tekton bundled)</td><td><strong>Yes</strong> - install a GitLab Runner</td></tr><tr><td>Best when</td><td>you want a unified, K8s-native CI surface in the Portal</td><td>your org standardizes on GitLab CI and its component catalog</td></tr></tbody></table>
<p><strong>Use Tekton</strong> when you want every Codebase to share one Kubernetes-native CI surface inside the Portal, with pipeline history in <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">Tekton Results</a>. <strong>Use GitLab CI</strong> when your organization has standardized on GitLab CI/CD components and wants pipeline authoring and logs to stay in GitLab - while still getting the KubeRocketCI Codebase lifecycle, Portal, and GitOps deployment around them. And because the choice is per-Codebase, you can migrate one application at a time.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="frequently-asked-questions">Frequently Asked Questions<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#frequently-asked-questions" class="hash-link" aria-label="Direct link to Frequently Asked Questions" title="Direct link to Frequently Asked Questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-does-citool-gitlab-do-in-kuberocketci">What does <code>ciTool: gitlab</code> do in KubeRocketCI?<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#what-does-citool-gitlab-do-in-kuberocketci" class="hash-link" aria-label="Direct link to what-does-citool-gitlab-do-in-kuberocketci" title="Direct link to what-does-citool-gitlab-do-in-kuberocketci" translate="no">​</a></h3>
<p>It switches a Codebase's CI engine from Tekton to GitLab CI. Instead of wiring the pipeline into Tekton, KubeRocketCI generates a <code>.gitlab-ci.yml</code> in the repository, and the pipeline runs as native GitLab CI jobs on a GitLab Runner. The only valid values for the field are <code>tekton</code> and <code>gitlab</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-kuberocketci-replace-gitlab-ci-or-run-on-top-of-it">Does KubeRocketCI replace GitLab CI, or run on top of it?<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#does-kuberocketci-replace-gitlab-ci-or-run-on-top-of-it" class="hash-link" aria-label="Direct link to Does KubeRocketCI replace GitLab CI, or run on top of it?" title="Direct link to Does KubeRocketCI replace GitLab CI, or run on top of it?" translate="no">​</a></h3>
<p>It runs on top of it. With GitLab CI integration, KubeRocketCI manages the Codebase lifecycle, owns the injected <code>.gitlab-ci.yml</code>, and provides the Portal and GitOps deployment - but the pipeline itself executes in GitLab CI, and the job logs live in GitLab. There is no Tekton in the pipeline path at all.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="do-i-need-a-gitlab-runner-for-the-gitlab-ci-path">Do I need a GitLab Runner for the GitLab CI path?<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#do-i-need-a-gitlab-runner-for-the-gitlab-ci-path" class="hash-link" aria-label="Direct link to Do I need a GitLab Runner for the GitLab CI path?" title="Direct link to Do I need a GitLab Runner for the GitLab CI path?" translate="no">​</a></h3>
<p>Yes. KubeRocketCI bundles Tekton but not a GitLab Runner. With <code>ciTool: gitlab</code>, jobs run as GitLab CI and need a registered runner to execute. The testbed installs the official <code>gitlab/gitlab-runner</code> Helm chart with the Kubernetes executor, registered via a GitLab 17.x runner authentication token. Without a runner, pipelines stay <code>pending</code>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-do-i-have-to-onboard-the-cicd-components-into-my-own-gitlab-instance">Why do I have to onboard the CI/CD components into my own GitLab instance?<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#why-do-i-have-to-onboard-the-cicd-components-into-my-own-gitlab-instance" class="hash-link" aria-label="Direct link to Why do I have to onboard the CI/CD components into my own GitLab instance?" title="Direct link to Why do I have to onboard the CI/CD components into my own GitLab instance?" translate="no">​</a></h3>
<p>Because GitLab CI/CD components only resolve on the same instance (<code>$CI_SERVER_FQDN</code>). A reference like <code>$CI_SERVER_FQDN/kuberocketci/ci-java17-mvn/build@0.1.1</code> is looked up on <em>your</em> GitLab, not <code>gitlab.com</code>. So you must mirror (onboard) the KubeRocketCI components - or your own forks - into your instance and tag them at the version your template pins, before enabling GitLab CI on any Codebase.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-the-configmap-for-in-gitlab-ci-integration">What is the ConfigMap for in GitLab CI integration?<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#what-is-the-configmap-for-in-gitlab-ci-integration" class="hash-link" aria-label="Direct link to What is the ConfigMap for in GitLab CI integration?" title="Direct link to What is the ConfigMap for in GitLab CI integration?" translate="no">​</a></h3>
<p>The ConfigMap holds the <code>.gitlab-ci.yml</code> template that KubeRocketCI injects. You create one per tech stack (for example <code>gitlab-ci-java-maven</code>), and the Codebase points at it with an annotation. KubeRocketCI substitutes your codebase name and commits the result. The template is thin orchestration - it <code>include</code>s the onboarded CI/CD components and gates them with <code>workflow</code> rules.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-does-the-mr-to-build-flow-work-without-a-webhook">How does the MR-to-build flow work without a webhook?<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#how-does-the-mr-to-build-flow-work-without-a-webhook" class="hash-link" aria-label="Direct link to How does the MR-to-build flow work without a webhook?" title="Direct link to How does the MR-to-build flow work without a webhook?" translate="no">​</a></h3>
<p>GitLab CI integration relies on GitLab's own <code>workflow</code> rules instead of a webhook. The injected <code>.gitlab-ci.yml</code> includes the <code>review</code> component gated on <code>$CI_PIPELINE_SOURCE == "merge_request_event"</code> and the <code>build</code> component gated on the protected/default branch. Opening a merge request runs review; merging pushes to the protected branch and runs build. GitLab triggers both natively, so no project webhook is created.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="can-tekton-and-gitlab-ci-codebases-coexist-on-the-same-cluster">Can Tekton and GitLab CI codebases coexist on the same cluster?<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#can-tekton-and-gitlab-ci-codebases-coexist-on-the-same-cluster" class="hash-link" aria-label="Direct link to Can Tekton and GitLab CI codebases coexist on the same cluster?" title="Direct link to Can Tekton and GitLab CI codebases coexist on the same cluster?" translate="no">​</a></h3>
<p>Yes. The choice is per-Codebase, and the same <code>gitlab</code> GitServer is reused for both engines. On the testbed, Tekton codebases (the Go app from parts one and two) run side by side with the GitLab-CI Java app on the same KubeRocketCI platform. You can adopt GitLab CI for one application without touching the rest.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="where-does-the-container-image-go-in-the-gitlab-ci-build-pipeline">Where does the container image go in the GitLab CI build pipeline?<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#where-does-the-container-image-go-in-the-gitlab-ci-build-pipeline" class="hash-link" aria-label="Direct link to Where does the container image go in the GitLab CI build pipeline?" title="Direct link to Where does the container image go in the GitLab CI build pipeline?" translate="no">​</a></h3>
<p>The <code>buildkit-build</code> job builds and pushes the image to GitLab's bundled Container Registry using the CI job token - no external registry or Docker Hub credentials required. On the testbed it pushes to the in-cluster GitLab registry, and the <code>git-tag</code> job creates a matching release tag. From there the image feeds the same CodebaseImageStream and Argo CD deployment flow as the Tekton path.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://docs.kuberocketci.io/blog/gitlab-ci-integration-kuberocketci#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<p>GitLab CI integration in KubeRocketCI lets a Codebase run its pipeline in <strong>GitLab CI instead of Tekton</strong> by setting <code>spec.ciTool: gitlab</code>. KubeRocketCI then <strong>generates a <code>.gitlab-ci.yml</code></strong> that pulls in reusable GitLab CI/CD components, gated so that a merge request runs a review pipeline and a merge runs a build pipeline. There is no webhook and no Tekton in the path - the proof is <strong>zero <code>PipelineRun</code>s</strong> for the Codebase.</p>
<p>Before you enable it, set up three things: a <strong>GitLab Runner</strong> (KubeRocketCI does not bundle one), the <strong>onboarded CI/CD components</strong> in your own instance (they only resolve on the same GitLab), and the <strong>template ConfigMap</strong> KubeRocketCI injects. Complete these once, and subsequent GitLab-CI Codebases require no additional setup.</p>
<p>I ran the full flow on the local <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">try-kuberocketci</a> testbed: review pipeline green in 91s (10 jobs), build pipeline green in 100s (12 jobs), image pushed to GitLab's Container Registry, release tags created, and zero Tekton PipelineRuns - every command and result above is from that run.</p>
<p>The most useful next steps from here:</p>
<ul>
<li class="">Spin up the testbed with <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">Try KubeRocketCI Locally</a>, then run <code>make gitlab-ci</code> and <code>make e2e-gitlabci</code> to reproduce this flow.</li>
<li class="">Read the <a class="" href="https://docs.kuberocketci.io/docs/operator-guide/upgrade/upgrade-krci-3.13">3.13 upgrade notes</a> for the multi-CI <code>ciTool</code> enum and the <a class="" href="https://docs.kuberocketci.io/docs/api/codebase">Codebase API</a> for the field reference.</li>
<li class="">Compare the Tekton path in <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">Kubernetes-native CI/CD with Tekton</a> and the GitOps delivery in <a class="" href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch">Ephemeral Preview Environments</a>.</li>
<li class="">Browse the <a href="https://docs.kuberocketci.io/" target="_blank" rel="noopener noreferrer" class="">KubeRocketCI documentation</a> and the <a class="" href="https://docs.kuberocketci.io/docs/basic-concepts">basic concepts</a> for Codebases, CodebaseImageStreams, and deployment flows.</li>
</ul>
<p>KubeRocketCI is open source under Apache License 2.0. The platform, Helm charts, and the testbed are all on <a href="https://github.com/KubeRocketCI/try-kuberocketci" target="_blank" rel="noopener noreferrer" class="">GitHub</a>.</p>
]]></content>
        <author>
            <name>Sergiy Kulanov</name>
            <uri>https://github.com/sergk</uri>
        </author>
        <category label="KubeRocketCI" term="KubeRocketCI"/>
        <category label="GitLab CI" term="GitLab CI"/>
        <category label="CI/CD" term="CI/CD"/>
        <category label="Tekton" term="Tekton"/>
        <category label="Kubernetes" term="Kubernetes"/>
        <category label="GitLab Runner" term="GitLab Runner"/>
        <category label="Multi-CI" term="Multi-CI"/>
        <category label="GitOps" term="GitOps"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="DevOps" term="DevOps"/>
        <category label="Open Source" term="Open Source"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Ephemeral Preview Environments on Kubernetes]]></title>
        <id>https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch</id>
        <link href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch"/>
        <updated>2026-06-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Spin up per-feature-branch preview environments on Kubernetes with KubeRocketCI, Tekton, and Argo CD — isolated namespaces, auto-cleanup, no SaaS bill.]]></summary>
        <content type="html"><![CDATA[<p>An ephemeral preview environment is an isolated, temporary Kubernetes deployment created from a single feature branch and torn down when the work is done. Every branch gets its own namespace, its own image, its own URL - and zero of it lingers afterward. Ephemeral environments on Kubernetes make this pattern available on your own cluster - but a hands-on, open-source, portal-native version - feature branch to isolated namespace to one-click destroy, backed by real Tekton CI and Argo CD GitOps - is conspicuously missing from the public record.</p>
<p>So I built one, end to end, on the same local <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">try-kuberocketci</a> testbed from my last post: a <a href="https://kind.sigs.k8s.io/" target="_blank" rel="noopener noreferrer" class="">kind</a> cluster running <a class="" href="https://docs.kuberocketci.io/docs/about-platform">KubeRocketCI</a> 3.14.0 with Tekton, Argo CD, and self-hosted GitLab. This post is the full walkthrough - every screenshot, every line of terminal output, captured from a live run. We will take a stable <code>main</code> deployment, branch off it, ship a change that is visible <em>only</em> in the preview environment, inject per-environment config through GitOps, prove the two environments never touch each other, and then destroy the whole thing - leaving the baseline exactly as it was.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-an-ephemeral-preview-environment">What Is an Ephemeral Preview Environment?<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#what-is-an-ephemeral-preview-environment" class="hash-link" aria-label="Direct link to What Is an Ephemeral Preview Environment?" title="Direct link to What Is an Ephemeral Preview Environment?" translate="no">​</a></h2>
<p>An <strong>ephemeral preview environment</strong> is a short-lived, namespace-isolated copy of your application, provisioned automatically from a feature branch or pull request and destroyed when that branch merges or is no longer needed. Unlike a shared staging environment - where one branch at a time monopolizes the slot - a preview environment is dedicated to a single branch, carries no long-term state, and releases its resources (namespace, pods, ingress) the moment you tear it down.</p>
<p>The terms <em>preview environment</em> and <em>ephemeral environment</em> are used interchangeably. "Preview" emphasizes the use case (review a branch before merge); "ephemeral" emphasizes the lifecycle (temporary by design). In KubeRocketCI they are the same object: a Deployment <strong>Stage</strong> with its own isolated Kubernetes namespace, created for a branch and removed on demand.</p>
<p>A preview environment lifecycle has five stages:</p>
<!-- -->
<ol>
<li class=""><strong>Trigger</strong> - create or push a branch in the Portal.</li>
<li class=""><strong>Build</strong> - Tekton pipeline builds and pushes a versioned image.</li>
<li class=""><strong>Provision</strong> - namespace created, Argo CD syncs the application.</li>
<li class=""><strong>Validate</strong> - test the live URL.</li>
<li class=""><strong>Destroy</strong> - namespace and Argo CD application deleted.</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="ephemeral-vs-staging-why-shared-environments-become-bottlenecks">Ephemeral vs. Staging: Why Shared Environments Become Bottlenecks<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#ephemeral-vs-staging-why-shared-environments-become-bottlenecks" class="hash-link" aria-label="Direct link to Ephemeral vs. Staging: Why Shared Environments Become Bottlenecks" title="Direct link to Ephemeral vs. Staging: Why Shared Environments Become Bottlenecks" translate="no">​</a></h3>
<p>A shared staging environment serializes your team: only one branch can occupy it at a time, so parallel feature work cannot be validated simultaneously, and "who broke staging?" becomes a recurring standup question. Ephemeral preview environments give every branch its own namespace, so several features can be tested concurrently without interfering - and when a branch merges, its namespace is deleted, so there is no idle staging box quietly accruing cost.</p>
<p>That last point matters more than it looks. Industry surveys consistently find a large share of provisioned Kubernetes capacity sits idle, and the trend is getting worse - <a href="https://cast.ai/blog/2026-state-of-kubernetes-resource-optimization-cpu-at-8-memory-at-20-and-getting-worse/" target="_blank" rel="noopener noreferrer" class="">CAST AI's 2026 State of Kubernetes Optimization Report</a> measured average CPU utilization at just <strong>8%</strong> across roughly 23,000 production clusters (down from 10% a year earlier), and <a href="https://www.flexera.com/about-us/press-center/flexera-finds-cloud-value-is-rising-while-ai-waste-grows" target="_blank" rel="noopener noreferrer" class="">Flexera's 2026 State of the Cloud report</a> pegs self-estimated cloud waste at <strong>~29%</strong>, its first rise in five years. Permanent staging environments are a textbook source of that waste. An environment that deletes itself cannot become idle.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="namespace-per-branch-vcluster-or-dedicated-cluster">Namespace-per-Branch, vCluster, or Dedicated Cluster?<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#namespace-per-branch-vcluster-or-dedicated-cluster" class="hash-link" aria-label="Direct link to Namespace-per-Branch, vCluster, or Dedicated Cluster?" title="Direct link to Namespace-per-Branch, vCluster, or Dedicated Cluster?" translate="no">​</a></h3>
<p>There are three common isolation models. <strong>Namespace-per-branch</strong> reuses one shared control plane, provisions in seconds, and is sufficient for most teams - it is the model used here. <strong>vCluster</strong> adds a lightweight virtual API server per environment for stronger isolation at higher overhead. A <strong>dedicated cluster per branch</strong> is maximum isolation but impractical at scale. KubeRocketCI uses namespace-per-branch, with Argo CD managing each namespace's application manifests.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>Namespace isolation is not network isolation</div><div class="admonitionContent_BuS1"><p>A separate namespace scopes names, RBAC, and quotas - but by default pods in different namespaces can still reach each other over the cluster network. If your preview environments need network isolation too, add <code>NetworkPolicy</code> rules; the namespace boundary alone does not provide them.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-kuberocketci-delivers-preview-environments">How KubeRocketCI Delivers Preview Environments<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#how-kuberocketci-delivers-preview-environments" class="hash-link" aria-label="Direct link to How KubeRocketCI Delivers Preview Environments" title="Direct link to How KubeRocketCI Delivers Preview Environments" translate="no">​</a></h2>
<p>Before the walkthrough, a quick map of the moving parts. KubeRocketCI wraps a few <a class="" href="https://docs.kuberocketci.io/docs/basic-concepts">core concepts</a> that turn a Git branch into a running environment:</p>
<ul>
<li class=""><strong>Codebase / CodebaseBranch</strong> - your application (<code>test-go-app</code>) and a branch within it (<code>feature-tt-123</code>). Creating a branch in the Portal creates the <code>CodebaseBranch</code> resource and mirrors the branch into Git.</li>
<li class=""><strong>CodebaseImageStream (CBIS)</strong> - the record of container image tags built for a branch. The build pipeline writes new tags here; the CD side reads from it. This handoff is the key to ordering (more on that below).</li>
<li class=""><strong>Deployment (CDPipeline) and Environment (Stage)</strong> - the deployment flow and the individual environments within it. A Stage owns a namespace and an Argo CD Application.</li>
<li class=""><strong>Trigger type</strong> - <code>Manual</code>, <code>Auto</code>, or <code>Auto-stable</code>. We use <strong>Auto</strong>: the environment redeploys whenever a new image tag lands in the CBIS, so the preview env always tracks the branch head with no manual step. (See <a class="" href="https://docs.kuberocketci.io/docs/user-guide/auto-stable-trigger-type">Deployment Strategies</a>.)</li>
</ul>
<p>Here is the full path from a feature-branch build to a live, isolated workload:</p>
<!-- -->
<p>The crucial detail is the <strong>CBIS handoff</strong>: Tekton writes the image tag <em>after</em> the image is actually pushed, and the Auto trigger only fires once that tag exists. The image is therefore guaranteed to be present before Argo CD tries to deploy it - which avoids the <code>ImagePullBackOff</code> race that bare deploy-only tooling is prone to.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="baseline-the-local-testbed-starting-point">Baseline: The Local Testbed Starting Point<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#baseline-the-local-testbed-starting-point" class="hash-link" aria-label="Direct link to Baseline: The Local Testbed Starting Point" title="Direct link to Baseline: The Local Testbed Starting Point" translate="no">​</a></h2>
<p>The starting point is the end state of the <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">previous post</a>: the sample Go/Gin app <code>test-go-app</code> built from <code>main</code> and deployed by the <code>demo</code> Deployment into namespace <code>krci-demo-dev</code>.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl get applications </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAMESPACE   NAME                   SYNC STATUS   HEALTH STATUS</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci        demo-dev-test-go-app   Synced        Healthy</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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci-demo-dev get deploy test-go-app </span><span class="token parameter variable" style="color:#36acaa">-o</span><span class="token plain"> </span><span class="token assign-left variable" style="color:#36acaa">jsonpath</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">'{.spec.template.spec.containers[0].image}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">gitlab.127.0.0.1.nip.io:5050/krci/test-go-app:main-20260606-132413@sha256:a39d120b</span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">.</span><br></div></code></pre></div></div>
<p>One property of <code>main</code> matters for the rest of this post: the Gin app only handles <code>/hello</code>, so the root path returns 404. That 404 is our control - the feature environment will return 200 on the same path, and <code>main</code> will keep returning 404 the entire time.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># main, GET /         (no root handler on main)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ </span><span class="token function" style="color:#d73a49">curl</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-s</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-o</span><span class="token plain"> /dev/null </span><span class="token parameter variable" style="color:#36acaa">-w</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"%{http_code}</span><span class="token string entity" style="color:#36acaa">\n</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> http://localhost:18080/</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token number" style="color:#36acaa">404</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1-create-the-feature-branch-via-the-portal">Step 1: Create the Feature Branch via the Portal<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#step-1-create-the-feature-branch-via-the-portal" class="hash-link" aria-label="Direct link to Step 1: Create the Feature Branch via the Portal" title="Direct link to Step 1: Create the Feature Branch via the Portal" translate="no">​</a></h2>
<p>In the <code>test-go-app</code> component, <strong>Branches → Create branch</strong>. I name it <code>feature-tt-123</code>, leave <strong>From</strong> as <code>main</code>, and keep the default review and build pipelines.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal Create Branch dialog for test-go-app, creating feature-tt-123 from main with default review and build pipelines" src="https://docs.kuberocketci.io/assets/images/create-feature-branch-00bc67a73a38bf09854d5840af4df5cd.png" title="Creating a feature branch in the KubeRocketCI Portal" width="2880" height="1800" class="img_ev3q"></p>
<p>Clicking <strong>Create</strong> provisions a <code>CodebaseBranch</code> resource and mirrors the branch into GitLab with an (initially empty) CodebaseImageStream:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebasebranch,codebaseimagestream </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> feature</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">codebasebranch.v2.edp.epam.com/test-go-app-feature-tt-123-0afaf       created   feature-tt-123</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">codebaseimagestream.v2.edp.epam.com/test-go-app-feature-tt-123-0afaf  test-go-app</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2-make-a-change-that-only-the-preview-will-show">Step 2: Make a Change That Only the Preview Will Show<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#step-2-make-a-change-that-only-the-preview-will-show" class="hash-link" aria-label="Direct link to Step 2: Make a Change That Only the Preview Will Show" title="Direct link to Step 2: Make a Change That Only the Preview Will Show" translate="no">​</a></h2>
<p>To prove isolation, the feature branch needs to behave differently from <code>main</code>. I add a root <code>/</code> handler that returns <code>200</code> and echoes a <code>NAME</code> environment variable (we will set that variable per-environment in Step 7), plus the Helm <code>extraEnv</code> plumbing the override needs. The whole change is 30 lines across four files:</p>
<div class="language-go codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_OeMC">main.go (feature-tt-123)</div><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-go 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">	r </span><span class="token operator" style="color:#393A34">:=</span><span class="token plain"> gin</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Default</span><span class="token punctuation" style="color:#393A34">(</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">	r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GET</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">func</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">gin</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </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">		c</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">JSON</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">http</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">StatusOK</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> gin</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">H</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 string" style="color:#e3116c">"status"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"ok"</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 string" style="color:#e3116c">"name"</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">   os</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">Getenv</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"NAME"</span><span class="token punctuation" style="color:#393A34">)</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 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 punctuation" style="color:#393A34">)</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">	r</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#d73a49">GET</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#e3116c">"/hello"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">func</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">c </span><span class="token operator" style="color:#393A34">*</span><span class="token plain">gin</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">Context</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><br></div></code></pre></div></div>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_OeMC">deploy-templates/templates/deployment.yaml (feature-tt-123)</div><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 plain">          </span><span class="token key atrule" style="color:#00a4db">ports</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">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> http</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">              </span><span class="token key atrule" style="color:#00a4db">containerPort</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> .Values.service.port </span><span class="token punctuation" style="color:#393A34">}</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">protocol</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> TCP</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 punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> if .Values.extraEnv </span><span class="token punctuation" style="color:#393A34">}</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">env</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 punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> toYaml .Values.extraEnv </span><span class="token punctuation" style="color:#393A34">|</span><span class="token plain"> nindent 12 </span><span class="token punctuation" style="color:#393A34">}</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 punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> end </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>The default <code>extraEnv: {}</code> goes into <code>values.yaml</code>, and a matching unit test goes into <code>main_test.go</code>. Crucially, this change lives <strong>only on <code>feature-tt-123</code></strong> - <code>main</code> is never touched.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3-build-the-feature-branch">Step 3: Build the Feature Branch<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#step-3-build-the-feature-branch" class="hash-link" aria-label="Direct link to Step 3: Build the Feature Branch" title="Direct link to Step 3: Build the Feature Branch" translate="no">​</a></h2>
<p>Feature branches are built on demand: on the <strong>Branches</strong> tab, the <code>feature-tt-123</code> row has a <strong>Build</strong> button. One click starts the <code>gitlab-go-gin-app-build-default</code> pipeline.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal Branches tab showing feature-tt-123 with a successful build next to the main branch" src="https://docs.kuberocketci.io/assets/images/branches-after-build-22fd7a4fdd36af6ff78822f83d95906d.png" title="The feature branch built and ready, alongside main" width="2880" height="1800" class="img_ev3q"></p>
<p>The build is the familiar Tekton DAG - fetch, version, SonarQube quality gate, kaniko image build, Git tag, and the all-important <code>update-cbis</code> step that records the new tag:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI build pipeline DAG for feature-tt-123, all tasks green: fetch-repository, get-version, sonar, build, container-build, git-tag, update-cbis" src="https://docs.kuberocketci.io/assets/images/build-pipeline-dag-d156ccdec2c671b2677d20d6a5f42fba.png" title="The feature-branch build pipeline, all green" width="2880" height="1800" class="img_ev3q"></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get pipelinerun </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> build-test-go-app-feature-tt-123-0afaf</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">build-test-go-app-feature-tt-123-0afaf-2d31   True   Completed</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="reading-the-new-image-tag-from-the-codebaseimagestream">Reading the New Image Tag from the CodebaseImageStream<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#reading-the-new-image-tag-from-the-codebaseimagestream" class="hash-link" aria-label="Direct link to Reading the New Image Tag from the CodebaseImageStream" title="Direct link to Reading the New Image Tag from the CodebaseImageStream" translate="no">​</a></h3>
<p>With <strong>default</strong> <a class="" href="https://docs.kuberocketci.io/docs/user-guide/artifact-versioning">versioning</a>, the tag is <code>BRANCH-DATETIME</code>. The build writes it to the CBIS:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebaseimagestream test-go-app-feature-tt-123-0afaf </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 parameter variable" style="color:#36acaa">-o</span><span class="token plain"> </span><span class="token assign-left variable" style="color:#36acaa">jsonpath</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">'{.spec.tags[*].name}'</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">feature-tt-123-20260607-060023</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4-create-the-deployment-flow-and-auto-trigger-environment">Step 4: Create the Deployment Flow and Auto-Trigger Environment<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#step-4-create-the-deployment-flow-and-auto-trigger-environment" class="hash-link" aria-label="Direct link to Step 4: Create the Deployment Flow and Auto-Trigger Environment" title="Direct link to Step 4: Create the Deployment Flow and Auto-Trigger Environment" translate="no">​</a></h2>
<p>Now the environment. I follow the recommended convention of naming the deployment flow after my initials - <code>sk</code> - so personal preview flows are easy to find. In <strong>Deployments → Create Deployment</strong>, I select <code>test-go-app</code> and set its branch to <code>feature-tt-123</code>.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Create Deployment wizard with test-go-app selected and its branch set to feature-tt-123" src="https://docs.kuberocketci.io/assets/images/deployment-select-app-branch-502793ac9daabd5688071b9972fefe07.png" title="Selecting the application and the feature branch for the deployment flow" width="2880" height="1800" class="img_ev3q"></p>
<p>Then I add an environment named <code>feat</code> with trigger type <strong>Auto</strong>:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Create Environment wizard showing the Auto trigger type selected for automatic deployment" src="https://docs.kuberocketci.io/assets/images/environment-auto-trigger-5c5b4469b3a340617e1f085efeb91947.png" title="Choosing the Auto trigger type so the environment tracks the branch" width="2880" height="1800" class="img_ev3q"></p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="naming-conventions-how-krci-builds-the-namespace">Naming Conventions: How KRCI Builds the Namespace<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#naming-conventions-how-krci-builds-the-namespace" class="hash-link" aria-label="Direct link to Naming Conventions: How KRCI Builds the Namespace" title="Direct link to Naming Conventions: How KRCI Builds the Namespace" translate="no">​</a></h3>
<p>The namespace is derived as <code>krci-&lt;deployment&gt;-&lt;environment&gt;</code>, which the form pre-fills as <code>krci-sk-feat</code>. The environment overview confirms the wiring - Auto trigger, <code>in-cluster</code>, namespace <code>krci-sk-feat</code>, deploy and clean pipeline templates, quality gate Manual:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI environment overview for feat: Auto trigger, in-cluster, namespace krci-sk-feat, deploy and clean pipelines" src="https://docs.kuberocketci.io/assets/images/environment-overview-bbd920cac803ae968d81272ec78b72e8.png" title="The feat environment overview" width="2880" height="1800" class="img_ev3q"></p>
<p>Creating the environment <em>before</em> the next build is deliberate: with Auto, the deploy fires on a CBIS <em>update</em>. Arm the environment first, and the build that follows will deploy itself.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5-the-feature-image-auto-deploys">Step 5: The Feature Image Auto-Deploys<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#step-5-the-feature-image-auto-deploys" class="hash-link" aria-label="Direct link to Step 5: The Feature Image Auto-Deploys" title="Direct link to Step 5: The Feature Image Auto-Deploys" translate="no">​</a></h2>
<p>Triggering one more build updates the CBIS, the Auto trigger fires, and the <code>cd-pipeline-operator</code> creates the deploy run - no clicks. The deploy pipeline runs its four tasks and goes green in 47 seconds:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI auto-triggered deploy pipeline DAG for the feat environment: pre-deploy, deploy-app, post-deploy, promote-images, all green" src="https://docs.kuberocketci.io/assets/images/auto-deploy-pipeline-dag-a768a78478d4c919398659821c302a91.png" title="The Auto-triggered deploy pipeline for the preview environment" width="2880" height="1800" class="img_ev3q"></p>
<p>The whole preview environment now shows in one card - the <code>feat</code> Stage, its namespace, the deploy/clean pipelines, and the application <code>Healthy + Synced</code> on the feature tag:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Deployments view showing the sk deployment with the feat preview environment, namespace krci-sk-feat, and test-go-app Healthy and Synced" src="https://docs.kuberocketci.io/assets/images/preview-environment-card-12aac7b16d3c267ffdd789a543caa297.png" title="The live preview environment card" width="2880" height="1800" class="img_ev3q"></p>
<p>The cluster agrees:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get pipelinerun </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> sk-feat</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">deploy-sk-feat-pjsxx   True   Succeeded</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">$ kubectl get applications </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAMESPACE   NAME                  SYNC STATUS   HEALTH STATUS</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci        demo-dev-test-go-app  Synced        Healthy   </span><span class="token comment" style="color:#999988;font-style:italic"># main, untouched</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci        sk-feat-test-go-app   Synced        Healthy   </span><span class="token comment" style="color:#999988;font-style:italic"># the new preview env</span><span class="token plain"></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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci-sk-feat get deploy,pods</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                          READY   UP-TO-DATE   AVAILABLE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">deployment.apps/test-go-app   </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">/1     </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">            </span><span class="token number" style="color:#36acaa">1</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                               READY   STATUS</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pod/test-go-app-666f6c6994-rltdl   </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">/1     Running</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-6-proof-of-isolation---feature-gets-200-main-gets-404">Step 6: Proof of Isolation - Feature Gets 200, Main Gets 404<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#step-6-proof-of-isolation---feature-gets-200-main-gets-404" class="hash-link" aria-label="Direct link to Step 6: Proof of Isolation - Feature Gets 200, Main Gets 404" title="Direct link to Step 6: Proof of Isolation - Feature Gets 200, Main Gets 404" translate="no">​</a></h2>
<p>This is the part no one publishes with real output. Two namespaces, two image tags, two versions of the code, running side by side on the same cluster:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci-demo-dev get deploy test-go-app </span><span class="token parameter variable" style="color:#36acaa">-o</span><span class="token plain"> </span><span class="token assign-left variable" style="color:#36acaa">jsonpath</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">'{..image}'</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">.test-go-app:main-20260606-132413@sha256:a39d120b</span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">.        </span><span class="token comment" style="color:#999988;font-style:italic"># main</span><span class="token plain"></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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci-sk-feat get deploy test-go-app </span><span class="token parameter variable" style="color:#36acaa">-o</span><span class="token plain"> </span><span class="token assign-left variable" style="color:#36acaa">jsonpath</span><span class="token operator" style="color:#393A34">=</span><span class="token string" style="color:#e3116c">'{..image}'</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">.test-go-app:feature-tt-123-20260607-060023@sha256:812a57de</span><span class="token punctuation" style="color:#393A34">..</span><span class="token plain">.  </span><span class="token comment" style="color:#999988;font-style:italic"># feature</span><br></div></code></pre></div></div>
<p>And the behavioral proof - same path, two answers:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># feature env (krci-sk-feat)  -&gt;  GET /</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">HTTP/1.1 </span><span class="token number" style="color:#36acaa">200</span><span class="token plain"> OK</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 string" style="color:#e3116c">"name"</span><span class="token builtin class-name">:</span><span class="token string" style="color:#e3116c">""</span><span class="token plain">,</span><span class="token string" style="color:#e3116c">"status"</span><span class="token builtin class-name">:</span><span class="token string" style="color:#e3116c">"ok"</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" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># main env (krci-demo-dev)    -&gt;  GET /</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">HTTP/1.1 </span><span class="token number" style="color:#36acaa">404</span><span class="token plain"> Not Found</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"></span><span class="token comment" style="color:#999988;font-style:italic"># sanity: both still serve /hello</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">feature  /hello -</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">200</span><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"message"</span><span class="token builtin class-name">:</span><span class="token string" style="color:#e3116c">"Hello, World!"</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">main     /hello -</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">200</span><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">{</span><span class="token string" style="color:#e3116c">"message"</span><span class="token builtin class-name">:</span><span class="token string" style="color:#e3116c">"Hello, World!"</span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<p>The feature branch's new code is live in <code>krci-sk-feat</code> and answers <code>200</code>. The exact same request to <code>main</code> in <code>krci-demo-dev</code> still returns <code>404</code>. Nothing on <code>main</code> changed. That is the entire promise of a preview environment, demonstrated rather than asserted.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-7-per-environment-config-via-gitops-values-override">Step 7: Per-Environment Config via GitOps Values Override<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#step-7-per-environment-config-via-gitops-values-override" class="hash-link" aria-label="Direct link to Step 7: Per-Environment Config via GitOps Values Override" title="Direct link to Step 7: Per-Environment Config via GitOps Values Override" translate="no">​</a></h2>
<p>Preview environments usually need their own configuration - a different feature flag, API endpoint, or, here, a <code>NAME</code> variable. The GitOps way to do this is a values file in the <a class="" href="https://docs.kuberocketci.io/docs/user-guide/gitops">GitOps repository</a> at the path <code>&lt;deployment&gt;/&lt;environment&gt;/&lt;application&gt;-values.yaml</code>, with <strong>Values override</strong> enabled for the environment.</p>
<div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockTitle_OeMC">sk/feat/test-go-app-values.yaml (in the GitOps repo)</div><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">extraEnv</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">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> NAME</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">value</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Hello from the sk/feat preview environment"</span><br></div></code></pre></div></div>
<p>On the environment's <strong>Applications</strong> tab, I open <strong>Configure Deploy</strong>, flip the per-app <strong>Values override</strong> toggle, and <strong>Start Deploy</strong>:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Configure Deploy view with the Values override toggle enabled for test-go-app and the Start Deploy button" src="https://docs.kuberocketci.io/assets/images/values-override-enabled-a20c53cc852ac95f50839f30c4197b45.png" title="Enabling per-environment Values override before deploying" width="2880" height="1800" class="img_ev3q"></p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get pipelinerun </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> sk-feat</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">deploy-sk-feat-pjsxx   True   Succeeded</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">deploy-sk-feat-ec0e    True   Succeeded</span><br></div></code></pre></div></div>
<p>KubeRocketCI pulls the values file from the GitOps repo and applies it to the Argo CD Application at sync time - no image rebuild required. The pod picks up the variable, and because our root handler echoes it, the override is now visible over HTTP too:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci-sk-feat </span><span class="token builtin class-name">exec</span><span class="token plain"> deploy/test-go-app -- </span><span class="token function" style="color:#d73a49">env</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> ^NAME</span><span class="token operator" 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 assign-left variable" style="color:#36acaa">NAME</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">Hello from the sk/feat preview environment</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"></span><span class="token comment" style="color:#999988;font-style:italic"># feature env GET /  (now reflects the per-environment value)</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 string" style="color:#e3116c">"name"</span><span class="token builtin class-name">:</span><span class="token string" style="color:#e3116c">"Hello from the sk/feat preview environment"</span><span class="token plain">,</span><span class="token string" style="color:#e3116c">"status"</span><span class="token builtin class-name">:</span><span class="token string" style="color:#e3116c">"ok"</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" style="display:inline-block"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># main pod has no such variable</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci-demo-dev </span><span class="token builtin class-name">exec</span><span class="token plain"> deploy/test-go-app -- </span><span class="token function" style="color:#d73a49">env</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> ^NAME</span><span class="token operator" 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">no output</span><span class="token punctuation" style="color:#393A34">)</span><br></div></code></pre></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-8-destroy-the-environment---namespace-gone-zero-residual-cost">Step 8: Destroy the Environment - Namespace Gone, Zero Residual Cost<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#step-8-destroy-the-environment---namespace-gone-zero-residual-cost" class="hash-link" aria-label="Direct link to Step 8: Destroy the Environment - Namespace Gone, Zero Residual Cost" title="Direct link to Step 8: Destroy the Environment - Namespace Gone, Zero Residual Cost" translate="no">​</a></h2>
<p>Teardown is a first-class action, not a side effect of closing a PR. On the deployment, <strong>Actions → Delete</strong> asks me to type the name to confirm:</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI confirm-deletion dialog for the sk deployment, requiring the name to be typed to delete the deployment and all its environments" src="https://docs.kuberocketci.io/assets/images/destroy-environment-confirm-469453922b282df210d0298292b2b6aa.png" title="Destroying the preview environment from the Portal" width="2880" height="1800" class="img_ev3q"></p>
<p>The <code>cd-pipeline-operator</code> deletes the namespace and its Argo CD Application. In about 5 seconds the preview environment is gone - and the <code>main</code> baseline is exactly as it was:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl get ns krci-sk-feat</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Error from server </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">NotFound</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain">: namespaces </span><span class="token string" style="color:#e3116c">"krci-sk-feat"</span><span class="token plain"> not found</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">$ kubectl get applications </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAMESPACE   NAME                   SYNC STATUS   HEALTH STATUS</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci        demo-dev-test-go-app   Synced        Healthy        </span><span class="token comment" style="color:#999988;font-style:italic"># baseline intact</span><span class="token plain"></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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci-demo-dev get deploy test-go-app</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME          READY   UP-TO-DATE   AVAILABLE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app   </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">/1     </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">            </span><span class="token number" style="color:#36acaa">1</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="cleanup-checklist-stage-branch-gitops-entry-are-independent">Cleanup Checklist: Stage, Branch, GitOps Entry Are Independent<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#cleanup-checklist-stage-branch-gitops-entry-are-independent" class="hash-link" aria-label="Direct link to Cleanup Checklist: Stage, Branch, GitOps Entry Are Independent" title="Direct link to Cleanup Checklist: Stage, Branch, GitOps Entry Are Independent" translate="no">​</a></h3>
<p>One thing to know: destroying the Stage does <strong>not</strong> delete the branch.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get codebasebranch </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">grep</span><span class="token plain"> feature</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">test-go-app-feature-tt-123-0afaf   created   feature-tt-123     </span><span class="token comment" style="color:#999988;font-style:italic"># still here</span><br></div></code></pre></div></div>
<p>The Stage, the <code>CodebaseBranch</code>, the Git branch, and the GitOps values file are independent lifecycle objects. To fully clean up after a feature, delete the branch on the <strong>Branches</strong> tab and remove the <code>&lt;deployment&gt;/&lt;environment&gt;/</code> entry from the GitOps repo. That independence is a feature: you can spin a preview environment up and down repeatedly for the same branch without ever touching Git history.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kuberocketci-vs-other-preview-environment-approaches">KubeRocketCI vs. Other Preview Environment Approaches<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#kuberocketci-vs-other-preview-environment-approaches" class="hash-link" aria-label="Direct link to KubeRocketCI vs. Other Preview Environment Approaches" title="Direct link to KubeRocketCI vs. Other Preview Environment Approaches" translate="no">​</a></h2>
<table><thead><tr><th>Capability</th><th>KubeRocketCI</th><th>Argo CD ApplicationSet (PR generator)</th><th>Uffizzi</th><th>Okteto / Qovery / Vercel (SaaS)</th></tr></thead><tbody><tr><td>License</td><td>Apache-2.0</td><td>Apache-2.0</td><td>Proprietary (SaaS only since 2024)</td><td>Proprietary / freemium</td></tr><tr><td>Self-hosted</td><td>Yes</td><td>Yes</td><td>No</td><td>Control plane is SaaS</td></tr><tr><td>Builds the image</td><td>Yes (Tekton)</td><td><strong>No</strong> - deploy only</td><td>Yes</td><td>Yes</td></tr><tr><td>GitOps CD</td><td>Yes (Argo CD)</td><td>Yes (Argo CD)</td><td>No (Compose/Helm)</td><td>Varies</td></tr><tr><td>Developer Portal UI</td><td>Yes</td><td>No (YAML)</td><td>Limited</td><td>Yes</td></tr><tr><td>Per-branch namespace</td><td>Yes</td><td>Yes</td><td>Yes</td><td>Yes</td></tr><tr><td>Per-env values override</td><td>Yes (GitOps)</td><td>Manual</td><td>Limited</td><td>Yes</td></tr><tr><td>One-click destroy</td><td>Yes</td><td>Manual</td><td>Yes</td><td>Yes</td></tr><tr><td>Cost per environment</td><td>Compute only</td><td>Compute only</td><td>Compute only</td><td>Per-env / per-seat fees</td></tr></tbody></table>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-argo-cd-applicationset-pr-generator-powerful-but-incomplete">The Argo CD ApplicationSet PR Generator: Powerful, but Incomplete<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#the-argo-cd-applicationset-pr-generator-powerful-but-incomplete" class="hash-link" aria-label="Direct link to The Argo CD ApplicationSet PR Generator: Powerful, but Incomplete" title="Direct link to The Argo CD ApplicationSet PR Generator: Powerful, but Incomplete" translate="no">​</a></h3>
<p>The Argo CD ApplicationSet <strong>Pull Request generator</strong> is the closest pure-OSS pattern, and it is genuinely useful - it watches open PRs and creates an Argo CD Application for each. But it is a <em>deployment</em> tool only: <strong>it does not build container images</strong>. The CI pipeline must build and push the tagged image first, and if the ApplicationSet reconciles before the image exists, the pods hit <code>ImagePullBackOff</code>. KubeRocketCI sidesteps that ordering problem with the CodebaseImageStream handoff described earlier - Tekton writes the tag, and the Auto trigger only fires once it is there. You also get a Portal, a Build button, and a Destroy button instead of hand-written <code>ApplicationSet</code> YAML.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-saas-preview-tools-dont-cover-full-stack-kubernetes">Why SaaS Preview Tools Don't Cover Full-Stack Kubernetes<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#why-saas-preview-tools-dont-cover-full-stack-kubernetes" class="hash-link" aria-label="Direct link to Why SaaS Preview Tools Don't Cover Full-Stack Kubernetes" title="Direct link to Why SaaS Preview Tools Don't Cover Full-Stack Kubernetes" translate="no">​</a></h3>
<p>Vercel and Netlify nail preview deployments for frontend/static workloads, and Qovery/Okteto/Bunnyshell offer polished managed experiences - but they put a SaaS control plane (and a per-environment or per-seat bill) between you and your cluster. Demand is real, but the tooling still frustrates teams: in <a href="https://www.port.io/state-of-internal-developer-portals" target="_blank" rel="noopener noreferrer" class="">Port's 2025 State of Internal Developer Portals survey</a>, just <strong>6%</strong> of developers were satisfied with the self-service tooling they use to spin up environments on demand. The open question that leaves is how to get that developer experience <strong>on your own cluster, with no SaaS bill</strong> - exactly the gap an Apache-2.0 IDP like KubeRocketCI fills.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="frequently-asked-questions">Frequently Asked Questions<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#frequently-asked-questions" class="hash-link" aria-label="Direct link to Frequently Asked Questions" title="Direct link to Frequently Asked Questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-the-difference-between-a-preview-environment-and-an-ephemeral-environment">What is the difference between a preview environment and an ephemeral environment?<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#what-is-the-difference-between-a-preview-environment-and-an-ephemeral-environment" class="hash-link" aria-label="Direct link to What is the difference between a preview environment and an ephemeral environment?" title="Direct link to What is the difference between a preview environment and an ephemeral environment?" translate="no">​</a></h3>
<p>They are used interchangeably. "Preview environment" emphasizes the use case - reviewing a feature branch before merge - while "ephemeral environment" emphasizes the lifecycle: it is temporary and torn down when done. In KubeRocketCI both refer to the same object: a Deployment Stage with its own isolated Kubernetes namespace, created for a branch and destroyed on demand.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="do-i-need-a-saas-subscription-to-get-preview-environments-on-kubernetes">Do I need a SaaS subscription to get preview environments on Kubernetes?<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#do-i-need-a-saas-subscription-to-get-preview-environments-on-kubernetes" class="hash-link" aria-label="Direct link to Do I need a SaaS subscription to get preview environments on Kubernetes?" title="Direct link to Do I need a SaaS subscription to get preview environments on Kubernetes?" translate="no">​</a></h3>
<p>No. KubeRocketCI is Apache-2.0 open source and runs entirely on your own cluster - kind locally, or EKS/GKE/AKS in production. There is no per-environment fee and no SaaS control plane. You bring the cluster; KRCI provides the Portal, CI (Tekton), and GitOps CD (Argo CD) to manage the full preview-environment lifecycle.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-setting-up-ephemeral-environments-require-yaml-expertise">Does setting up ephemeral environments require YAML expertise?<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#does-setting-up-ephemeral-environments-require-yaml-expertise" class="hash-link" aria-label="Direct link to Does setting up ephemeral environments require YAML expertise?" title="Direct link to Does setting up ephemeral environments require YAML expertise?" translate="no">​</a></h3>
<p>Not for the day-to-day flow. Creating a branch, building it, and deploying to an isolated namespace are all Portal form actions. The only YAML you write is the optional per-environment values file in the GitOps repo - and only when you actually need to override a value for a specific preview. The CDPipeline and Argo CD Application resources are managed by KRCI operators.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-does-per-environment-configuration-override-work-in-a-gitops-workflow">How does per-environment configuration override work in a GitOps workflow?<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#how-does-per-environment-configuration-override-work-in-a-gitops-workflow" class="hash-link" aria-label="Direct link to How does per-environment configuration override work in a GitOps workflow?" title="Direct link to How does per-environment configuration override work in a GitOps workflow?" translate="no">​</a></h3>
<p>You add a file at <code>&lt;deployment&gt;/&lt;environment&gt;/&lt;application&gt;-values.yaml</code> in the GitOps repository - for example <code>sk/feat/test-go-app-values.yaml</code> - containing Helm values that override the application's defaults. Enable the <strong>Values override</strong> toggle on the environment, then deploy. KubeRocketCI applies those values to the Argo CD Application at sync time, so the pod reflects them without rebuilding the image.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-happens-to-the-namespace-when-i-delete-the-environment-in-krci">What happens to the namespace when I delete the environment in KRCI?<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#what-happens-to-the-namespace-when-i-delete-the-environment-in-krci" class="hash-link" aria-label="Direct link to What happens to the namespace when I delete the environment in KRCI?" title="Direct link to What happens to the namespace when I delete the environment in KRCI?" translate="no">​</a></h3>
<p>The <code>cd-pipeline-operator</code> deletes the Kubernetes namespace and the associated Argo CD Application - all pods, services, and configmaps in that namespace are removed. The <code>CodebaseBranch</code> and the Git branch are separate objects, so destroying the Stage does not delete or merge the branch; clean those up separately if you want to.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-the-argo-cd-applicationset-pull-request-generator-build-container-images">Does the Argo CD ApplicationSet Pull Request generator build container images?<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#does-the-argo-cd-applicationset-pull-request-generator-build-container-images" class="hash-link" aria-label="Direct link to Does the Argo CD ApplicationSet Pull Request generator build container images?" title="Direct link to Does the Argo CD ApplicationSet Pull Request generator build container images?" translate="no">​</a></h3>
<p>No. It is a deployment tool that watches open pull requests and creates Argo CD Applications for them. Your CI pipeline (Tekton, GitHub Actions, GitLab CI) must build and push the image first. If the ApplicationSet fires before the image is ready, pods hit <code>ImagePullBackOff</code>. KubeRocketCI avoids this with the CodebaseImageStream handoff: Tekton writes the tag, and the Auto trigger deploys only after it exists.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-much-does-it-cost-to-run-ephemeral-environments-on-kubernetes-with-kuberocketci">How much does it cost to run ephemeral environments on Kubernetes with KubeRocketCI?<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#how-much-does-it-cost-to-run-ephemeral-environments-on-kubernetes-with-kuberocketci" class="hash-link" aria-label="Direct link to How much does it cost to run ephemeral environments on Kubernetes with KubeRocketCI?" title="Direct link to How much does it cost to run ephemeral environments on Kubernetes with KubeRocketCI?" translate="no">​</a></h3>
<p>The software cost is zero - KubeRocketCI is Apache-2.0 with no per-environment fee. The only cost is the compute the environment consumes while it runs, and because preview environments are destroyed when the branch work is done, they do not accumulate idle compute the way permanent staging environments do. On the local kind testbed in this post, the preview environment cost nothing beyond the Docker Desktop resources already allocated.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://docs.kuberocketci.io/blog/ephemeral-preview-environments-kubernetes-feature-branch#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<p>Starting from a stable <code>main</code> deployment, we created a feature branch in the Portal, shipped a change visible only on that branch, built it through Tekton, and let an <strong>Auto</strong> environment deploy it to its own isolated namespace through Argo CD GitOps. We proved the isolation with real output - <code>feature</code> returns <code>200</code>, <code>main</code> keeps returning <code>404</code> - injected per-environment config through a GitOps values override, and then destroyed the environment in one action, leaving the baseline untouched and zero residual cost behind.</p>
<p>No SaaS bill, no hand-written <code>ApplicationSet</code> YAML, no <code>kubectl</code> required for the happy path - just a feature branch, a few portal clicks, and a real isolated Kubernetes environment that cleans up after itself. The pattern - feature branch deployment on Kubernetes with isolated namespaces - is entirely self-hosted and free. I ran the entire flow on June 7, 2026 on the local <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">try-kuberocketci</a> testbed; every screenshot and command output above is from that run.</p>
<p>The most useful next steps from here:</p>
<ul>
<li class="">Spin up the testbed yourself with <a class="" href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally">Try KubeRocketCI Locally</a>, then follow this flow on your own machine.</li>
<li class="">Read the <a class="" href="https://docs.kuberocketci.io/docs/use-cases/deploy-application-from-feature-branch">Deploy Application From a Feature Branch</a> use case and <a class="" href="https://docs.kuberocketci.io/docs/user-guide/manage-environments">Manage Deployments</a> for the full reference.</li>
<li class="">Explore <a class="" href="https://docs.kuberocketci.io/docs/user-guide/auto-stable-trigger-type">Deployment Strategies</a> (Manual, Auto, Auto-stable) and the <a class="" href="https://docs.kuberocketci.io/docs/user-guide/argo-cd-preview">Argo CD Diff preview</a> for gated promotions.</li>
<li class="">See the deeper <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">Kubernetes-native CI/CD with Tekton</a> architecture and the <a class="" href="https://docs.kuberocketci.io/blog/krci-cli-daily-use">krci CLI</a> for driving all of this from the terminal.</li>
</ul>
<p>KubeRocketCI is open source under Apache License 2.0. The platform, Helm charts, and the testbed are all on <a href="https://github.com/KubeRocketCI/try-kuberocketci" target="_blank" rel="noopener noreferrer" class="">GitHub</a>.</p>
]]></content>
        <author>
            <name>Sergiy Kulanov</name>
            <uri>https://github.com/sergk</uri>
        </author>
        <category label="KubeRocketCI" term="KubeRocketCI"/>
        <category label="Preview Environments" term="Preview Environments"/>
        <category label="Ephemeral Environments" term="Ephemeral Environments"/>
        <category label="GitOps" term="GitOps"/>
        <category label="Argo CD" term="Argo CD"/>
        <category label="Tekton" term="Tekton"/>
        <category label="Kubernetes" term="Kubernetes"/>
        <category label="Feature Branch" term="Feature Branch"/>
        <category label="CD" term="CD"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="CI/CD" term="CI/CD"/>
        <category label="Open Source" term="Open Source"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Try KubeRocketCI Locally in 2 Commands]]></title>
        <id>https://docs.kuberocketci.io/blog/try-kuberocketci-locally</id>
        <link href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally"/>
        <updated>2026-06-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Spin up the full KubeRocketCI CI/CD platform on a local kind cluster in 2 commands. No cloud account needed. Try it in under 35 minutes.]]></summary>
        <content type="html"><![CDATA[<p>Evaluating an internal developer platform without a working instance is like buying a car from a brochure. Every KubeRocketCI install path in the official docs assumes a cluster you already have - AWS EKS, GKE, an on-prem control plane. Today I ran the <a href="https://github.com/KubeRocketCI/try-kuberocketci" target="_blank" rel="noopener noreferrer" class="">try-kuberocketci testbed</a> end-to-end on my Apple Silicon Mac using Docker Desktop and two commands: <strong><code>make testbed</code></strong> (approximately 18–20 minutes) and <strong><code>make e2e</code></strong> (approximately 12 minutes). The result is a fully wired KubeRocketCI local install - Tekton, Argo CD, SonarQube, self-hosted GitLab CE, Prometheus, Grafana, Tekton Results, and the Portal - running in a disposable <a href="https://kind.sigs.k8s.io/" target="_blank" rel="noopener noreferrer" class="">kind</a> cluster. No cloud account. No <code>/etc/hosts</code> edits. No clicking through UIs to trigger pipelines. This post walks through exactly what happened, command by command, screenshot by screenshot.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-kuberocketci">What is KubeRocketCI?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#what-is-kuberocketci" class="hash-link" aria-label="Direct link to What is KubeRocketCI?" title="Direct link to What is KubeRocketCI?" translate="no">​</a></h2>
<p><a class="" href="https://docs.kuberocketci.io/docs/about-platform">KubeRocketCI</a> (KRCI) is an open-source, Kubernetes-native internal developer platform for cloud-native CI/CD, developed and maintained under Apache 2.0. It assembles <a href="https://tekton.dev/" target="_blank" rel="noopener noreferrer" class="">Tekton</a>, <a href="https://argo-cd.readthedocs.io/" target="_blank" rel="noopener noreferrer" class="">Argo CD</a>, SonarQube, and your Git provider into a cohesive, opinionated developer experience - managing the lifecycle of your Codebases (applications, libraries, autotests) from source through review, build, and GitOps-based deployment, exposed through a single Portal UI.</p>
<p>KubeRocketCI markets itself as cutting time-from-project-initiation-to-active-development from days to hours. The testbed lets you verify that claim in 30 minutes on a laptop. On our own dogfooding cluster over a recent 90-day window, the platform logged <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">18,397 pipeline runs with a 93% build success rate</a> - and it runs the same Tekton pipeline stack you are about to stand up locally.</p>
<p>This repository is not KubeRocketCI itself - it is a local installer and test harness that brings the whole platform up on your machine, ready for evaluation, development, and demo preparation.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="prerequisites-and-hardware-requirements">Prerequisites and Hardware Requirements<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#prerequisites-and-hardware-requirements" class="hash-link" aria-label="Direct link to Prerequisites and Hardware Requirements" title="Direct link to Prerequisites and Hardware Requirements" translate="no">​</a></h2>
<p>To run a KubeRocketCI Docker Desktop setup, you need only Docker Desktop - no cloud account, no pre-existing Kubernetes cluster, and no registry account are required.</p>
<table><thead><tr><th>Requirement</th><th>Minimum</th><th>Recommended</th><th>Notes</th></tr></thead><tbody><tr><td>Docker Desktop</td><td>4.x+</td><td>Latest</td><td>Allocate RAM in Docker Settings → Resources</td></tr><tr><td>RAM allocation</td><td>8 GB (core only)</td><td>12 GB+</td><td>Full bed with GitLab + SonarQube needs 12 GB+</td></tr><tr><td>Disk space</td><td>~20 GB free</td><td>~30 GB</td><td>Container images and volumes</td></tr><tr><td>OS</td><td>macOS, Linux, WSL2</td><td>macOS/Linux</td><td>Windows via WSL2 supported</td></tr><tr><td>Apple Silicon</td><td>Supported</td><td>-</td><td>amd64 images run via Docker Desktop Rosetta 2</td></tr><tr><td><code>make</code></td><td>Any version</td><td>-</td><td>Pre-installed on macOS/Linux</td></tr><tr><td><code>kind</code>, <code>helm</code>, <code>kubectl</code></td><td>Latest stable</td><td>-</td><td><code>make tools</code> installs <code>kind</code> via Homebrew</td></tr><tr><td>Internet access</td><td>Required</td><td>-</td><td>Images pulled from public registries during install</td></tr></tbody></table>
<div class="theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"></path></svg></span>RAM is the most common failure mode</div><div class="admonitionContent_BuS1"><p>Docker Desktop with <strong>at least 12 GB of RAM allocated</strong> is required for the full bed. Below this threshold, GitLab CE or SonarQube will OOM-kill during startup. My live run used ~11.7 GB allocated to the Docker engine - right at the edge, and it completed successfully, but the README's 12 GB+ recommendation is well-founded. Check Docker Settings → Resources → Memory before running <code>make testbed</code>.</p></div></div>
<p>On Apple Silicon (M1/M2/M3), Docker Desktop's Rosetta 2 emulation runs the amd64-only images (Portal, GitLab, sonar-operator) transparently. No configuration changes are needed in the testbed - it just works.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-to-try-kuberocketci-locally-the-full-stack-you-get">How to Try KubeRocketCI Locally: The Full Stack You Get<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#how-to-try-kuberocketci-locally-the-full-stack-you-get" class="hash-link" aria-label="Direct link to How to Try KubeRocketCI Locally: The Full Stack You Get" title="Direct link to How to Try KubeRocketCI Locally: The Full Stack You Get" translate="no">​</a></h2>
<p>The fastest way to try KubeRocketCI locally is two commands on Docker Desktop. The try-kuberocketci testbed spins up a KubeRocketCI kind cluster with the full CI/CD platform - including Tekton, Argo CD, Prometheus/Grafana, Tekton Results, SonarQube, and a self-hosted GitLab CE - in two commands: <code>make testbed</code> (approximately 18–20 minutes) and <code>make e2e</code> (approximately 12 minutes). All versions are pinned through the <a href="https://github.com/epam/edp-cluster-add-ons" target="_blank" rel="noopener noreferrer" class="">edp-cluster-add-ons</a> GitOps repository - the same source of truth used in production KubeRocketCI deployments - making the local testbed a faithful reproduction of a real cluster rather than a stripped-down demo.</p>
<table><thead><tr><th>Step</th><th>Layer</th><th>Component</th><th>Version</th><th>Namespace</th></tr></thead><tbody><tr><td>1</td><td>Cluster</td><td>kind (Kubernetes-in-Docker)</td><td>v1.36.1 (kind default; node image not pinned)</td><td>-</td></tr><tr><td>2</td><td>Ingress</td><td>ingress-nginx</td><td>controller-v1.11.3</td><td>ingress-nginx</td></tr><tr><td>3</td><td>Certs</td><td>cert-manager</td><td>v1.16.2</td><td>cert-manager</td></tr><tr><td>4</td><td>CI engine</td><td><a href="https://tekton.dev/" target="_blank" rel="noopener noreferrer" class="">Tekton</a> Pipelines / Triggers</td><td>v1.6.0 / v0.34.0</td><td>tekton-pipelines</td></tr><tr><td>5</td><td>CD engine</td><td><a href="https://argo-cd.readthedocs.io/" target="_blank" rel="noopener noreferrer" class="">Argo CD</a></td><td>chart 9.5.17</td><td>argocd</td></tr><tr><td>6</td><td>Observability</td><td>kube-prometheus-stack + Grafana</td><td>84.5.0</td><td>monitoring</td></tr><tr><td>7</td><td>Run storage</td><td>Tekton Results</td><td>v0.17.2</td><td>tekton-pipelines</td></tr><tr><td>8</td><td>Code quality</td><td>SonarQube Community + sonar-operator 3.3.0</td><td>2025.3.1 (chart) / 25.5-community</td><td>sonar</td></tr><tr><td>9</td><td>SCM + Registry</td><td>GitLab CE + bundled Container Registry</td><td>17.5.1-ce</td><td>gitlab</td></tr><tr><td>10</td><td>Platform</td><td>KubeRocketCI (edp-install)</td><td><strong>3.14.0</strong></td><td>krci</td></tr></tbody></table>
<p><strong>Why is KubeRocketCI installed last?</strong> The edp-install chart renders provider resources - the GitServer CRD, EventListener, Ingress rules - at install time. If GitLab or Argo CD is not already running when the chart applies, the operator's SSH connection check fails and the platform never reaches a healthy state. Installing KubeRocketCI last, after every dependency is ready, is what makes the chart wire itself correctly on first reconcile.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1---clone-the-repo-and-spin-up-the-testbed">Step 1 - Clone the Repo and Spin Up the Testbed<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#step-1---clone-the-repo-and-spin-up-the-testbed" class="hash-link" aria-label="Direct link to Step 1 - Clone the Repo and Spin Up the Testbed" title="Direct link to Step 1 - Clone the Repo and Spin Up the Testbed" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">git</span><span class="token plain"> clone https://github.com/KubeRocketCI/try-kuberocketci</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">cd</span><span class="token plain"> try-kuberocketci</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#d73a49">make</span><span class="token plain"> testbed   </span><span class="token comment" style="color:#999988;font-style:italic"># ~18-20 min</span><br></div></code></pre></div></div>
<p>That is the entire KubeRocketCI local install. One clone, one command.</p>
<div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>Check Docker Desktop RAM allocation first</div><div class="admonitionContent_BuS1"><p>Before running <code>make testbed</code>, open Docker Desktop → Settings → Resources and verify the memory slider is set to 12 GB or higher. This is the single most common reason the install fails, and it takes 30 seconds to check.</p></div></div>
<p><code>make testbed</code> builds in strict dependency order, installing KubeRocketCI last. The sequence:</p>
<!-- -->
<p>GitLab CE is the slowest component to initialize - plan for it to take several minutes of the total. Once <code>make testbed</code> completes, run <code>make status</code> to see the live cluster state and print all service URLs with credentials:</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">NAME                 STATUS   ROLES           AGE   VERSION</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci-control-plane   Ready    control-plane   13m   v1.36.1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">--- krci pods ---</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                                    READY   STATUS    RESTARTS   AGE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">cd-pipeline-operator-57b96f56ff-x7nlw   1/1     Running   0          84s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">codebase-operator-68b7599446-h8cn7      1/1     Running   0          41s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">el-edp-gitlab-6cfc7857d9-fnxxd          1/1     Running   0          84s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">gitfusion-86cb475dc9-mdbgb              1/1     Running   0          84s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci-portal-65956587d5-6tvtf            1/1     Running   0          31s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tekton-cache-89846975f-86fx8            1/1     Running   0          84s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">tekton-interceptor-6c68789887-88z86     1/1     Running   0          84s</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">================ Tool URLs &amp; credentials (local only) ================</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Argo CD UI:     http://argocd.127.0.0.1.nip.io</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  SonarQube UI:   http://sonar.127.0.0.1.nip.io</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  GitLab UI:      https://gitlab.127.0.0.1.nip.io</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Results API:    http://tekton-results.127.0.0.1.nip.io</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Grafana UI:     http://grafana.127.0.0.1.nip.io</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  Portal UI:      https://portal.127.0.0.1.nip.io</span><br></div></code></pre></div></div>
<p>All six services are immediately reachable in a browser. No <code>/etc/hosts</code> edits - I will explain why in the next section.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="nipio-wildcard-dns-no-etchosts-editing-required">nip.io Wildcard DNS: No /etc/hosts Editing Required<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#nipio-wildcard-dns-no-etchosts-editing-required" class="hash-link" aria-label="Direct link to nip.io Wildcard DNS: No /etc/hosts Editing Required" title="Direct link to nip.io Wildcard DNS: No /etc/hosts Editing Required" translate="no">​</a></h2>
<p>The try-kuberocketci testbed uses <a href="https://nip.io/" target="_blank" rel="noopener noreferrer" class="">nip.io</a> wildcard DNS so every platform service gets a stable, browser-accessible URL - no <code>/etc/hosts</code> edits, no port-forwards, no local DNS configuration required. Any subdomain of <code>&lt;IP&gt;.nip.io</code> resolves to <code>&lt;IP&gt;</code> via a public DNS server; the testbed generates all ingress hostnames in the <code>*.127.0.0.1.nip.io</code> pattern, so every service is immediately reachable from your browser with zero local DNS configuration.</p>
<p>Every competing local Kubernetes tutorial either requires manual <code>/etc/hosts</code> edits to map ingress hostnames to <code>127.0.0.1</code>, or skips DNS entirely and uses port-forwards. For a KubeRocketCI Docker Desktop setup that serves six platform UIs simultaneously, manual host-file management would be a non-starter - nip.io eliminates the entire problem class.</p>
<table><thead><tr><th>Service</th><th>URL</th></tr></thead><tbody><tr><td>KubeRocketCI Portal</td><td><code>https://portal.127.0.0.1.nip.io</code></td></tr><tr><td>Argo CD</td><td><code>http://argocd.127.0.0.1.nip.io</code></td></tr><tr><td>GitLab CE</td><td><code>https://gitlab.127.0.0.1.nip.io</code></td></tr><tr><td>SonarQube</td><td><code>http://sonar.127.0.0.1.nip.io</code></td></tr><tr><td>Grafana</td><td><code>http://grafana.127.0.0.1.nip.io</code></td></tr><tr><td>Tekton Results API</td><td><code>http://tekton-results.127.0.0.1.nip.io</code></td></tr></tbody></table>
<p>The DNS trick works for your browser on the host. Inside the cluster, pods resolve <code>gitlab.127.0.0.1.nip.io</code> to the in-cluster GitLab Service via CoreDNS rewrites, and the kind node's containerd uses a registry mirror config to pull images from GitLab's bundled registry. This split-horizon DNS design is one of the more interesting pieces of the testbed architecture - the full walkthrough is in <a href="https://github.com/KubeRocketCI/try-kuberocketci/blob/main/docs/architecture.md" target="_blank" rel="noopener noreferrer" class="">docs/architecture.md</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2---run-the-end-to-end-pipeline">Step 2 - Run the End-to-End Pipeline<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#step-2---run-the-end-to-end-pipeline" class="hash-link" aria-label="Direct link to Step 2 - Run the End-to-End Pipeline" title="Direct link to Step 2 - Run the End-to-End Pipeline" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">make</span><span class="token plain"> e2e   </span><span class="token comment" style="color:#999988;font-style:italic"># ~12 min</span><br></div></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-does-make-e2e-run-the-full-cicd-pipeline-automatically">How Does <code>make e2e</code> Run the Full CI/CD Pipeline Automatically?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#how-does-make-e2e-run-the-full-cicd-pipeline-automatically" class="hash-link" aria-label="Direct link to how-does-make-e2e-run-the-full-cicd-pipeline-automatically" title="Direct link to how-does-make-e2e-run-the-full-cicd-pipeline-automatically" translate="no">​</a></h3>
<p><code>make e2e</code> automates the complete CI/CD cycle - opening a GitLab Merge Request, triggering a Tekton review pipeline, merging the MR, building and pushing a container image with kaniko to GitLab's bundled registry, and asserting the Go/Gin sample application is live via Argo CD sync - in approximately 12 minutes with zero UI clicks.</p>
<p>Here is the verbatim output from my run today:</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">==&gt; Applying the sample Codebase (test-go-app)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Waiting for the Codebase to be provisioned in GitLab (project + template push)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Waiting for the codebase-operator to create the project webhook</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Waiting for the CodebaseImageStream test-go-app-main to exist</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Creating the demo CDPipeline + dev Stage (triggerType: Auto)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Opening a merge request (branch ci-e2e-...) to trigger the review pipeline</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    merge request !1 opened</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Waiting for the webhook to create the review PipelineRun(s)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    triggered: pipelinerun.tekton.dev/review-test-go-app-main-2g7kd</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Evaluating per-task results (review)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    review-test-go-app-main-2g7kd -&gt; ALL_GREEN</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Merging merge request !1 (action=merge fires the build trigger)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    merge request !1 merged</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Waiting for the build run(s) to finish (kaniko build+push can take several minutes)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Evaluating per-task results (build)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    build-test-go-app-main-m8kpm -&gt; ALL_GREEN</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Reading the built image tag from CodebaseImageStream test-go-app-main</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    built tag: main-20260606-132413</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Waiting for the Auto trigger to deploy tag main-20260606-132413</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    triggered: pipelinerun.tekton.dev/deploy-demo-dev-kkmqg</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Evaluating per-task results (deploy)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    deploy-demo-dev-kkmqg -&gt; ALL_GREEN</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">==&gt; Verifying the application is deployed in krci-demo-dev on tag main-20260606-132413</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    OK test-go-app gitlab.127.0.0.1.nip.io:5050/krci/test-go-app:main-20260606-132413@sha256:a39d120b...</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">E2E RESULT: PASS - review + build + deploy all green; test-go-app:main-20260606-132413 auto-deployed to krci-demo-dev.</span><br></div></code></pre></div></div>
<p>The post-run cluster state confirms everything:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci get pipelinerun</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                            TYPE     SUCCEEDED   REASON</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">build-test-go-app-main-m8kpm    build    True        Completed</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">deploy-demo-dev-kkmqg           deploy   True        Succeeded</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">review-test-go-app-main-2g7kd   review   True        Completed</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">$ kubectl </span><span class="token parameter variable" style="color:#36acaa">-n</span><span class="token plain"> krci-demo-dev get deploy,pods</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAME                          READY   UP-TO-DATE   AVAILABLE   AGE</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">deployment.apps/test-go-app   </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">/1     </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">            </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">           85s</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">pod/test-go-app-595fd7f5dc-8sbrc   </span><span class="token number" style="color:#36acaa">1</span><span class="token plain">/1   Running   </span><span class="token number" style="color:#36acaa">0</span><span class="token plain">           85s</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">$ kubectl get applications </span><span class="token parameter variable" style="color:#36acaa">-A</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">NAMESPACE  NAME                  SYNC STATUS  HEALTH STATUS</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci       demo-dev-test-go-app  Synced       Healthy</span><br></div></code></pre></div></div>
<p>The full MR-to-deployed-workload sequence, as a pipeline diagram:</p>
<!-- -->
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-gitlabs-bundled-container-registry">Why GitLab's Bundled Container Registry?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#why-gitlabs-bundled-container-registry" class="hash-link" aria-label="Direct link to Why GitLab's Bundled Container Registry?" title="Direct link to Why GitLab's Bundled Container Registry?" translate="no">​</a></h3>
<p>A common blocker in local CI/CD setups is the container registry: DockerHub rate limits, Harbor setup complexity, credentials management. GitLab CE ships with a built-in Container Registry served on port 5050. The testbed wires kaniko to push images directly to it - eliminating every external registry dependency. Kaniko trusts the self-signed cert via a chart flag (<code>edp-tekton.kaniko.customCert: true</code>), and a group deploy token backs both push and pull. The registry URL is <code>gitlab.127.0.0.1.nip.io:5050/krci/&lt;codebase&gt;</code>, which maps cleanly to each codebase's GitLab project registry. No DockerHub account, no Harbor, no separate registry pod.</p>
<p>A note on the review/build trigger split: the EventListener has two separate Trigger CRs - <code>gitlab-review</code> fires on MR <code>open/reopen/update</code>; <code>gitlab-build</code> fires on MR <code>action=merge</code>. This means <strong>merging</strong> the MR - not a push to the branch - kicks the build pipeline. It is an intentional design choice that avoids spurious build runs on every force-push. Also worth noting: GitLab can occasionally deliver the MR webhook twice, creating a duplicate review run that fails fast with a harmless HTTP 400 when it tries to post the same commit status context. The e2e script asserts at least one run is fully green - the duplicate does not affect the result.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="exploring-the-platform-after-install">Exploring the Platform After Install<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#exploring-the-platform-after-install" class="hash-link" aria-label="Direct link to Exploring the Platform After Install" title="Direct link to Exploring the Platform After Install" translate="no">​</a></h2>
<p>After <code>make e2e</code> passes, every component has real data in it. Here is what I found navigating each UI - a practical KubeRocketCI getting started tour of each surface.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="kuberocketci-portal">KubeRocketCI Portal<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#kuberocketci-portal" class="hash-link" aria-label="Direct link to KubeRocketCI Portal" title="Direct link to KubeRocketCI Portal" translate="no">​</a></h3>
<p>The Portal signs in with a Kubernetes bearer token rather than a username/password - run <code>make token</code> to mint a 24-hour token, then paste it into the Portal's "More options → Use Service Account Token" field. (OIDC is intentionally not wired up in the testbed - there is no identity issuer inside a kind cluster - so the service-account token is the local sign-in path. <code>make status</code> flags the in-cluster Portal's token login as still being wired up; I captured these views through the Portal using that same <code>make token</code> service-account flow.)</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal home dashboard - try KubeRocketCI locally on a kind cluster, showing 3 pipeline runs with 100% success rate" src="https://docs.kuberocketci.io/assets/images/krci-portal-home-3c13c6e2998281994445f55de34aecab.png" width="2880" height="1800" class="img_ev3q"></p>
<p>The home dashboard shows the pipeline metrics from the e2e run: <strong>3 total runs, 100% success rate, 0 failed, average duration 2m 18s</strong>. This is the same observability surface that surfaces 18,397 runs on the production cluster - just seeded with a single e2e cycle.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal Projects page showing krci-gitops and test-go-app codebases registered on local Kubernetes" src="https://docs.kuberocketci.io/assets/images/krci-portal-projects-9ad89e9288cb984b6ccba21ea614c428.png" width="2880" height="1800" class="img_ev3q"></p>
<p>The Projects page shows both registered codebases: <code>krci-gitops</code> (the GitOps/Helm system codebase that KubeRocketCI requires) and <code>test-go-app</code> (the Go/Gin application created by the e2e script). Both show status Created.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal PipelineRuns list showing deploy, build, and review runs all green for test-go-app" src="https://docs.kuberocketci.io/assets/images/krci-portal-pipelineruns-5584590a776e3e351be3b23018ab2d48.png" width="2880" height="1800" class="img_ev3q"></p>
<p>The PipelineRuns list shows all three runs - deploy, build, review - all green, all for <code>test-go-app</code> on PR #1.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal Build PipelineRun DAG visualization showing all tasks green including sonar, kaniko, git-tag steps" src="https://docs.kuberocketci.io/assets/images/krci-portal-build-diagram-da05a9efceb1d73753d247ecb4d2cd76.png" width="2880" height="1800" class="img_ev3q"></p>
<p>The build pipeline DAG is the most visually satisfying part. Every node in the React Flow graph is green: <code>report-pipeline-start</code> → <code>fetch-repository</code> → <code>get-version</code> → <code>sonar</code> → <code>build</code> → <code>container-build</code> → <code>git-tag</code> → <code>update-cbis</code> → <code>save-cache</code>. The sonar step includes a quality gate wait - the build would have failed here if SonarQube reported issues.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal Build PipelineRun details showing gitlab-go-gin-app-build-default pipeline, 1m 34s duration, all tasks completed" src="https://docs.kuberocketci.io/assets/images/krci-portal-pipelinerun-dag-44425cb6356770a47e36964825528c8e.png" width="2880" height="1800" class="img_ev3q"></p>
<p>The build run details confirm the pipeline name (<code>gitlab-go-gin-app-build-default</code>) and duration (1m 34s). This is the Helm-templated pipeline library at work - the name encodes the Git provider, language, framework, and pipeline type, as described in the <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">Kubernetes-native CI/CD with Tekton</a> post.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal Deployments page showing demo CD pipeline with test-go-app application deployed" src="https://docs.kuberocketci.io/assets/images/krci-portal-deployments-aff16ec19eb05875cea6df4fb8c2ab7f.png" width="2880" height="1800" class="img_ev3q"></p>
<p>The Deployments page shows the <code>demo</code> CDPipeline created by the e2e script, with <code>test-go-app</code> as its application.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal Pipeline Metrics showing 3 total runs, 100% success, 2m 18s average duration for build, review, and deploy pipelines" src="https://docs.kuberocketci.io/assets/images/krci-portal-metrics-a3d3152d5eb9d9ed82d1274abe75f2b9.png" width="2880" height="1800" class="img_ev3q"></p>
<p>The Observability → Pipeline Metrics view breaks down the three runs: Build 1/100%, Review 1/100%, Deploy 1/100%, average 2m 18s across all types.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="argo-cd">Argo CD<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#argo-cd" class="hash-link" aria-label="Direct link to Argo CD" title="Direct link to Argo CD" translate="no">​</a></h3>
<p><img decoding="async" loading="lazy" alt="Argo CD UI showing demo-dev-test-go-app application Synced and Healthy with target revision main-20260606-132413 in krci-demo-dev namespace" src="https://docs.kuberocketci.io/assets/images/argocd-applications-b3793f92ac7fe344b32a895ee85c6397.png" width="2880" height="1800" class="img_ev3q"></p>
<p>Argo CD shows <code>demo-dev-test-go-app</code> as Synced + Healthy, target revision <code>main-20260606-132413</code>, path <code>deploy-templates</code>, namespace <code>krci-demo-dev</code>. This is the GitOps delivery leg: after kaniko pushed the image and the CodebaseImageStream updated, the cd-pipeline-operator created a CDStageDeploy resource, which triggered a deploy PipelineRun, which applied the Helm chart through Argo CD. The full GitOps chain, locally.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="gitlab">GitLab<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#gitlab" class="hash-link" aria-label="Direct link to GitLab" title="Direct link to GitLab" translate="no">​</a></h3>
<p><img decoding="async" loading="lazy" alt="Self-hosted GitLab CE showing test-go-app project pipelines both Passed - merge build and e2e review Tekton commit statuses" src="https://docs.kuberocketci.io/assets/images/gitlab-project-f24b76f7911eebde46293550cee78627.png" width="2880" height="1800" class="img_ev3q"></p>
<p>The self-hosted GitLab project shows both pipelines as Passed - Tekton's <code>gitlab-set-status</code> task wrote commit statuses back to GitLab after each pipeline run completed. Pipeline #1 is the review run (MR open), pipeline #2 is the build run (MR merge). The Go/Gin app is live at <code>test-go-app</code> in namespace <code>krci-demo-dev</code> - its <code>/</code> route returns 404 by design (no root handler in the Gin app), but the server is live and the deployment is <code>1/1 Available</code>.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Portal deploy PipelineRun details showing all four deploy tasks green - pre-deploy, deploy-app, post-deploy, promote-images - for the demo-dev stage" src="https://docs.kuberocketci.io/assets/images/krci-portal-deploy-dag-44368cb48fa425ba6fa4424e576d5dc9.png" width="2880" height="1800" class="img_ev3q"></p>
<p>The deploy PipelineRun details confirm the full GitOps delivery chain completed in 57 seconds - all four tasks green (pre-deploy → deploy-app → post-deploy → promote-images), the Argo CD sync applied, and the workload live in <code>krci-demo-dev</code>.</p>
<p>To explore <a class="" href="https://docs.kuberocketci.io/docs/user-guide/tekton-pipelines">Tekton pipelines in KubeRocketCI</a> further, or start customizing pipelines for your own stack, the <a class="" href="https://docs.kuberocketci.io/blog/krci-cli-daily-use">krci CLI daily use</a> post covers how to drive the platform from the terminal and hand off to AI agents. For the foundational <a class="" href="https://docs.kuberocketci.io/docs/basic-concepts">basic concepts</a>, the docs cover Codebases, CDPipelines, CodebaseImageStreams, and Stages in depth.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="tearing-down-make-down">Tearing Down: make down<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#tearing-down-make-down" class="hash-link" aria-label="Direct link to Tearing Down: make down" title="Direct link to Tearing Down: make down" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">make</span><span class="token plain"> down</span><br></div></code></pre></div></div>
<p><code>make down</code> deletes the kind cluster and all associated Kubernetes resources, leaving Docker Desktop in a clean state. Nothing persists outside the kind cluster - no volumes, no registry state, no credentials. The testbed is fully disposable and can be re-run from scratch in under 20 minutes.</p>
<p>This disposability is a first-class design goal, not an afterthought. For full from-zero validation: <code>make down &amp;&amp; make testbed &amp;&amp; make e2e</code>. The <a href="https://github.com/KubeRocketCI/try-kuberocketci" target="_blank" rel="noopener noreferrer" class="">try-kuberocketci repository</a> runs this sequence as part of its own CI regression.</p>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>Safe on shared machines</div><div class="admonitionContent_BuS1"><p>The testbed uses fixed local-only credentials, self-signed TLS, and broad RBAC - all intentionally, for a throwaway kind cluster bound to localhost. It is explicitly not safe to expose or use on any shared, internet-reachable, or production cluster. See <a href="https://github.com/KubeRocketCI/try-kuberocketci/blob/main/SECURITY.md" target="_blank" rel="noopener noreferrer" class="">SECURITY.md</a> for the details.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="use-cases-why-try-kuberocketci-locally">Use Cases: Why Try KubeRocketCI Locally?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#use-cases-why-try-kuberocketci-locally" class="hash-link" aria-label="Direct link to Use Cases: Why Try KubeRocketCI Locally?" title="Direct link to Use Cases: Why Try KubeRocketCI Locally?" translate="no">​</a></h2>
<p>The fastest way to try KubeRocketCI locally is also the safest way to evaluate it - no cloud spend, no shared infrastructure, no consequences if something breaks. Here are the four use cases where the testbed pays off immediately.</p>
<p><strong>Evaluating KubeRocketCI before adoption.</strong> Before committing months of engineering time to an IDP, run <code>make testbed</code> and spend 30 minutes with the real platform - not a demo video or a vendor-configured sandbox. Compare hands-on against Backstage, kubriX, or other IDPs with a working instance under your own fingers. The <a class="" href="https://docs.kuberocketci.io/docs/quick-start/platform-installation">official platform installation guide</a> covers the cloud path when you are ready to move from local to a real cluster.</p>
<p><strong>Learning platform engineering.</strong> The testbed is a coherent, fully-wired example of Tekton, Argo CD, GitOps, SonarQube, and Tekton Results working together. Every piece is independently inspectable with <code>kubectl</code>, <code>helm</code>, and the respective UIs. Productive learning by reading a real system, not a toy demo.</p>
<p><strong>Contributing to KubeRocketCI.</strong> The testbed is designed as a contributor sandbox. Run <code>make down &amp;&amp; make testbed</code> to reproduce an issue from scratch without a cloud account. Each component can be rebuilt independently (<code>make sonar</code>, <code>make argocd</code>, etc.), so debugging one piece does not require tearing down the rest.</p>
<p><strong>Demo preparation.</strong> Spin up a fresh instance in under 20 minutes before a presentation; tear down afterward. The <code>make e2e</code> output gives you a verified, live end-state to present - not a screen recording.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-this-compares-to-other-local-idp-options">How This Compares to Other Local IDP Options<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#how-this-compares-to-other-local-idp-options" class="hash-link" aria-label="Direct link to How This Compares to Other Local IDP Options" title="Direct link to How This Compares to Other Local IDP Options" translate="no">​</a></h2>
<p>The table below compares try-kuberocketci against the three closest local IDP alternatives across the features that matter most for local evaluation - install time, automated testing, component depth, and Apple Silicon support.</p>
<table><thead><tr><th>Feature</th><th>try-kuberocketci</th><th>CNOE idpBuilder</th><th>kubriX kind guide</th><th>Manual Tekton+ArgoCD</th></tr></thead><tbody><tr><td>Commands to full stack</td><td>2 (<code>make testbed</code>, <code>make e2e</code>)</td><td>1 (<code>idpbuilder create</code>)</td><td>6 manual steps</td><td>15+ manual steps</td></tr><tr><td>Install time</td><td>~18–20 min</td><td>~5–10 min</td><td>~30 min</td><td>60+ min</td></tr><tr><td>Automated E2E test</td><td>Yes (zero UI clicks)</td><td>No</td><td>No</td><td>No</td></tr><tr><td>Tekton CI pipelines</td><td>Yes</td><td>No</td><td>No</td><td>Manual</td></tr><tr><td>Self-hosted GitLab CE</td><td>Yes</td><td>No (Gitea)</td><td>No</td><td>No</td></tr><tr><td>SonarQube</td><td>Yes</td><td>No</td><td>No</td><td>Optional/manual</td></tr><tr><td>Tekton Results</td><td>Yes</td><td>No</td><td>No</td><td>No</td></tr><tr><td>Prometheus + Grafana</td><td>Yes</td><td>No</td><td>No</td><td>No</td></tr><tr><td>Apple Silicon support</td><td>Explicit (Rosetta)</td><td>Yes</td><td>Not documented</td><td>Not documented</td></tr><tr><td>nip.io DNS (no /etc/hosts)</td><td>Yes</td><td>Partial</td><td>No</td><td>No</td></tr><tr><td>GitOps-pinned versions</td><td>Yes (edp-cluster-add-ons)</td><td>Partial</td><td>No</td><td>No</td></tr><tr><td>Teardown command</td><td><code>make down</code></td><td>Manual</td><td>Manual</td><td>Manual</td></tr></tbody></table>
<p>CNOE idpBuilder is the closest open-source alternative - it installs Argo CD, Gitea, and ingress-nginx in approximately 5–10 minutes with a single command. It is a solid choice for a minimal GitOps sandbox. What it does not have: Tekton CI pipelines, SonarQube quality gates, Tekton Results for pipeline history, Prometheus/Grafana observability, or an automated end-to-end pipeline proof. The try-kuberocketci testbed installs a significantly larger stack and uniquely validates the full MR-to-deployed-workload cycle without any UI interaction. Also note: GitHub integration is fully supported in production KubeRocketCI (<a class="" href="https://docs.kuberocketci.io/docs/quick-start/integrate-github">integrate GitHub with KubeRocketCI</a>) - the testbed uses GitLab specifically because it can be self-hosted in the kind cluster without a cloud account.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="frequently-asked-questions">Frequently Asked Questions<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#frequently-asked-questions" class="hash-link" aria-label="Direct link to Frequently Asked Questions" title="Direct link to Frequently Asked Questions" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-kuberocketci-1">What is KubeRocketCI?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#what-is-kuberocketci-1" class="hash-link" aria-label="Direct link to What is KubeRocketCI?" title="Direct link to What is KubeRocketCI?" translate="no">​</a></h3>
<p>KubeRocketCI (KRCI) is an open-source internal developer platform for cloud-native CI/CD on Kubernetes, developed by EPAM and released under Apache 2.0, that integrates Tekton, Argo CD, and GitLab (or GitHub, Bitbucket) into a unified developer workflow platform. It manages Codebases from source through review, build, and GitOps-based deployment, surfacing everything through a single Portal UI and CLI.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-i-try-kuberocketci-without-a-cloud-account">How do I try KubeRocketCI without a cloud account?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#how-do-i-try-kuberocketci-without-a-cloud-account" class="hash-link" aria-label="Direct link to How do I try KubeRocketCI without a cloud account?" title="Direct link to How do I try KubeRocketCI without a cloud account?" translate="no">​</a></h3>
<p>Clone the <a href="https://github.com/KubeRocketCI/try-kuberocketci" target="_blank" rel="noopener noreferrer" class="">try-kuberocketci repository</a> and try KubeRocketCI locally by running <code>make testbed</code> - it installs the full KubeRocketCI platform on a local kind cluster on Docker Desktop in approximately 18–20 minutes, with no cloud account or pre-existing Kubernetes cluster required. Run <code>make e2e</code> afterward to validate the full CI/CD pipeline automatically.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-are-the-hardware-requirements-to-run-kuberocketci-locally">What are the hardware requirements to run KubeRocketCI locally?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#what-are-the-hardware-requirements-to-run-kuberocketci-locally" class="hash-link" aria-label="Direct link to What are the hardware requirements to run KubeRocketCI locally?" title="Direct link to What are the hardware requirements to run KubeRocketCI locally?" translate="no">​</a></h3>
<p>Docker Desktop with at least 12 GB of RAM allocated to the Docker engine is required for the full bed (8 GB suffices if you skip GitLab and SonarQube). Approximately 20 GB of free disk space is also needed for container images and volumes. No other cloud or network infrastructure is required.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="does-kuberocketci-work-on-apple-silicon-m1m2m3">Does KubeRocketCI work on Apple Silicon (M1/M2/M3)?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#does-kuberocketci-work-on-apple-silicon-m1m2m3" class="hash-link" aria-label="Direct link to Does KubeRocketCI work on Apple Silicon (M1/M2/M3)?" title="Direct link to Does KubeRocketCI work on Apple Silicon (M1/M2/M3)?" translate="no">​</a></h3>
<p>Yes. Apple Silicon (M1/M2/M3) is fully supported: Docker Desktop's Rosetta 2 emulation runs the amd64 container images (Portal, GitLab CE, sonar-operator) transparently on ARM64 hardware, with no configuration changes required in the testbed. I ran the entire flow on an Apple Silicon Mac for this post - the testbed explicitly documents and tests ARM64 support.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-long-does-it-take-to-install-kuberocketci-locally">How long does it take to install KubeRocketCI locally?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#how-long-does-it-take-to-install-kuberocketci-locally" class="hash-link" aria-label="Direct link to How long does it take to install KubeRocketCI locally?" title="Direct link to How long does it take to install KubeRocketCI locally?" translate="no">​</a></h3>
<p><strong><code>make testbed</code></strong> installs the full platform in <strong>approximately 18–20 minutes</strong>; <strong><code>make e2e</code></strong> runs a complete automated pipeline proof in <strong>approximately 12 minutes</strong>. Total time from zero to a verified live workload is under 35 minutes. GitLab CE is typically the slowest component to initialize - the rest of the stack comes up faster.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-components-does-make-testbed-install">What components does <code>make testbed</code> install?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#what-components-does-make-testbed-install" class="hash-link" aria-label="Direct link to what-components-does-make-testbed-install" title="Direct link to what-components-does-make-testbed-install" translate="no">​</a></h3>
<p>In dependency order: a single-node kind cluster, ingress-nginx, cert-manager, Tekton Pipelines/Triggers, Argo CD, Prometheus and Grafana, Tekton Results, SonarQube Community, self-hosted GitLab CE (with bundled container registry), and KubeRocketCI edp-install 3.14.0 - 10 components in a defined sequence, with KubeRocketCI installed last so the chart can wire itself to every running dependency on first reconcile.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-i-run-an-end-to-end-cicd-pipeline-locally-with-kuberocketci">How do I run an end-to-end CI/CD pipeline locally with KubeRocketCI?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#how-do-i-run-an-end-to-end-cicd-pipeline-locally-with-kuberocketci" class="hash-link" aria-label="Direct link to How do I run an end-to-end CI/CD pipeline locally with KubeRocketCI?" title="Direct link to How do I run an end-to-end CI/CD pipeline locally with KubeRocketCI?" translate="no">​</a></h3>
<p>Run <strong><code>make e2e</code></strong> after <code>make testbed</code> completes. It automates the full cycle: applies a sample Go/Gin Codebase, waits for the codebase-operator to provision the GitLab project and webhook, opens a Merge Request to trigger the Tekton review pipeline, merges the MR to trigger the build pipeline (kaniko pushes the image to GitLab's bundled registry), waits for Argo CD to sync the deployment, and asserts the workload is <code>1/1 Available</code> - all in approximately 12 minutes with zero UI clicks.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-do-i-tear-down-the-local-kuberocketci-environment">How do I tear down the local KubeRocketCI environment?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#how-do-i-tear-down-the-local-kuberocketci-environment" class="hash-link" aria-label="Direct link to How do I tear down the local KubeRocketCI environment?" title="Direct link to How do I tear down the local KubeRocketCI environment?" translate="no">​</a></h3>
<p>Run <strong><code>make down</code></strong> to delete the kind cluster and all associated Kubernetes resources, leaving Docker Desktop in a clean state. Nothing persists outside the kind cluster. The testbed can be re-run from scratch in under 20 minutes and is designed to be fully disposable - suitable for evaluation, demo preparation, contributor testing, and automated platform CI regression.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-is-there-no-etchosts-editing-required">Why is there no /etc/hosts editing required?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#why-is-there-no-etchosts-editing-required" class="hash-link" aria-label="Direct link to Why is there no /etc/hosts editing required?" title="Direct link to Why is there no /etc/hosts editing required?" translate="no">​</a></h3>
<p>The testbed uses <a href="https://nip.io/" target="_blank" rel="noopener noreferrer" class="">nip.io</a> wildcard DNS: any subdomain of <code>&lt;IP&gt;.nip.io</code> resolves to <code>&lt;IP&gt;</code> via public DNS, so all platform services get stable browser-accessible URLs (<code>*.127.0.0.1.nip.io</code> → <code>127.0.0.1</code>) without any local DNS configuration. Inside the cluster, CoreDNS rewrites handle pod-to-service resolution, and the kind node's containerd uses a registry mirror config for image pulls - no host-level DNS changes at any point.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-the-difference-between-try-kuberocketci-and-cnoe-idpbuilder">What is the difference between try-kuberocketci and CNOE idpBuilder?<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#what-is-the-difference-between-try-kuberocketci-and-cnoe-idpbuilder" class="hash-link" aria-label="Direct link to What is the difference between try-kuberocketci and CNOE idpBuilder?" title="Direct link to What is the difference between try-kuberocketci and CNOE idpBuilder?" translate="no">​</a></h3>
<p>CNOE idpBuilder spins up a minimal IDP stack (Argo CD + Gitea + ingress-nginx) in approximately 5–10 minutes but ships no Tekton CI pipelines, no SonarQube, no GitLab CE, no Tekton Results, and no automated e2e validation. The try-kuberocketci testbed installs a full production-parity platform with all of these in ~18–20 minutes and uniquely includes a fully automated end-to-end pipeline test that validates a real MR-to-deployed-workload cycle without any UI interaction.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="summary">Summary<a href="https://docs.kuberocketci.io/blog/try-kuberocketci-locally#summary" class="hash-link" aria-label="Direct link to Summary" title="Direct link to Summary" translate="no">​</a></h2>
<p>The fastest way to try KubeRocketCI locally is two commands on Docker Desktop. <strong><code>make testbed</code></strong> brings up a complete KubeRocketCI platform - 10 components including Tekton, Argo CD, SonarQube, GitLab CE, Prometheus/Grafana, and Tekton Results - in approximately 18–20 minutes. <strong><code>make e2e</code></strong> validates the full MR-to-deployed-workload pipeline automatically in approximately 12 minutes. Total: under 35 minutes from zero to a verified live workload, on Docker Desktop, with no cloud account. <strong><code>make down</code></strong> leaves nothing behind.</p>
<p>I ran this end-to-end on June 6, 2026 on an Apple Silicon Mac with Docker Desktop. The e2e test passed with 3/3 green runs (review + build + deploy), 100% success rate, average duration 2m 18s, and the Go/Gin sample app deployed at tag <code>main-20260606-132413</code>.</p>
<p>The most useful next steps from here:</p>
<ul>
<li class="">Explore the <a class="" href="https://docs.kuberocketci.io/docs/quick-start/quick-start-overview">KubeRocketCI quick start overview</a> to continue KubeRocketCI getting started and move from a local kind cluster to a real cloud deployment.</li>
<li class="">Read <a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">Kubernetes-native CI/CD with Tekton</a> for the deeper pipeline architecture and production metrics.</li>
<li class="">Use the <a class="" href="https://docs.kuberocketci.io/blog/krci-cli-daily-use">krci CLI</a> to drive the platform from the terminal and AI agents after onboarding your first real application.</li>
<li class="">Check <a href="https://docs.kuberocketci.io/" target="_blank" rel="noopener noreferrer" class="">KubeRocketCI documentation</a> for the full operator guide and integration references.</li>
</ul>
<p>KubeRocketCI is open-source under Apache License 2.0. The testbed, platform source, and Helm charts are all on <a href="https://github.com/KubeRocketCI/try-kuberocketci" target="_blank" rel="noopener noreferrer" class="">GitHub</a>.</p>
]]></content>
        <author>
            <name>Sergiy Kulanov</name>
            <uri>https://github.com/sergk</uri>
        </author>
        <category label="KubeRocketCI" term="KubeRocketCI"/>
        <category label="Tutorial" term="Tutorial"/>
        <category label="Getting Started" term="Getting Started"/>
        <category label="CI/CD" term="CI/CD"/>
        <category label="Kubernetes" term="Kubernetes"/>
        <category label="kind" term="kind"/>
        <category label="DevOps" term="DevOps"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="Open Source" term="Open Source"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[krci CLI: From Terminal to AI Agents]]></title>
        <id>https://docs.kuberocketci.io/blog/krci-cli-daily-use</id>
        <link href="https://docs.kuberocketci.io/blog/krci-cli-daily-use"/>
        <updated>2026-05-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How I use the krci CLI as a tool surface for Claude Code and other AI assistants to answer day-to-day platform questions in plain language.]]></summary>
        <content type="html"><![CDATA[<p>Most of my day-to-day platform work happens in a conversation. I sit in a Claude Code session - or any AI assistant with shell access - and ask plain-language questions about the state of our delivery cluster: <em>what's failing, what's drifting, what's vulnerable, what's stale.</em> The agent answers by calling the <a href="https://github.com/KubeRocketCI/cli" target="_blank" rel="noopener noreferrer" class="">krci CLI</a>, the predictable, JSON-emitting client over the <a class="" href="https://docs.kuberocketci.io/docs/developer-guide/reference-architecture">KubeRocketCI Portal's tRPC API</a>. I read the answer, decide what to do, and when a question turns into a routine I drop the underlying invocation into a script and let it run on cron or <code>/loop</code>. This post is a snapshot of that workflow with one running example - operator vulnerability status - and a tour of the other questions the same pattern answers.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="workflows-not-sessions">Workflows, Not Sessions<a href="https://docs.kuberocketci.io/blog/krci-cli-daily-use#workflows-not-sessions" class="hash-link" aria-label="Direct link to Workflows, Not Sessions" title="Direct link to Workflows, Not Sessions" translate="no">​</a></h2>
<p>KubeRocketCI is built so the platform's primitives are reachable from every runtime that matters in modern engineering: the portal, the CLI, Tekton pipelines, GitLab CI, schedulers, and AI agents. A workflow I write once runs everywhere it needs to:</p>
<ul>
<li class=""><strong>Pipelines.</strong> A <code>krci</code> invocation that answers a question in my terminal runs unchanged inside a Tekton task or a GitLab CI job. Quality gates and release governance read live platform state without bespoke API clients.</li>
<li class=""><strong>Schedulers.</strong> A short bash wrapper in cron, or a Kubernetes CronJob produces a daily report on a hands-off cadence - same command, same output, same trust boundary.</li>
<li class=""><strong>Chat and webhooks.</strong> JSON output drops straight into Slack, Teams, or PagerDuty without a translation layer.</li>
<li class=""><strong>AI agents in the SDLC.</strong> AI coding agents - <a href="https://code.claude.com/docs" target="_blank" rel="noopener noreferrer" class="">Claude Code</a>, Cursor, the Anthropic and OpenAI SDKs - read terminal output natively. Giving an agent shell access to <code>krci</code> turns the platform into a tool surface it can call: list codebases, inspect SBOMs, compare branches, summarize changes since the previous run. No MCP server to write, no proprietary protocol to learn, no stale API client to maintain.</li>
</ul>
<p>The portal and the CLI are peers: the portal is the right interface for browsing, comparing, and approving; the CLI is the right interface for composing, scheduling, and handing off to agents. Both are first-class products and ship in lockstep.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-krci-cli-as-a-tool-surface">The krci CLI as a Tool Surface<a href="https://docs.kuberocketci.io/blog/krci-cli-daily-use#the-krci-cli-as-a-tool-surface" class="hash-link" aria-label="Direct link to The krci CLI as a Tool Surface" title="Direct link to The krci CLI as a Tool Surface" translate="no">​</a></h2>
<p>A short tour of the top-level surface, because this is what the agent has access to:</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">$ krci --help</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">Available Commands:</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  auth        Authentication commands</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  deployment  Manage deployments (CDPipelines)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  env         Inspect KRCI environments (Stages)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  pipelinerun Manage pipeline runs</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  project     Manage projects (Codebases)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  sca         Inspect Dependency-Track projects, components, and vulnerability findings</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  sonar       Inspect SonarQube projects, quality gates, and issues</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  version     Print krci CLI version</span><br></div></code></pre></div></div>
<p>When you run any of these commands - say, <code>krci deployment list</code> to inspect CD pipelines across your environments-the output follows the same disciplined structure. Columns are consistent, status values are predictable, and the table is human-readable <em>and</em> machine-parseable without any translation. Here's what that looks like in practice:</p>
<p><img decoding="async" loading="lazy" alt="Example of krci deployment list command output showing a table with deployment status, environment, sync state, and other metadata. The table displays three deployments: krci-gitfusion with healthy status in dev, and two tekton instances in dev and qa environments with varying sync states." src="https://docs.kuberocketci.io/assets/images/krci-deployment-example-caf267064ce6a879812ee983987af264.png" width="1904" height="290" class="img_ev3q"></p>
<p>This table shows exactly what's deployed where. The <code>VERSION</code> column tracks the build artifact (<code>0.5.0-SNAPSHOT.4</code>), the <code>ENV</code> column shows which environment it's in, and the <code>STATUS</code> column tells you health at a glance -<code>healthy</code> or <code>missing</code>. The <code>SYNC</code> column flags when an environment has drifted (<code>OutOfSync</code>) from what GitOps expects. In seconds, you can answer: <em>"What's actually running in qa? Is it in sync? What version?"</em> The agent can answer it too, and answer the next question - <em>"Show me all deployments older than 30 days"</em> - without any special prompting.</p>
<p>Every group follows the same shape - <code>list</code>, <code>get</code>, sometimes a third verb - and the flags repeat (<code>--branch</code>, <code>--severity</code>, <code>-o json|table</code>). Predictability is the point: an agent can reason about the next command from the previous one, and so can I. Authentication is OIDC against the same Keycloak the portal uses; the agent inherits the existing session, so there is no long-lived API key embedded in any prompt.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="asking-the-agent-whats-the-vulnerability-status-of-our-operators">Asking the Agent: "What's the Vulnerability Status of Our Operators?"<a href="https://docs.kuberocketci.io/blog/krci-cli-daily-use#asking-the-agent-whats-the-vulnerability-status-of-our-operators" class="hash-link" aria-label="Direct link to Asking the Agent: &quot;What's the Vulnerability Status of Our Operators?&quot;" title="Direct link to Asking the Agent: &quot;What's the Vulnerability Status of Our Operators?&quot;" translate="no">​</a></h2>
<p>Here's a recent question, verbatim, from a Claude Code session:</p>
<blockquote>
<p><em>"Using krci, give me the current vulnerability status of every operator registered on the platform. Main branch only, critical and high counts, top affected components per operator."</em></p>
</blockquote>
<p>What the agent returned:</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">OPERATOR               CRIT  HIGH  TOP COMPONENTS (HIGH+)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">cd-pipeline-operator     0     0   clean</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">codebase-operator        0     2   github.com/tektoncd/pipeline</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">keycloak-operator        0     0   clean</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">nexus-operator           0     0   clean</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">sonar-operator           0     0   clean</span><br></div></code></pre></div></div>
<p>One row is doing all the work: <code>codebase-operator</code> master carries two high-severity findings, and both trace back to a single <code>github.com/tektoncd/pipeline</code> cluster. Everything else is clean. That's the kind of answer I can act on without scrolling - one operator, one component, one upgrade to plan.</p>
<p>The command I run by hand to verify the answer is one line:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">$ krci sca list --page-size </span><span class="token number" style="color:#36acaa">500</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-o</span><span class="token plain"> json </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 operator" style="color:#393A34">|</span><span class="token plain"> jq </span><span class="token parameter variable" style="color:#36acaa">-r</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'.data.items[]</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">           | select(.name | endswith("-operator"))</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">           | select(.version == "master")</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">           | [.name, .metrics.critical, .metrics.high] | @tsv'</span><br></div></code></pre></div></div>
<p>That's the whole loop. I described what I wanted in the language of the work, the agent shaped a <code>krci</code> invocation, and I can re-run any step independently to validate the result. No prior knowledge of <code>jq</code> flags, subcommand layout, or the platform's internal data model was required on my side.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="drilling-in-stays-in-plain-language">Drilling In Stays in Plain Language<a href="https://docs.kuberocketci.io/blog/krci-cli-daily-use#drilling-in-stays-in-plain-language" class="hash-link" aria-label="Direct link to Drilling In Stays in Plain Language" title="Direct link to Drilling In Stays in Plain Language" translate="no">​</a></h3>
<p>When a row of the answer needs follow-up, I keep the conversation going:</p>
<blockquote>
<p><em>"Drill into <code>codebase-operator</code> master - which components are driving those two highs?"</em></p>
</blockquote>
<p>The agent's reply (verifiable by running the command yourself):</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">$ krci sca components codebase-operator --branch=master --severity=high</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">COMPONENT                       VULNS (C/H/M/L)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">github.com/tektoncd/pipeline    0/2/4/0</span><br></div></code></pre></div></div>
<p>One component, both highs, two natural-language turns, two commands. I never had to remember the flag shape; the agent bridged intent to invocation and the CLI's predictable output bridged invocation to answer.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-same-pattern-other-questions">The Same Pattern, Other Questions<a href="https://docs.kuberocketci.io/blog/krci-cli-daily-use#the-same-pattern-other-questions" class="hash-link" aria-label="Direct link to The Same Pattern, Other Questions" title="Direct link to The Same Pattern, Other Questions" translate="no">​</a></h2>
<p>Vulnerability status is one of many. The agent + CLI handle the same shape of question across the platform - I describe it in plain language, the agent runs <code>krci</code>, I verify the output against the platform:</p>
<ul>
<li class=""><em>"Which pipeline runs failed in the last 24 hours, grouped by codebase?"</em> → <code>krci pipelinerun list -o json</code> filtered on <code>.status.conditions</code>.</li>
<li class=""><em>"Which environments are running an out-of-date image of <code>code-assistant</code>?"</em> → <code>krci env list -o json</code> joined against the latest tag.</li>
<li class=""><em>"What's the SonarQube quality gate state of every Java codebase?"</em> → <code>krci sonar list -o json</code> filtered by language.</li>
<li class=""><em>"Which projects on the platform haven't produced an SBOM yet?"</em> → diff <code>krci project list</code> against <code>krci sca list</code>.</li>
<li class=""><em>"Show me every CD pipeline whose latest deploy is older than 30 days."</em> → <code>krci deployment list -o json</code> filtered on <code>.status.lastDeploy</code>.</li>
</ul>
<p>Same pattern every time: ask in language, get an answer the CLI can verify, move on. The agent does the orchestration; the CLI does the work.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="making-it-stick-schedule-script-notify">Making It Stick: Schedule, Script, Notify<a href="https://docs.kuberocketci.io/blog/krci-cli-daily-use#making-it-stick-schedule-script-notify" class="hash-link" aria-label="Direct link to Making It Stick: Schedule, Script, Notify" title="Direct link to Making It Stick: Schedule, Script, Notify" translate="no">​</a></h2>
<p>The moment a question becomes recurring, it stops belonging in chat. The same <code>krci</code> invocation the agent assembled in the conversation becomes the body of a script:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token shebang important">#!/usr/bin/env bash</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># Daily operator vulnerability digest - main branches only.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">krci sca list --page-size </span><span class="token number" style="color:#36acaa">500</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-o</span><span class="token plain"> json </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 operator" style="color:#393A34">|</span><span class="token plain"> jq </span><span class="token parameter variable" style="color:#36acaa">-r</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">'.data.items[]</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">           | select(.name | endswith("-operator"))</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">           | select(.version == "master")</span><br></div><div class="token-line" style="color:#393A34"><span class="token string" style="color:#e3116c">           | [.name, .metrics.critical, .metrics.high] | @tsv'</span><span class="token plain"> </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 operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token function" style="color:#d73a49">column</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-t</span><br></div></code></pre></div></div>
<p>From there it's the standard fan-out:</p>
<ul>
<li class=""><strong>Cron or Kubernetes CronJob</strong> for a daily markdown digest in <code>~/reports/operators-$(date +%F).md</code>.</li>
<li class=""><strong>Slack/Teams webhook</strong> for "first new critical on a release branch" alerts. Pipe the JSON through <code>jq</code>, post on threshold.</li>
<li class=""><strong>Claude Code <code>/loop</code></strong> for a six-hour heartbeat that reports only what changed:</li>
</ul>
<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">/loop 6h  Run cli/scripts/operators-digest.sh and tell me ONLY what</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">          changed since the previous run. Reply in under 60 words.</span><br></div></code></pre></div></div>
<p>Same script, same JSON, three runtimes. The CLI is unchanged; only the wrapper changes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-the-krci-cli-works-well-as-an-agent-tool-surface">Why the krci CLI Works Well as an Agent Tool Surface<a href="https://docs.kuberocketci.io/blog/krci-cli-daily-use#why-the-krci-cli-works-well-as-an-agent-tool-surface" class="hash-link" aria-label="Direct link to Why the krci CLI Works Well as an Agent Tool Surface" title="Direct link to Why the krci CLI Works Well as an Agent Tool Surface" translate="no">​</a></h2>
<p>Three properties matter, and each one comes from deliberate design rather than accident:</p>
<ul>
<li class=""><strong>Stable, structured output.</strong> <code>krci ... -o json</code> is shaped consistently across releases. The agent doesn't need a parser; it navigates JSON.</li>
<li class=""><strong>Identity is solved upstream.</strong> OIDC sessions are inherited from the developer's existing login. No API keys leak into prompts, transcripts, or scripts.</li>
<li class=""><strong>Predictable verb shape.</strong> <code>list</code>, <code>get</code>, sometimes a third verb, with the same flags everywhere. The agent generalizes from one command to the next, and so do humans.</li>
</ul>
<p>The result is that the conversation with the agent stays in the language of the work - <em>"vulnerability status of operators"</em>, <em>"failed runs in the last 24 hours"</em>, <em>"environments running an out-of-date image"</em> - and the CLI handles the translation. Both pieces are independently inspectable: I can replay any command myself, the agent can replay it tomorrow, and the script that captures a routine is short enough to review in one sitting.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="honest-notes-before-you-adopt-the-pattern">Honest Notes Before You Adopt the Pattern<a href="https://docs.kuberocketci.io/blog/krci-cli-daily-use#honest-notes-before-you-adopt-the-pattern" class="hash-link" aria-label="Direct link to Honest Notes Before You Adopt the Pattern" title="Direct link to Honest Notes Before You Adopt the Pattern" translate="no">​</a></h2>
<p>A few things I learned the slightly hard way:</p>
<ul>
<li class="">Not every codebase produces an SBOM yet. Some appear in <code>project list</code> but are absent from <code>sca list</code> because no CycloneDX upload has happened on their build pipelines. The CLI surfaces the gap rather than hiding it, which is itself useful, but means agent answers should always cross-check both lists.</li>
<li class="">Cross-project aggregation sometimes still uses a shell loop. There's no <code>krci sca list --filter='critical&gt;0'</code> flag yet - native flags are on the roadmap, the agent compensates in the meantime.</li>
<li class="">BOM age (<code>Last BOM</code>) shows up in <code>krci sca get</code> but not on <code>sca list</code>. If staleness matters to your routine, pin it explicitly in your script.</li>
</ul>
<p>I include this list because tools earn trust by being honest about what they don't do yet.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="try-it-yourself">Try It Yourself<a href="https://docs.kuberocketci.io/blog/krci-cli-daily-use#try-it-yourself" class="hash-link" aria-label="Direct link to Try It Yourself" title="Direct link to Try It Yourself" translate="no">​</a></h2>
<ol>
<li class="">Install krci from <a href="https://github.com/KubeRocketCI/cli/releases" target="_blank" rel="noopener noreferrer" class="">the release page</a> and run <code>krci auth login</code>.</li>
<li class="">Open your AI assistant of choice, give it shell access, and ask your own version of the question above. Anything along the lines of <em>"using krci, show me the vulnerability status of every operator on the platform, main branch only"</em> will work on day one.</li>
<li class="">When the answer becomes a routine, drop the underlying command into a script and run it on cron, a CronJob, or <code>/loop</code>.</li>
</ol>
<p>That's the entire loop: ask in language, get an answer the CLI can verify, schedule it when it matters.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="further-reading">Further Reading<a href="https://docs.kuberocketci.io/blog/krci-cli-daily-use#further-reading" class="hash-link" aria-label="Direct link to Further Reading" title="Direct link to Further Reading" translate="no">​</a></h2>
<ul>
<li class=""><a href="https://github.com/KubeRocketCI/cli" target="_blank" rel="noopener noreferrer" class="">krci CLI on GitHub</a> - command reference, authentication setup, and release binaries.</li>
<li class=""><a class="" href="https://docs.kuberocketci.io/docs/operator-guide/devsecops/dependency-track">SCA with Dependency-Track</a> - how SBOMs reach the platform.</li>
<li class=""><a class="" href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci">Kubernetes-Native CI/CD with Tekton</a> - the pipeline layer that produces those SBOMs.</li>
<li class=""><a href="https://cyclonedx.org/" target="_blank" rel="noopener noreferrer" class="">CycloneDX SBOM</a>, <a href="https://dependencytrack.org/" target="_blank" rel="noopener noreferrer" class="">Dependency-Track</a>, and <a href="https://code.claude.com/docs" target="_blank" rel="noopener noreferrer" class="">Claude Code</a> - the upstream tools used in this post.</li>
</ul>
<p>The krci CLI is open-source under Apache License 2.0. Source, issues, and release binaries live on <a href="https://github.com/KubeRocketCI/cli" target="_blank" rel="noopener noreferrer" class="">GitHub</a>.</p>]]></content>
        <author>
            <name>Sergiy Kulanov</name>
            <uri>https://github.com/sergk</uri>
        </author>
        <category label="KubeRocketCI" term="KubeRocketCI"/>
        <category label="CLI" term="CLI"/>
        <category label="AI Agents" term="AI Agents"/>
        <category label="DevOps" term="DevOps"/>
        <category label="SCA" term="SCA"/>
        <category label="Claude Code" term="Claude Code"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="Dependency-Track" term="Dependency-Track"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Kubernetes-Native CI/CD with Tekton]]></title>
        <id>https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci</id>
        <link href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci"/>
        <updated>2026-05-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Learn how KubeRocketCI builds Kubernetes-native CI/CD on Tekton - covering pipeline task sequences, webhook trigger chain, and how to author custom pipelines.]]></summary>
        <content type="html"><![CDATA[<p>Building CI/CD on Kubernetes used to mean running Jenkins or GitLab CI in a pod and calling it done. <a href="https://tekton.dev/docs/" target="_blank" rel="noopener noreferrer" class="">Tekton</a> changed that by making pipelines first-class Kubernetes objects - Tasks and Pipelines are CRDs, PipelineRuns are namespaced resources, and every step log is a container log. KubeRocketCI goes a step further: it ships a complete, production-grade CI/CD platform on top of Tekton so your team gets sensible defaults, a portal UI, GitOps-managed pipeline definitions, and opinionated quality gates - without the months of plumbing work that comes with assembling those pieces from scratch. I've seen teams go from a bare cluster to a working build-deploy loop in under a day using this stack.</p>
<p>Here's how the layering works in practice - from Tekton's native primitives to the Helm-templated pipeline library, the webhook-to-PipelineRun chain, each pipeline type, and the portal UI that surfaces it all.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-does-kubernetes-native-cicd-matter">Why Does Kubernetes-Native CI/CD Matter?<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#why-does-kubernetes-native-cicd-matter" class="hash-link" aria-label="Direct link to Why Does Kubernetes-Native CI/CD Matter?" title="Direct link to Why Does Kubernetes-Native CI/CD Matter?" translate="no">​</a></h2>
<p>Traditional CI servers are external systems that <em>talk to</em> Kubernetes. They authenticate, schedule jobs, wait for responses, and manage their own scaling independently of the cluster. Tekton turns that model inside out - the cluster <em>is</em> the CI engine. Every pipeline run is a pod, every step is a container, and the Kubernetes control plane handles scheduling, scaling, and secrets injection natively.</p>
<p>The practical consequences:</p>
<ul>
<li class=""><strong>No CI server to maintain.</strong> No Jenkins controller HA setup, no separate runner fleet. Tekton scales horizontally with your cluster.</li>
<li class=""><strong>Uniform RBAC.</strong> Pipeline permissions use Kubernetes ServiceAccounts - no separate credential management UI.</li>
<li class=""><strong>Pipeline as code.</strong> Pipeline definitions are YAML manifests, version-controlled alongside application code, and applied to any conformant Kubernetes cluster identically.</li>
<li class=""><strong>Native secret injection.</strong> Tekton Tasks consume Kubernetes Secrets and ConfigMaps directly - no plugin abstraction required.</li>
<li class=""><strong>Shift-left by default.</strong> Code quality, security scanning, and test gates run inside the cluster as ordinary containers, co-located with the workloads they protect.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="kuberocketci-and-the-tekton-stack">KubeRocketCI and the Tekton Stack<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#kuberocketci-and-the-tekton-stack" class="hash-link" aria-label="Direct link to KubeRocketCI and the Tekton Stack" title="Direct link to KubeRocketCI and the Tekton Stack" translate="no">​</a></h2>
<p><a class="" href="https://docs.kuberocketci.io/docs/about-platform">KubeRocketCI</a> is an open-source, cloud-agnostic SaaS/PaaS platform built on <a href="https://tekton.dev/docs/" target="_blank" rel="noopener noreferrer" class="">Tekton</a> - a CNCF Graduated project - developed and maintained by EPAM Systems. Where Tekton gives you primitives, KubeRocketCI gives you a working system.</p>
<p>The platform deploys and manages the full Tekton component set:</p>
<table><thead><tr><th>Component</th><th>Role</th></tr></thead><tbody><tr><td><strong>Tekton Pipelines</strong></td><td>Executes all CI and CD pipeline runs</td></tr><tr><td><strong>Tekton Triggers</strong></td><td>Listens for Git webhook events and fires the matching pipeline</td></tr><tr><td><strong>Tekton Interceptors</strong></td><td>Enriches and filters GitHub, GitLab, and Bitbucket payloads before routing</td></tr><tr><td><strong>Tekton Chains</strong></td><td>Signs pipeline artifacts for supply-chain provenance</td></tr><tr><td><strong>Tekton Results</strong></td><td>Stores run history for audit trails and portal display</td></tr></tbody></table>
<p>On top of Tekton, KubeRocketCI adds:</p>
<ul>
<li class=""><strong>Pipeline library</strong> - Helm-packaged Tasks and Pipelines for Java, Go, Node.js, Python, .NET, Helm, Terraform, and more.</li>
<li class=""><strong>Portal UI</strong> - full pipeline management with DAG visualization, live log streaming, and inline manual approval gates.</li>
<li class=""><strong>Argo CD integration</strong> - pipeline definitions are stored in Git and synchronized by Argo CD. Every change to a pipeline is a reviewed, audited commit.</li>
</ul>
<p>See <a class="" href="https://docs.kuberocketci.io/docs/basic-concepts">Basic Concepts</a> and the <a class="" href="https://docs.kuberocketci.io/docs/user-guide/tekton-pipelines">Tekton Overview</a> for the full capability matrix.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-are-tekton-pipelines-defined-in-kuberocketci">How Are Tekton Pipelines Defined in KubeRocketCI?<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#how-are-tekton-pipelines-defined-in-kuberocketci" class="hash-link" aria-label="Direct link to How Are Tekton Pipelines Defined in KubeRocketCI?" title="Direct link to How Are Tekton Pipelines Defined in KubeRocketCI?" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-helm-templated-library">The Helm-Templated Library<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#the-helm-templated-library" class="hash-link" aria-label="Direct link to The Helm-Templated Library" title="Direct link to The Helm-Templated Library" translate="no">​</a></h3>
<p>Pipelines in KubeRocketCI are not hand-written one-offs - they are generated by Helm from a structured library in <a href="https://github.com/epam/edp-tekton" target="_blank" rel="noopener noreferrer" class="">edp-tekton</a>. The naming convention follows:</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">{gitProvider}-{buildTool}-{framework}-app-{pipelineType}-{versioning}</span><br></div></code></pre></div></div>
<p>For example, a Java 17 Maven build pipeline triggered from GitHub with default versioning renders as:</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">github-maven-java17-app-build-default</span><br></div></code></pre></div></div>
<p>The same pipeline for GitLab becomes <code>gitlab-maven-java17-app-build-default</code>. This pattern means every combination of git provider, language, framework, and versioning strategy gets a consistently named, independently configurable pipeline - without duplicating task logic. Shared task sequences live in common Helm templates (e.g., <code>_common_java_maven.yaml</code>) and are included by reference.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-real-build-task-sequence">The Real Build Task Sequence<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#the-real-build-task-sequence" class="hash-link" aria-label="Direct link to The Real Build Task Sequence" title="Direct link to The Real Build Task Sequence" translate="no">​</a></h3>
<p>For a Java Maven application, the build pipeline executes these tasks in 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">get-version</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  └─ update-build-number</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">       └─ get-cache</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">            └─ compile</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                 └─ test  (JaCoCo coverage)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                      └─ sonar  (quality gate: sonar.qualitygate.wait=true)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                           └─ build  (mvn clean package -DskipTests)</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">                                └─ push  (mvn deploy -DskipTests)</span><br></div></code></pre></div></div>
<p>The <code>sonar</code> step blocks with <code>sonar.qualitygate.wait=true</code> - the build fails fast at the quality gate rather than discovering issues downstream. In my experience, this single flag is the difference between catching technical debt in minutes versus after a release candidate is already tagged. Code that doesn't pass SonarQube never produces an artifact.</p>
<p>Each task is a reusable Tekton <code>Task</code> CRD (<code>maven</code>, <code>sonarqube-maven</code>) - the pipeline only sets parameters and <code>runAfter</code> ordering. Swap the <code>maven</code> task image to change the JDK version; the pipeline logic stays the same.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-webhook-to-pipelinerun-chain">The Webhook-to-PipelineRun Chain<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#the-webhook-to-pipelinerun-chain" class="hash-link" aria-label="Direct link to The Webhook-to-PipelineRun Chain" title="Direct link to The Webhook-to-PipelineRun Chain" translate="no">​</a></h3>
<p>When a pull request is merged on GitHub, the event travels through four Tekton resources before a PipelineRun starts:</p>
<!-- -->
<p>The <strong>Custom Interceptor</strong> is the key piece. It enriches the raw GitHub payload with platform-specific <code>extensions</code> that the webhook payload alone doesn't contain:</p>
<ul>
<li class=""><code>extensions.codebase</code> - the KubeRocketCI codebase name for this repository</li>
<li class=""><code>extensions.codebasebranch</code> - the branch object with its pipeline configuration</li>
<li class=""><code>extensions.pipelines.build</code> - the exact pipeline name to run (e.g., <code>github-maven-java17-app-build-default</code>)</li>
<li class=""><code>extensions.pullRequest.headSha</code>, <code>extensions.pullRequest.author</code>, <code>extensions.pullRequest.url</code></li>
</ul>
<p>The <strong>TriggerBinding</strong> then extracts these into named parameters: <code>gitrevision</code>, <code>targetBranch</code>, <code>gitsha</code>, <code>codebase</code>, <code>codebasebranch</code>, <code>pipelineName</code>, <code>commitMessagePattern</code>, and Jira integration fields. The <strong>TriggerTemplate</strong> stamps these into a PipelineRun spec and creates it in the cluster.</p>
<p>This architecture means the decision of <em>which pipeline runs for which codebase</em> is stored in the platform's Kubernetes objects - not hardcoded in webhook configuration.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="pipeline-types-from-code-review-to-release">Pipeline Types: From Code Review to Release<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#pipeline-types-from-code-review-to-release" class="hash-link" aria-label="Direct link to Pipeline Types: From Code Review to Release" title="Direct link to Pipeline Types: From Code Review to Release" translate="no">​</a></h2>
<p>KubeRocketCI defines seven pipeline types, identified by the <code>app.edp.epam.com/pipelinetype</code> label. All are implemented as Tekton Pipelines.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="review-pipeline-pipelinetype-review">Review Pipeline (<code>pipelinetype: review</code>)<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#review-pipeline-pipelinetype-review" class="hash-link" aria-label="Direct link to review-pipeline-pipelinetype-review" title="Direct link to review-pipeline-pipelinetype-review" translate="no">​</a></h3>
<p>Triggered by a pull request. Runs static analysis, linting, unit tests, and code-quality gates before a merge is allowed. Typically completes in under five minutes.</p>
<p>Trigger methods: open a PR against a configured branch, comment <code>/recheck</code> or <code>/ok-to-test</code>, or use the <strong>Run Again</strong> button in the portal.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="build-pipeline-pipelinetype-build">Build Pipeline (<code>pipelinetype: build</code>)<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#build-pipeline-pipelinetype-build" class="hash-link" aria-label="Direct link to build-pipeline-pipelinetype-build" title="Direct link to build-pipeline-pipelinetype-build" translate="no">​</a></h3>
<p>Triggered on merge to the target branch. Executes the full task sequence (see above), builds and pushes a container image, and produces a versioned artifact. Versioning follows either <code>BRANCH-[DATETIME]</code> (default) or SemVer (MAJOR.MINOR.PATCH-BUILD_ID) depending on project configuration.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="deploy-pipeline-pipelinetype-deploy">Deploy Pipeline (<code>pipelinetype: deploy</code>)<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#deploy-pipeline-pipelinetype-deploy" class="hash-link" aria-label="Direct link to deploy-pipeline-pipelinetype-deploy" title="Direct link to deploy-pipeline-pipelinetype-deploy" translate="no">​</a></h3>
<p>Takes a versioned artifact and applies it to a target environment. Trigger modes: manual (via portal), <code>Auto</code> (deploy automatically on new artifact), or <code>Auto-stable</code> (promote only the rebuilt component, keep others at stable versions).</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="clean-pipeline-pipelinetype-clean">Clean Pipeline (<code>pipelinetype: clean</code>)<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#clean-pipeline-pipelinetype-clean" class="hash-link" aria-label="Direct link to clean-pipeline-pipelinetype-clean" title="Direct link to clean-pipeline-pipelinetype-clean" translate="no">​</a></h3>
<p>Tears down an environment on demand. Paired with <code>Auto</code> deploys and dynamic environments, this enables full ephemeral workflows - spin up for a PR, validate, clean up on merge.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="security-pipeline-pipelinetype-security">Security Pipeline (<code>pipelinetype: security</code>)<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#security-pipeline-pipelinetype-security" class="hash-link" aria-label="Direct link to security-pipeline-pipelinetype-security" title="Direct link to security-pipeline-pipelinetype-security" translate="no">​</a></h3>
<p>A standalone pipeline decoupled from build, purpose-built for vulnerability scanning. Scan results surface in the portal's <strong>Results</strong> tab and link through to DefectDojo. Decoupling security means scans can run at any cadence without affecting build cycle time.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="test-pipeline-pipelinetype-tests">Test Pipeline (<code>pipelinetype: tests</code>)<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#test-pipeline-pipelinetype-tests" class="hash-link" aria-label="Direct link to test-pipeline-pipelinetype-tests" title="Direct link to test-pipeline-pipelinetype-tests" translate="no">​</a></h3>
<p>Executes automated test suites against already-deployed environments, independent of the deploy cycle. Teams validate application behavior on demand without triggering a full redeploy.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="release-pipeline-pipelinetype-release">Release Pipeline (<code>pipelinetype: release</code>)<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#release-pipeline-pipelinetype-release" class="hash-link" aria-label="Direct link to release-pipeline-pipelinetype-release" title="Direct link to release-pipeline-pipelinetype-release" translate="no">​</a></h3>
<p>Orchestrates approval and publishing for new releases. KubeRocketCI does not ship a pre-built release pipeline - release governance is organization-specific. The full Tekton API surface is available to build custom approval steps, external system integrations, and auditable release flows.</p>
<p>The complete pipeline type reference is in <a class="" href="https://docs.kuberocketci.io/docs/user-guide/pipelines">Pipelines Overview</a> and <a class="" href="https://docs.kuberocketci.io/docs/user-guide/tekton-pipelines">Tekton Overview</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="customizing-pipelines">Customizing Pipelines<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#customizing-pipelines" class="hash-link" aria-label="Direct link to Customizing Pipelines" title="Direct link to Customizing Pipelines" translate="no">​</a></h2>
<p>The pre-built library covers most tech stacks, but production workloads always have exceptions. KubeRocketCI supports two customization patterns without forking the platform.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="custom-framework--build-tool">Custom Framework / Build Tool<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#custom-framework--build-tool" class="hash-link" aria-label="Direct link to Custom Framework / Build Tool" title="Direct link to Custom Framework / Build Tool" translate="no">​</a></h3>
<p>Define a custom pipeline once with a naming pattern, and every project matching that pattern picks it up automatically. The <a class="" href="https://docs.kuberocketci.io/docs/use-cases/tekton-custom-pipelines">Tekton custom pipelines use case</a> walks through the full authoring flow.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="branch-specific-pipelines">Branch-Specific Pipelines<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#branch-specific-pipelines" class="hash-link" aria-label="Direct link to Branch-Specific Pipelines" title="Direct link to Branch-Specific Pipelines" translate="no">​</a></h3>
<p>When different branches need different logic - <code>main</code> with full integration tests, <code>release</code> with image signing - pipelines are defined explicitly per branch. See the <a class="" href="https://docs.kuberocketci.io/docs/use-cases/custom-pipelines-flow#replace-pipelines-with-personalized-versions">custom pipelines flow guide</a>.</p>
<p>Both approaches follow the same Git-first workflow: write Tekton YAML, apply to the cluster to verify, commit, and let Argo CD synchronize the authoritative state.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-portal-pipeline-management-ui">The Portal: Pipeline Management UI<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#the-portal-pipeline-management-ui" class="hash-link" aria-label="Direct link to The Portal: Pipeline Management UI" title="Direct link to The Portal: Pipeline Management UI" translate="no">​</a></h2>
<p>The KubeRocketCI portal ships a dedicated <code>tekton</code> module with full pipeline management across four page areas: pipeline list, pipeline details, PipelineRun list, and PipelineRun details.</p>
<p><strong>Pipeline list with type filter.</strong> The pipeline list filters by <code>app.edp.epam.com/pipelinetype</code> label - select <code>build</code>, <code>review</code>, <code>deploy</code>, or any of the seven types to narrow the view. Each pipeline shows its type, last run status, and a <strong>Run with params</strong> button.</p>
<p><strong>Run with params.</strong> Clicking <strong>Run with params</strong> fetches the <code>TriggerTemplate</code> linked to the pipeline (via the <code>app.edp.epam.com/triggertemplate</code> label), generates a PipelineRun draft pre-filled with correct parameters, opens a YAML editor for review, and creates the PipelineRun resource via the Kubernetes API on save. No <code>kubectl</code> required.</p>
<p><strong>DAG visualization.</strong> The pipeline details page renders the task graph using React Flow - nodes for each task, directed edges from <code>runAfter</code> dependencies, isolated tasks shown separately. Colors map to live status: blue (running), green (succeeded), red (failed), amber (pending). Finally-tasks are distinguished visually from the main execution chain.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI portal DAG view of a review pipeline - tasks for sonar, helm-lint, dockerfile-lint, and GitHub status reporting" src="https://docs.kuberocketci.io/assets/images/dag-pipeline-visualization-8b8c71a308819a038a39e685720d4ef1.png" width="2000" height="1090" class="img_ev3q"></p>
<p><strong>Live logs and history.</strong> The PipelineRun details page streams live container logs while a run is active. After completion, logs are served from Tekton Results - the same view, no separate log aggregation setup needed.</p>
<p><strong>Inline manual approval.</strong> When a pipeline step is an <code>ApprovalTask</code> (a platform-native CRD), the portal renders Approve / Reject buttons directly in the task view - with an optional comment field. The approval decision (<code>spec.action</code>, <code>spec.approve.approvedBy</code>, <code>spec.approve.comment</code>) is written back to the <code>ApprovalTask</code> resource. This is how release gates and environment promotion approvals are implemented as Kubernetes-native objects.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="getting-started">Getting Started<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#getting-started" class="hash-link" aria-label="Direct link to Getting Started" title="Direct link to Getting Started" translate="no">​</a></h2>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-1---install-the-platform">Step 1 - Install the platform<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#step-1---install-the-platform" class="hash-link" aria-label="Direct link to Step 1 - Install the platform" title="Direct link to Step 1 - Install the platform" translate="no">​</a></h3>
<p>Follow the <a class="" href="https://docs.kuberocketci.io/docs/quick-start/platform-installation">Quick Start: Install KubeRocketCI</a> guide for a local cluster. For production on AWS, see <a class="" href="https://docs.kuberocketci.io/docs/operator-guide/deploy-aws-eks">Deploy on AWS EKS</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-2---install-tekton-components">Step 2 - Install Tekton components<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#step-2---install-tekton-components" class="hash-link" aria-label="Direct link to Step 2 - Install Tekton components" title="Direct link to Step 2 - Install Tekton components" translate="no">​</a></h3>
<p>The <a class="" href="https://docs.kuberocketci.io/docs/operator-guide/install-tekton">Install Tekton</a> guide covers both vanilla Kubernetes and OKD/OpenShift (Tekton Operator). For the fully automated path, see <a class="" href="https://docs.kuberocketci.io/docs/operator-guide/add-ons-overview">Add-Ons Overview</a>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-3---connect-your-git-provider">Step 3 - Connect your Git provider<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#step-3---connect-your-git-provider" class="hash-link" aria-label="Direct link to Step 3 - Connect your Git provider" title="Direct link to Step 3 - Connect your Git provider" translate="no">​</a></h3>
<p>Walk through <a class="" href="https://docs.kuberocketci.io/docs/quick-start/integrate-github">Integrate GitHub</a> or configure GitLab/Bitbucket in the portal under <strong>Git Servers</strong>.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-4---integrate-container-registry-and-code-quality">Step 4 - Integrate container registry and code quality<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#step-4---integrate-container-registry-and-code-quality" class="hash-link" aria-label="Direct link to Step 4 - Integrate container registry and code quality" title="Direct link to Step 4 - Integrate container registry and code quality" translate="no">​</a></h3>
<p><a class="" href="https://docs.kuberocketci.io/docs/quick-start/integrate-container-registry">Integrate DockerHub</a> wires up image push. <a class="" href="https://docs.kuberocketci.io/docs/quick-start/integrate-sonarcloud">Integrate SonarQube</a> activates the quality gate step in the build pipeline.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-5---onboard-your-first-application">Step 5 - Onboard your first application<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#step-5---onboard-your-first-application" class="hash-link" aria-label="Direct link to Step 5 - Onboard your first application" title="Direct link to Step 5 - Onboard your first application" translate="no">​</a></h3>
<p><a class="" href="https://docs.kuberocketci.io/docs/quick-start/create-application">Create Application</a> registers your codebase. The platform provisions a review and build pipeline for the default branch automatically - the Helm-templated library selects the correct pipeline name based on your tech stack.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="step-6---configure-a-deployment-environment">Step 6 - Configure a deployment environment<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#step-6---configure-a-deployment-environment" class="hash-link" aria-label="Direct link to Step 6 - Configure a deployment environment" title="Direct link to Step 6 - Configure a deployment environment" translate="no">​</a></h3>
<p><a class="" href="https://docs.kuberocketci.io/docs/quick-start/deploy-application">Deploy Application</a> connects Argo CD and configures the first CD pipeline stage. <a class="" href="https://docs.kuberocketci.io/docs/quick-start/integrate-argocd">Integrate Argo CD</a> covers the Argo CD setup.</p>
<p>From a fresh cluster to a working build-and-deploy loop typically takes a few hours.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-every-onboarded-app-gets-automatically">What Every Onboarded App Gets Automatically<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#what-every-onboarded-app-gets-automatically" class="hash-link" aria-label="Direct link to What Every Onboarded App Gets Automatically" title="Direct link to What Every Onboarded App Gets Automatically" translate="no">​</a></h2>
<p>Once the platform is running, every onboarded application has:</p>
<ul>
<li class="">A <strong>review pipeline</strong> on every pull request - compile, test, SonarQube quality gate.</li>
<li class="">A <strong>build pipeline</strong> on every merge - versioned artifact, container image pushed, Tekton Chains provenance attestation.</li>
<li class="">A <strong>CD pipeline</strong> connected to Argo CD - promoting artifacts through configured stages.</li>
<li class=""><strong>Pipeline definitions in Git</strong> - every change to a pipeline goes through code review, synchronized by Argo CD.</li>
<li class=""><strong>Live + archived logs</strong> - streaming during a run, served from Tekton Results after completion.</li>
<li class=""><strong>Manual approval gates</strong> - inline approve/reject in the portal for any pipeline step that requires human sign-off.</li>
</ul>
<p>This is what our own dogfooding instance looks like - we run KubeRocketCI on KubeRocketCI itself. Over 90 days, the <code>krci</code> namespace logged <strong>18,397 pipeline runs</strong> (~200/day across build, review, and deploy types), with build pipelines hitting a <strong>93% success rate</strong> at an average duration of <strong>14 minutes 38 seconds</strong>. No external CI server involved.</p>
<p><img decoding="async" loading="lazy" alt="KubeRocketCI Pipeline Metrics: 18,397 runs over 90 days, 74% success rate, 93% build success rate, 14m 38s average duration" src="https://docs.kuberocketci.io/assets/images/pipeline-metrics-c39298a912c421e0e581550d8fbbed0d.png" width="2000" height="1130" class="img_ev3q"></p>
<p>The entire path - review pipeline on PR open, build pipeline on merge, artifact versioning, image push, and Argo CD sync - runs without the developer writing a single line of Jenkins Groovy or pipeline YAML. The only required input is declaring the tech stack at onboarding time.</p>
<p>No manual wiring required. The opinionated defaults deliver a production-grade setup from day one, and the full Tekton API surface is available when you need to diverge.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="next-steps">Next Steps<a href="https://docs.kuberocketci.io/blog/kubernetes-native-cicd-tekton-kuberocketci#next-steps" class="hash-link" aria-label="Direct link to Next Steps" title="Direct link to Next Steps" translate="no">​</a></h2>
<ul>
<li class="">Explore the <a class="" href="https://docs.kuberocketci.io/docs/user-guide/tekton-pipelines">Tekton Overview</a> for the complete pipeline type and trigger reference.</li>
<li class="">Try the <a class="" href="https://docs.kuberocketci.io/docs/use-cases/application-scaffolding">application scaffolding use case</a> to see the platform generate a project skeleton with a working CI/CD pipeline in minutes.</li>
<li class="">Follow <a class="" href="https://docs.kuberocketci.io/docs/use-cases/autotest-as-quality-gate">autotest as quality gate</a> to promote automatically only when integration tests pass.</li>
<li class="">Read <a class="" href="https://docs.kuberocketci.io/docs/use-cases/deploy-application-from-feature-branch">deploy application from a feature branch</a> for ephemeral environment patterns.</li>
</ul>
<p>KubeRocketCI is open-source under Apache License 2.0. Platform source, Helm charts, and the full Tekton pipeline library are on <a href="https://github.com/epam/edp-tekton" target="_blank" rel="noopener noreferrer" class="">GitHub</a>.</p>]]></content>
        <author>
            <name>Sergiy Kulanov</name>
            <uri>https://github.com/sergk</uri>
        </author>
        <category label="KubeRocketCI" term="KubeRocketCI"/>
        <category label="Tekton" term="Tekton"/>
        <category label="Kubernetes" term="Kubernetes"/>
        <category label="CI/CD" term="CI/CD"/>
        <category label="DevOps" term="DevOps"/>
        <category label="Platform Engineering" term="Platform Engineering"/>
        <category label="GitOps" term="GitOps"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Integrating OIDC Authentication with Microsoft Entra in AWS EKS]]></title>
        <id>https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks</id>
        <link href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks"/>
        <updated>2024-12-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Microsoft Entra OIDC integration secures AWS EKS with Single Sign-On: configure OIDC, RBAC, and KubeRocketCI Portal login step by step.]]></summary>
        <content type="html"><![CDATA[<p>In modern cloud environments, secure and efficient access management is essential, especially for platforms like Amazon EKS. This blog will guide you through integrating OpenID Connect (OIDC) authentication using Microsoft Entra, making it easier to manage access to your EKS clusters and KubeRocketCI Portal. By implementing this approach, you can simplify user authentication while ensuring strong security controls. Whether you're improving compliance or streamlining access for your team, this integration is a practical solution to enhance your cloud-native workflows.</p>
<div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_BuS1"><p><strong>Updated: July 2026</strong> — Links verified and integration steps re-checked against the current Microsoft Entra admin center.</p></div></div>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="prerequisites">Prerequisites<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#prerequisites" class="hash-link" aria-label="Direct link to Prerequisites" title="Direct link to Prerequisites" translate="no">​</a></h2>
<p>Before you begin, ensure you have the following:</p>
<ul>
<li class="">Access to the <a href="https://entra.microsoft.com/" target="_blank" rel="noopener noreferrer" class="">Microsoft Entra Admin Center</a> with administrative privileges.</li>
<li class="">A running <a href="https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html" target="_blank" rel="noopener noreferrer" class="">AWS EKS</a> cluster with the necessary permissions for access and management.</li>
<li class="">The <a href="https://github.com/int128/kubelogin" target="_blank" rel="noopener noreferrer" class="">kubelogin</a> plugin installed for authenticating to the EKS cluster using OIDC.</li>
<li class="">The <a href="https://kubernetes.io/docs/tasks/tools/#kubectl" target="_blank" rel="noopener noreferrer" class="">kubectl</a> CLI tool installed.</li>
<li class="">The <a href="https://aws.amazon.com/cli/" target="_blank" rel="noopener noreferrer" class="">aws cli</a> tool installed.</li>
</ul>
<p>If you use Keycloak instead of Microsoft Entra, see <a class="" href="https://docs.kuberocketci.io/docs/operator-guide/auth/configure-keycloak-oidc-eks">Keycloak OIDC for EKS</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="understanding-sso-oidc-and-microsoft-entra">Understanding SSO, OIDC, and Microsoft Entra<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#understanding-sso-oidc-and-microsoft-entra" class="hash-link" aria-label="Direct link to Understanding SSO, OIDC, and Microsoft Entra" title="Direct link to Understanding SSO, OIDC, and Microsoft Entra" translate="no">​</a></h2>
<p>In the context of enhancing digital security and user experience, we prioritize the integration of three key elements: Single Sign-On (SSO), OpenID Connect (OIDC), and Microsoft Entra. Here’s how they connect:</p>
<ul>
<li class="">
<p><strong>Single Sign-On (SSO)</strong> serves as the foundation, enabling users to access multiple applications with one set of login credentials, significantly simplifying the authentication process.</p>
</li>
<li class="">
<p><strong>OpenID Connect (OIDC)</strong> builds on the SSO framework by providing an authentication layer, which uses straightforward identity verification to ensure secure and seamless access across services.</p>
</li>
<li class="">
<p><strong>Microsoft Entra</strong> (formerly known as <strong>Azure Active Directory</strong>) is Microsoft's comprehensive identity and access management solution. It supports the implementation of both Single Sign-On (SSO) and OpenID Connect (OIDC), enabling organizations to securely manage user identities and enforce access controls. With its reliable set of tools, Microsoft Entra simplifies authentication, enhances security, and ensures seamless access to applications and services, making it an essential platform for modern identity management.</p>
</li>
</ul>
<p>Together, these technologies streamline the login process, reinforce security, and enhance the user experience by allowing secure, seamless navigation across our digital ecosystem.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="microsoft-entra-overview">Microsoft Entra Overview<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#microsoft-entra-overview" class="hash-link" aria-label="Direct link to Microsoft Entra Overview" title="Direct link to Microsoft Entra Overview" translate="no">​</a></h3>
<p>Microsoft Entra, formerly known as Azure Active Directory (Azure AD), is a modern identity and access management solution designed for secure access to applications and services. It provides features like Single Sign-On (SSO), identity federation, and seamless integration with on-premises directories such as LDAP and Active Directory. Microsoft Entra supports industry-standard protocols, including OpenID Connect (OIDC), OAuth 2.0, and Security Assertion Markup Language (SAML) 2.0, making it a versatile solution for managing user identities. By leveraging Microsoft Entra, organizations can enhance security, simplify user access, and avoid the complexities of building identity management features from scratch. For more details, see the <a href="https://learn.microsoft.com/en-gb/entra/identity/" target="_blank" rel="noopener noreferrer" class="">Microsoft Entra official documentation</a>.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="create-a-new-microsoft-entra-tenant">Create a new Microsoft Entra Tenant<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#create-a-new-microsoft-entra-tenant" class="hash-link" aria-label="Direct link to Create a new Microsoft Entra Tenant" title="Direct link to Create a new Microsoft Entra Tenant" translate="no">​</a></h2>
<p>To get started with Microsoft Entra, you need to create a new tenant in the Microsoft Entra Admin Center. Follow these steps:</p>
<ol>
<li class="">
<p>Log in to the <a href="https://entra.microsoft.com/" target="_blank" rel="noopener noreferrer" class="">Microsoft Entra Admin Center</a> using your Microsoft account.</p>
<p><img decoding="async" loading="lazy" alt="Microsoft Entra Admin Center" src="https://docs.kuberocketci.io/assets/images/microsoft-entra-admin-center-8a597425e8e1e33a8867e316ee37be41.png" width="3364" height="1648" class="img_ev3q"></p>
</li>
<li class="">
<p>In the left sidebar menu, select <strong>Overview</strong> section and then navigate to <strong>Manage tenants</strong> tab.</p>
<p><img decoding="async" loading="lazy" alt="Manage Tenants" src="https://docs.kuberocketci.io/assets/images/manage-tenants-c260f4975163bf0c5cb83eeb99217660.png" width="3364" height="1648" class="img_ev3q"></p>
</li>
<li class="">
<p>Click on <strong>Create</strong> button to create a new tenant.</p>
<p><img decoding="async" loading="lazy" alt="Create Tenant" src="https://docs.kuberocketci.io/assets/images/create-tenant-ea455de07150c662e89cc2896be821ff.png" width="3364" height="1648" class="img_ev3q"></p>
</li>
<li class="">
<p>Select the configuration type <strong>Workforce</strong>.</p>
<p><img decoding="async" loading="lazy" alt="Configuration Type" src="https://docs.kuberocketci.io/assets/images/configuration-type-31ef85a58475500f22b83a05c93b4276.png" width="3364" height="1648" class="img_ev3q"></p>
</li>
<li class="">
<p>Fill in the fields for <strong>Tenant Name</strong>, <strong>Domain Name</strong>, and <strong>Location</strong>.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>The <strong>Tenant Name</strong> and <strong>Domain Name</strong> <code>kuberocketci</code> are used as a demonstration examples. In your case, it is recommended to choose names that align with your organization's specific needs and naming conventions.</p></div></div>
<p><img decoding="async" loading="lazy" alt="Tenant Details" src="https://docs.kuberocketci.io/assets/images/tenant-details-6753dbbdbe872963be3e49632442654b.png" width="3364" height="1648" class="img_ev3q"></p>
</li>
<li class="">
<p>The new tenant will be created, and you can start configuring it for OIDC integration. Ensure you have switched to the new tenant.</p>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="creating-and-configuring-oidc-application-in-microsoft-entra">Creating and Configuring OIDC Application in Microsoft Entra<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#creating-and-configuring-oidc-application-in-microsoft-entra" class="hash-link" aria-label="Direct link to Creating and Configuring OIDC Application in Microsoft Entra" title="Direct link to Creating and Configuring OIDC Application in Microsoft Entra" translate="no">​</a></h2>
<p>After creating the tenant, you need to set up an OIDC Application in Microsoft Entra. Here's how you can do it:</p>
<ol>
<li class="">
<p>In the Microsoft Entra Admin Center, in the left sidebar menu, select <strong>Applications</strong> and then click on <strong>App registrations</strong>.</p>
<p><img decoding="async" loading="lazy" alt="App Registrations" src="https://docs.kuberocketci.io/assets/images/app-registrations-c04b119572cc459069182f6aeab95995.png" width="3364" height="1648" class="img_ev3q"></p>
</li>
<li class="">
<p>Click on the <strong>New registration</strong> button to create a new application.</p>
<p><img decoding="async" loading="lazy" alt="New Registration" src="https://docs.kuberocketci.io/assets/images/new-registration-63846c96d5ff493abbc9903c638fb6cf.png" width="3364" height="1648" class="img_ev3q"></p>
</li>
<li class="">
<p>Fill in the details for the application, such as <strong>Name</strong>, <strong>Supported account types</strong>, and <strong>Redirect URI</strong> (<code>http://localhost:8000/</code>).</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>The <strong>Name</strong> <code>kuberocketci</code> is used as a demonstration example. In your case, it is recommended to choose a name that aligns with your application's specific needs and naming conventions (e.g. your AWS EKS cluster name).</p></div></div>
<p><img decoding="async" loading="lazy" alt="Application Details" src="https://docs.kuberocketci.io/assets/images/application-details-b0691226247a0b3f611936922040e4b6.png" width="3364" height="1648" class="img_ev3q"></p>
</li>
<li class="">
<p>In the created application, navigate to the <strong>Authentication</strong> section from the left sidebar menu. In the <strong>Implicit grant and hybrid flows</strong> section, select <strong>ID tokens</strong> for the token type. In the <strong>Allow public client flows</strong> section, set the value to <strong>No</strong>.</p>
<p><img decoding="async" loading="lazy" alt="Authentication Settings" src="https://docs.kuberocketci.io/assets/images/authentication-settings-b3ad780dbc75b519a96e4138242666dd.png" width="3364" height="1647" class="img_ev3q"></p>
</li>
<li class="">
<p>Navigate to the <strong>Certificates &amp; secrets</strong> section from the left sidebar menu. In the <strong>Client secrets</strong> tab, click on the <strong>New client secret</strong> button to create a new secret.</p>
<p><img decoding="async" loading="lazy" alt="Client Secret" src="https://docs.kuberocketci.io/assets/images/client-secret-2adb6894937ed5c8e84c077423b51fbb.png" width="3364" height="1647" class="img_ev3q"></p>
</li>
<li class="">
<p>Copy the generated client secret value and store it securely.</p>
<p><img decoding="async" loading="lazy" alt="Client Secret Value" src="https://docs.kuberocketci.io/assets/images/client-secret-value-26326b1338acafd94f8e5a647f6407f4.png" width="3364" height="1647" class="img_ev3q"></p>
</li>
<li class="">
<p>Navigate to the <strong>Token configuration</strong> section and click on <strong>Add group claim</strong> button. Choose the group type as <strong>Security Groups</strong> and for the ID token type, select <strong>Group ID</strong>.</p>
<p><img decoding="async" loading="lazy" alt="Token Configuration" src="https://docs.kuberocketci.io/assets/images/token-configuration-0fa9c3f0e471ac1b1ffcf11f69bee22c.png" width="3364" height="1647" class="img_ev3q"></p>
</li>
<li class="">
<p>(Optional) Additionally, add an optional <strong>upn</strong> claim in the <strong>Token configuration</strong> section.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>This step is optional and should only be performed if external users (i.e., users with <strong>User type: Guest</strong>) will be added to the Microsoft Entra Tenant and require access to the Application.</p></div></div>
<p><img decoding="async" loading="lazy" alt="upn Claim" src="https://docs.kuberocketci.io/assets/images/upn-claim-0ae2415ae51d8eab1bfce10492cbf021.png" width="3362" height="1660" class="img_ev3q"></p>
</li>
<li class="">
<p>(Optional) After adding the <strong>upn</strong> claim, click on the three dots next to it, select <strong>Edit</strong>, and turn on the <strong>Externally authenticated</strong> toggle to <strong>Yes</strong> value.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>This step is optional and should only be performed if external users (i.e., users with <strong>User type: Guest</strong>) will be added to the Microsoft Entra Tenant and require access to the Application.</p></div></div>
<p><img decoding="async" loading="lazy" alt="Externally Authenticated" src="https://docs.kuberocketci.io/assets/images/externally-authenticated-1bfd6efbb519c1633eb00d846fd99dc6.png" width="3362" height="1660" class="img_ev3q"></p>
</li>
<li class="">
<p>Navigate to the <strong>API permissions</strong> section. Ensure that the <strong>User.Read</strong> permission is added under the <strong>Microsoft Graph</strong> API. If not, click on the <strong>Add a permission</strong> button, select <strong>Microsoft Graph</strong>, and add the <strong>User.Read</strong> permission. After adding the permission, click on the <strong>Grant admin consent for 'Tenant name'</strong> button to grant the required permissions.</p>
<p><img decoding="async" loading="lazy" alt="API Permissions" src="https://docs.kuberocketci.io/assets/images/api-permissions-ec305b38ce662e114e3e614ceabca58a.png" width="3364" height="1647" class="img_ev3q"></p>
</li>
<li class="">
<p>(Optional) Additionally, for <strong>Microsoft Graph</strong> API, add the <strong>OpenId</strong> permissions, such as <strong>openid</strong>, <strong>email</strong>, and <strong>profile</strong>.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>This step is optional and should only be performed if external users (i.e., users with <strong>User type: Guest</strong>) will be added to the Microsoft Entra Tenant and require access to the Application.</p></div></div>
<p><img decoding="async" loading="lazy" alt="OpenID Permissions" src="https://docs.kuberocketci.io/assets/images/openid-permissions-f130d9bd007be3509b7f33b48e6e488d.png" width="3362" height="1660" class="img_ev3q"></p>
</li>
<li class="">
<p>The OIDC Application in Microsoft Entra is now configured and ready for integration with AWS EKS.</p>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="creating-users-and-groups-in-microsoft-entra">Creating Users and Groups in Microsoft Entra<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#creating-users-and-groups-in-microsoft-entra" class="hash-link" aria-label="Direct link to Creating Users and Groups in Microsoft Entra" title="Direct link to Creating Users and Groups in Microsoft Entra" translate="no">​</a></h2>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>Only users who are part of the groups configured in the Microsoft Entra Admin Center will be able to authenticate to the AWS EKS cluster using OIDC.</p></div></div>
<p>To create users and groups in Microsoft Entra, follow these steps:</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="creating-a-group">Creating a Group<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#creating-a-group" class="hash-link" aria-label="Direct link to Creating a Group" title="Direct link to Creating a Group" translate="no">​</a></h3>
<ol>
<li class="">
<p>In the Microsoft Entra Admin Center, in the left sidebar menu, select <strong>Groups</strong> and then <strong>All groups</strong>. Click on <strong>New group</strong> button to create a new group(s) for users who will have access to the AWS EKS cluster.</p>
<p><img decoding="async" loading="lazy" alt="New Group" src="https://docs.kuberocketci.io/assets/images/new-group-057769fdc60e88276e453cfe1d13520a.png" width="3364" height="1647" class="img_ev3q"></p>
</li>
<li class="">
<p>Fill in the details for the group, such as <strong>Group type</strong> and <strong>Group name</strong>. Click on the <strong>Create</strong> button to create the group.</p>
<p><img decoding="async" loading="lazy" alt="Group Details" src="https://docs.kuberocketci.io/assets/images/group-details-f699a47eca753f3e4f7fed9bd27ac86d.png" width="3364" height="1647" class="img_ev3q"></p>
</li>
<li class="">
<p>The group will be created, and you can start adding users to it.</p>
</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="adding-users-to-the-group">Adding Users to the Group<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#adding-users-to-the-group" class="hash-link" aria-label="Direct link to Adding Users to the Group" title="Direct link to Adding Users to the Group" translate="no">​</a></h3>
<ol>
<li class="">
<p>In the Microsoft Entra Admin Center, in the left sidebar menu, select <strong>Users</strong> and then click on <strong>All users</strong>. In the <strong>New user</strong> tab, click on the <strong>Create new user</strong> button to create a new user.</p>
<p><img decoding="async" loading="lazy" alt="New User" src="https://docs.kuberocketci.io/assets/images/new-user-8d00fa515a4025b42a98f013a37d9a8f.png" width="3364" height="1648" class="img_ev3q"></p>
</li>
<li class="">
<p>Fill in the details for the user, such as <strong>User principal name</strong>, <strong>Mail nickname</strong>, <strong>Display name</strong>, and temporary password. In the <strong>Properties</strong> tab you can set the <strong>First name</strong>, <strong>Last name</strong>, and other details.</p>
<p><img decoding="async" loading="lazy" alt="User Details" src="https://docs.kuberocketci.io/assets/images/user-details-b5f580cd6fe8db3a519fd95a7d42fdc5.png" width="3364" height="1647" class="img_ev3q"></p>
</li>
<li class="">
<p>In the <strong>Assignment</strong> tab, click on the <strong>Add group</strong> button. In the <strong>Select Group</strong> window, choose the group(s) you created earlier (e.g., <code>oidc-cluster-admins</code>) and click on the <strong>Select</strong> button.</p>
<p><img decoding="async" loading="lazy" alt="Add Group" src="https://docs.kuberocketci.io/assets/images/add-group-ad90704de9b5399c8ce85605ca530a7d.png" width="3364" height="1647" class="img_ev3q"></p>
</li>
<li class="">
<p>Click on the <strong>Review + create</strong> button to create the user. The user will be created and added to the group(s) you selected.</p>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="configuring-microsoft-entra-as-an-identity-provider-in-aws-eks">Configuring Microsoft Entra as an Identity Provider in AWS EKS<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#configuring-microsoft-entra-as-an-identity-provider-in-aws-eks" class="hash-link" aria-label="Direct link to Configuring Microsoft Entra as an Identity Provider in AWS EKS" title="Direct link to Configuring Microsoft Entra as an Identity Provider in AWS EKS" translate="no">​</a></h2>
<p>There are two methods to configure Microsoft Entra as an Identity Provider in AWS EKS: through the AWS Management Console and using Terraform.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>The Application data, such as <strong>Directory (tenant) ID</strong>, <strong>Application (client) ID</strong>, and <strong>Issuer URL</strong>, can be found in the <strong>Overview</strong> section of the OIDC Application in the Microsoft Entra Admin Center.
<img decoding="async" loading="lazy" alt="Application Data" src="https://docs.kuberocketci.io/assets/images/application-data-79ef843fc1e677bbbc0597ce1b9c2f50.png" width="1306" height="664" class="img_ev3q"></p></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="method-1-using-the-aws-management-console">Method 1: Using the AWS Management Console<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#method-1-using-the-aws-management-console" class="hash-link" aria-label="Direct link to Method 1: Using the AWS Management Console" title="Direct link to Method 1: Using the AWS Management Console" translate="no">​</a></h3>
<ol>
<li class="">
<p>Log in to the <a href="https://aws.amazon.com/console/" target="_blank" rel="noopener noreferrer" class="">AWS Management Console</a> and navigate to the <a href="https://console.aws.amazon.com/eks" target="_blank" rel="noopener noreferrer" class="">Amazon EKS console</a>. Select the EKS cluster you want to configure and click on the <strong>Access</strong> tab.</p>
<p><img decoding="async" loading="lazy" alt="EKS Cluster Access Tab" src="https://docs.kuberocketci.io/assets/images/eks-cluster-access-tab-707d67886863028877f32b1630411b4e.png" width="3356" height="1180" class="img_ev3q"></p>
</li>
<li class="">
<p>In the <strong>OIDC identity providers</strong> section, click on the <strong>Associate identity provider</strong> button.</p>
<p><img decoding="async" loading="lazy" alt="Associate Identity Provider" src="https://docs.kuberocketci.io/assets/images/associate-identity-provider-8d81c9b345b76eb0bf49961342edde54.png" width="3356" height="1642" class="img_ev3q"></p>
</li>
<li class="">
<p>Fill in the following details:</p>
<ul>
<li class="">
<p><strong>Name</strong>: <code>Entra</code></p>
</li>
<li class="">
<p><strong>Issuer URL</strong>: <code>https://login.microsoftonline.com/&lt;Tenant-ID&gt;/</code>, where <code>&lt;Tenant-ID&gt;</code> is the <strong>Directory</strong> (tenant) <strong>ID</strong>. Ensure that the URL ends with <code>/</code>.</p>
</li>
<li class="">
<p><strong>Client ID</strong>: <code>&lt;Application (client) ID&gt;</code>, which corresponds to the <strong>Application</strong> (client) <strong>ID</strong> of the OIDC Application.</p>
</li>
<li class="">
<p><strong>Username Claim</strong>: <code>upn</code>.</p>
</li>
<li class="">
<p><strong>Groups Claim</strong>: <code>groups</code>.</p>
<p><img decoding="async" loading="lazy" alt="Identity Provider Details" src="https://docs.kuberocketci.io/assets/images/identity-provider-details-43942f4911d1c02742267b8b19d106cb.png" width="1506" height="1022" class="img_ev3q"></p>
</li>
</ul>
</li>
<li class="">
<p>The process of applying the changes may take a few minutes. Once completed, the Microsoft Entra OIDC identity provider will be associated with the AWS EKS cluster.</p>
</li>
</ol>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="method-2-using-terraform">Method 2: Using Terraform<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#method-2-using-terraform" class="hash-link" aria-label="Direct link to Method 2: Using Terraform" title="Direct link to Method 2: Using Terraform" translate="no">​</a></h3>
<p>To configure Microsoft Entra as an Identity Provider in AWS EKS using Terraform, you can use <a href="https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/20.30.1" target="_blank" rel="noopener noreferrer" class="">AWS EKS Terraform module</a>. Here's an example of how you can do it:</p>
<ul>
<li class=""><strong>variables.tf</strong>:</li>
</ul>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">variable</span><span class="token keyword type variable" style="color:#36acaa"> "cluster_identity_providers" </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 property" style="color:#36acaa">description</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"Configuration for OIDC identity provider"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">type</span><span class="token plain">        </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> any</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">default</span><span class="token plain">     </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</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><br></div></code></pre></div></div>
<ul>
<li class=""><strong>terraform.tfvars</strong>:</li>
</ul>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token property" style="color:#36acaa">cluster_identity_providers</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> </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 property" style="color:#36acaa">entra</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> </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 property" style="color:#36acaa">client_id</span><span class="token plain">    </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"&lt;Application (client) ID&gt;"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">issuer_url</span><span class="token plain">   </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"https://sts.windows.net/&lt;Tenant ID&gt;/"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">groups_claim</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"groups"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token property" style="color:#36acaa">username_claim</span><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"upn"</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><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<ul>
<li class=""><strong>main.tf</strong>:</li>
</ul>
<div class="language-hcl codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-hcl codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">module</span><span class="token keyword type variable" style="color:#36acaa"> "eks" </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 property" style="color:#36acaa">source</span><span class="token plain">  </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"terraform-aws-modules/eks/aws"</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">version</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"20.14.0"</span><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><span class="token comment" style="color:#999988;font-style:italic"># OIDC Identity provider</span><span class="token plain"></span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token property" style="color:#36acaa">cluster_identity_providers</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">=</span><span class="token plain"> var.cluster_identity_providers</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></div></code></pre></div></div>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>You may notice the issuer URL format differs between methods: the AWS Console example above uses <code>https://login.microsoftonline.com/&lt;Tenant-ID&gt;/</code> (Entra v2), while this Terraform example uses <code>https://sts.windows.net/&lt;Tenant ID&gt;/</code> (the legacy Entra v1 format). Both are valid, but the issuer you configure must match the <code>iss</code> claim of the tokens your app registration actually issues, so check the app's manifest <code>accessTokenAcceptedVersion</code> setting before choosing one.</p></div></div>
<p>After applying the Terraform configuration, the Microsoft Entra OIDC identity provider will be associated with the AWS EKS cluster.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="configuring-rbac-resources-in-aws-eks-cluster-for-microsoft-entra-user-groups">Configuring RBAC Resources in AWS EKS cluster for Microsoft Entra User Groups<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#configuring-rbac-resources-in-aws-eks-cluster-for-microsoft-entra-user-groups" class="hash-link" aria-label="Direct link to Configuring RBAC Resources in AWS EKS cluster for Microsoft Entra User Groups" title="Direct link to Configuring RBAC Resources in AWS EKS cluster for Microsoft Entra User Groups" translate="no">​</a></h2>
<p>In this section, user authorization will be configured using Kubernetes Role-Based Access Control (RBAC). Microsoft Entra groups will be linked to Kubernetes ClusterRoles through ClusterRoleBinding resources, enabling precise control over resource access within the EKS cluster. Additionally, Roles and RoleBindings can be used for more granular access control within specific namespaces.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>The <strong>Object ID</strong> of the Microsoft Entra group can be found in the <strong>Overview</strong> section of the group in the Microsoft Entra Admin Center.
<img decoding="async" loading="lazy" alt="Group Object ID" src="https://docs.kuberocketci.io/assets/images/group-object-id-074d9c1a6ef1ebdd351442b25e07f37d.png" width="1504" height="1090" class="img_ev3q"></p></div></div>
<ol>
<li class="">
<p>Log in to the AWS EKS cluster and create the following <strong>ClusterRoleBinding</strong> resource, which associates the Microsoft Entra group <code>oidc-cluster-admins</code> with the <code>cluster-admin</code> Kubernetes Cluster Role. Replace <code>&lt;your-microsoft-entra-admin-group-object-id&gt;</code> with the Object ID of the <code>oidc-cluster-admins</code> group, which can be found on the <code>Group</code> overview page in the Microsoft Entra admin center.</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">apiVersion</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> rbac.authorization.k8s.io/v1</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">kind</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ClusterRoleBinding</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">metadata</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">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> oidc</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">cluster</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">admins</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">roleRef</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">apiGroup</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> rbac.authorization.k8s.io</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">kind</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> ClusterRole</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> cluster</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">admin</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key atrule" style="color:#00a4db">subjects</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">kind</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> Group</span><br></div><div class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token key atrule" style="color:#00a4db">name</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> &lt;your</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">microsoft</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">entra</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">admin</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">group</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">object</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">id</span><span class="token punctuation" style="color:#393A34">&gt;</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">apiGroup</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> rbac.authorization.k8s.io</span><br></div></code></pre></div></div>
<p>Save the above YAML to a file, for example, <code>clusterrolebinding.yaml</code>, and apply it to the EKS cluster using the following command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">kubectl apply </span><span class="token parameter variable" style="color:#36acaa">-f</span><span class="token plain"> clusterrolebinding.yaml</span><br></div></code></pre></div></div>
<p>The <code>oidc-cluster-admins</code> group will now have <code>cluster-admin</code> permissions within the EKS cluster.</p>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="authenticating-to-aws-eks-using-microsoft-entra-with-kubectl">Authenticating to AWS EKS using Microsoft Entra with kubectl<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#authenticating-to-aws-eks-using-microsoft-entra-with-kubectl" class="hash-link" aria-label="Direct link to Authenticating to AWS EKS using Microsoft Entra with kubectl" title="Direct link to Authenticating to AWS EKS using Microsoft Entra with kubectl" translate="no">​</a></h2>
<p>To authenticate to the AWS EKS cluster using Microsoft Entra, you can use the <code>kubectl</code> CLI tool with the <code>kubelogin</code> plugin. The <code>kubelogin</code> plugin simplifies the OIDC authentication process by handling the token exchange and session management. Here's how you can authenticate to the EKS cluster:</p>
<ol>
<li class="">
<p>Create or update the kubeconfig file with the OIDC configuration. Replace <code>&lt;cluster-name&gt;</code> with the name of your EKS cluster and <code>&lt;region-code&gt;</code> with the AWS region code where the cluster is located.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">aws eks update-kubeconfig </span><span class="token parameter variable" style="color:#36acaa">--region</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">region-code</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">--name</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">cluster-name</span><span class="token operator" style="color:#393A34">&gt;</span><br></div></code></pre></div></div>
</li>
<li class="">
<p>Execute the following command to create a new kubeconfig context using the <code>kubelogin</code> plugin. Replace <code>&lt;cluster-name&gt;</code> with the name of your EKS cluster.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">kubectl config set-credentials </span><span class="token string" style="color:#e3116c">"eks"</span><span class="token plain"> </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">  --exec-api-version</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">client.authentication.k8s.io/v1beta1 </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">  --exec-command</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">kubelogin </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">  --exec-arg</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">get-token </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">  --exec-arg</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">--oidc-issuer-url </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">  --exec-arg</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">https://sts.windows.net/</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">Tenant ID</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain">/ </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">  --exec-arg</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">--oidc-client-id </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">  --exec-arg</span><span class="token operator" style="color:#393A34">=</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">Application </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">client</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> ID</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </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">  --exec-arg</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">--oidc-client-secret </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">  --exec-arg</span><span class="token operator" style="color:#393A34">=</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">Application </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">client</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> Secret</span><span class="token operator" style="color:#393A34">&gt;</span><br></div></code></pre></div></div>
<p>Replace <code>&lt;Tenant ID&gt;</code>, <code>&lt;Application (client) ID&gt;</code>, and <code>&lt;Application (client) Secret&gt;</code> with the corresponding values from the OIDC Application in the Microsoft Entra Admin Center.</p>
<div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>You can test the authentication to the EKS cluster immediately by running the following command:</p><div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">kubectl </span><span class="token parameter variable" style="color:#36acaa">--user</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">eks get nodes</span><br></div></code></pre></div></div></div></div>
</li>
<li class="">
<p>Set the context for the kubeconfig file to use the <code>eks</code> user and the OIDC configuration. Replace <code>&lt;cluster-name&gt;</code> with the name of your EKS cluster.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">kubectl config set-context eks </span><span class="token parameter variable" style="color:#36acaa">--user</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">eks </span><span class="token parameter variable" style="color:#36acaa">--cluster</span><span class="token operator" style="color:#393A34">=</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token plain">cluster-name</span><span class="token operator" style="color:#393A34">&gt;</span><br></div></code></pre></div></div>
<p>To switch to the <code>eks</code> context, execute the following command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">kubectl config use-context eks</span><br></div></code></pre></div></div>
<p>Test the authentication by running the following command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash 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">kubectl get nodes</span><br></div></code></pre></div></div>
</li>
</ol>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="configuring-kuberocketci-portal-with-microsoft-entra-oidc-authentication">Configuring KubeRocketCI Portal with Microsoft Entra OIDC Authentication<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#configuring-kuberocketci-portal-with-microsoft-entra-oidc-authentication" class="hash-link" aria-label="Direct link to Configuring KubeRocketCI Portal with Microsoft Entra OIDC Authentication" title="Direct link to Configuring KubeRocketCI Portal with Microsoft Entra OIDC Authentication" translate="no">​</a></h2>
<ol>
<li class="">
<p>Starting from version 3.10, KubeRocketCI platform supports Microsoft Entra as an Identity Provider for OIDC authentication in the Portal UI. To configure Microsoft Entra OIDC authentication, navigate to the <a href="https://github.com/epam/edp-install" target="_blank" rel="noopener noreferrer" class="">edp-install</a> Helm chart repository and set the following values in the <code>values.yaml</code> file:</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 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">edp-headlamp</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">enabled</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</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">config</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">oidc</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">enabled</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token boolean important" style="color:#36acaa">true</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">issuerUrl</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"https://sts.windows.net/&lt;Tenant ID&gt;/"</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">clientID</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"&lt;Application (client) ID&gt;"</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">clientSecretName</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"&lt;name of the secret containing the client-secret value&gt;"</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">clientSecretKey</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"clientSecret"</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><br></div></code></pre></div></div>
<p>Replace <code>&lt;Tenant ID&gt;</code> and <code>&lt;Application (client) ID&gt;</code> with the corresponding values from the OIDC Application in the Microsoft Entra Admin Center. Also, specify the name of the Kubernetes Secret containing the Application <strong>Client secret</strong> value in the <code>clientSecretName</code> field.</p>
</li>
<li class="">
<p>In the Microsoft Entra Admin Center, navigate to the created OIDC Application and select the <strong>Authentication</strong> section. In the <strong>Redirect URIs</strong> field, add the URL of the KubeRocketCI Portal, for example, <code>https://portal-&lt;krci-namespace&gt;.&lt;dns-wildcard&gt;/oidc-callback</code>.</p>
<p><img decoding="async" loading="lazy" alt="Redirect URIs" src="https://docs.kuberocketci.io/assets/images/redirect-uris-7fdaed8d83ca7e79234435a232fce0ec.png" width="3364" height="1660" class="img_ev3q"></p>
</li>
<li class="">
<p>After applying the changes, the KubeRocketCI Portal will be configured to use Microsoft Entra OIDC authentication. Users will be able to log in to the Portal using <strong>Sign In</strong> option.</p>
<p><img decoding="async" loading="lazy" alt="Sign In" src="https://docs.kuberocketci.io/assets/images/sign-in-61897514acf347a032e4a3eaa2582c2d.png" width="3364" height="1660" class="img_ev3q"></p>
</li>
</ol>
<p>If the Portal sits behind an OAuth2 proxy, see <a class="" href="https://docs.kuberocketci.io/docs/operator-guide/auth/oauth2-proxy">OAuth2 Proxy</a> for the general setup and <a class="" href="https://docs.kuberocketci.io/docs/operator-guide/microsoft-entra/oauth2-proxy-authentication">Microsoft Entra OAuth2 Proxy Authentication</a> for the Entra-specific configuration.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="conclusion">Conclusion<a href="https://docs.kuberocketci.io/blog/integrating-oidc-authentication-microsoft-entra-aws-eks#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion" translate="no">​</a></h2>
<p>Integrating OpenID Connect (OIDC) authentication with Microsoft Entra in AWS EKS is a powerful way to enhance security and streamline user access management. By leveraging the capabilities of SSO, OIDC, and Microsoft Entra, organizations can simplify authentication processes, enforce access controls, and ensure secure navigation across cloud-native environments. Whether you're managing user identities, enhancing compliance, or improving user experience, this integration provides a robust solution to meet your identity and access management needs. By following the steps outlined in this guide, you can configure Microsoft Entra as an Identity Provider in AWS EKS, authenticate users using OIDC, and secure access to your EKS clusters and KubeRocketCI Portal effectively.</p>
<p><strong>Related reading:</strong></p>
<ul>
<li class=""><a class="" href="https://docs.kuberocketci.io/blog/advanced-aws-eks-management-oidc-keycloak">Advanced AWS EKS Management: Implementing SSO via OIDC and Keycloak</a> - the same OIDC pattern for AWS EKS, using Keycloak as the identity provider instead of Microsoft Entra.</li>
<li class=""><a class="" href="https://docs.kuberocketci.io/docs/operator-guide/auth/configure-keycloak-oidc-eks">Keycloak OIDC for EKS</a> - the docs reference for the Keycloak-based setup.</li>
<li class=""><a class="" href="https://docs.kuberocketci.io/docs/operator-guide/auth/oauth2-proxy">OAuth2 Proxy</a> and <a class="" href="https://docs.kuberocketci.io/docs/operator-guide/microsoft-entra/oauth2-proxy-authentication">Microsoft Entra OAuth2 Proxy Authentication</a> - securing the KubeRocketCI Portal behind an OAuth2 proxy.</li>
</ul>]]></content>
        <author>
            <name>Daniil Nedostup</name>
            <uri>https://github.com/daniil-nedostup</uri>
        </author>
        <category label="KubeRocketCI" term="KubeRocketCI"/>
        <category label="AWS EKS" term="AWS EKS"/>
        <category label="SSO" term="SSO"/>
        <category label="Microsoft Entra" term="Microsoft Entra"/>
        <category label="OIDC" term="OIDC"/>
        <category label="Kubernetes" term="Kubernetes"/>
        <category label="Security" term="Security"/>
    </entry>
</feed>