<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Antonio Ereiz, Author at ShiftMag</title>
	<atom:link href="https://shiftmag.dev/author/antonio-ereiz/feed/" rel="self" type="application/rss+xml" />
	<link>https://shiftmag.dev/author/antonio-ereiz/</link>
	<description>Insightful engineering content &#38; community</description>
	<lastBuildDate>Thu, 11 Jun 2026 12:58:22 +0000</lastBuildDate>
	<language>en-GB</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.3</generator>

<image>
	<url>https://shiftmag.dev/wp-content/uploads/2024/08/cropped-ShiftMag-favicon-32x32.png</url>
	<title>Antonio Ereiz, Author at ShiftMag</title>
	<link>https://shiftmag.dev/author/antonio-ereiz/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Solving the Multi-Contributor Challenge with Kubernetes and Ephemeral Environments</title>
		<link>https://shiftmag.dev/kubernetes-multi-contributor-challenge-infobip-9591/</link>
		
		<dc:creator><![CDATA[Antonio Ereiz]]></dc:creator>
		<pubDate>Thu, 11 Jun 2026 12:56:12 +0000</pubDate>
				<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Engineer Explains]]></category>
		<category><![CDATA[kazhoon]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<guid isPermaLink="false">https://shiftmag.dev/?p=9591</guid>

					<description><![CDATA[<p>When dozens of developers share the same environment and Git repository, testing changes quickly becomes chaotic. Here's how Kubernetes solves that.</p>
<p>The post <a href="https://shiftmag.dev/kubernetes-multi-contributor-challenge-infobip-9591/">Solving the Multi-Contributor Challenge with Kubernetes and Ephemeral Environments</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></description>
										<content:encoded><![CDATA[<figure class="wp-block-post-featured-image"><img fetchpriority="high" decoding="async" width="1376" height="768" src="https://shiftmag.dev/wp-content/uploads/2026/05/kubernetes-bottleneck-raw.png?x32039" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="" style="object-fit:cover;" srcset="https://shiftmag.dev/wp-content/uploads/2026/05/kubernetes-bottleneck-raw.png 1376w, https://shiftmag.dev/wp-content/uploads/2026/05/kubernetes-bottleneck-raw-300x167.png 300w, https://shiftmag.dev/wp-content/uploads/2026/05/kubernetes-bottleneck-raw-1024x572.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/05/kubernetes-bottleneck-raw-768x429.png 768w" sizes="(max-width: 1376px) 100vw, 1376px" /></figure>


<p class="wp-block-paragraph">I spend most of my time maintaining our companies&nbsp;<strong>internal developer portal (IDP),&nbsp;</strong>known internally as&nbsp;<strong>Kazhoon</strong>&nbsp;(which is built on&nbsp;<a href="https://backstage.io/">Backstage</a>). Like any developer portal, it provides a lot of features built by a wide range of engineering teams.</p>



<p class="wp-block-paragraph">Beyond my core team of maintainers, there are about 50 other contributors across the company who regularly develop their own plugins and features. All of these engineers push code to a single, giant&nbsp;<a href="https://monorepo.tools/">monorepo</a>.</p>



<p class="wp-block-paragraph">As you can imagine, there are going to be a lot of challenges with this kind of setup. In this guide, I&#8217;m going to focus on solving one of them.</p>



<h2 id="ScalingMultiContributorDevelopmentwithKubernetes&amp;EphemeralEnvironments-TheMulti-ContributorChallenge" class="wp-block-heading"><span id="the-multi-contributor-challenge"><strong>The Multi-Contributor Challenge</strong></span></h2>



<p class="wp-block-paragraph">When contributors are working on some feature, before merging, they would like to test it out, usually in a staging/testing environment. A small team working on a project has no issues here. They will just internally coordinate who gets to deploy at what time. But when you have 5 teams simultaneously working on different features, you&nbsp;<strong>will</strong>&nbsp;get conflicts.</p>



<p class="wp-block-paragraph">Our solution initially was to tell people to merge their &#8220;feature&#8221; branch into a &#8220;dev&#8221; branch that in theory should contain the features of all the other contributors as well. We would then deploy just this &#8220;dev&#8221; branch. In practice, however, we were still getting conflicts, just this time in a different place.</p>



<p class="wp-block-paragraph">Contributors would have issues merging their changes into the dev branch due to several reasons. Sometimes the dev branch was outdated, missing the latest changes from the master branch. Sometimes the contributors branch was not up-to-date. </p>



<p class="wp-block-paragraph">Sometimes someone doesn&#8217;t merge cleanly, which causes frustration to all the other people wanting to merge as well. Every few weeks we had to create a new &#8220;dev&#8221; branch from scratch because it just became impossible to fix the problems with the old one.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="491" src="https://shiftmag.dev/wp-content/uploads/2026/05/Photo1-kubernetes-1024x491.png?x32039" alt="The issue of having multiple contributors pushing requests to a single dev branch." class="wp-image-10046" srcset="https://shiftmag.dev/wp-content/uploads/2026/05/Photo1-kubernetes-1024x491.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/05/Photo1-kubernetes-300x144.png 300w, https://shiftmag.dev/wp-content/uploads/2026/05/Photo1-kubernetes-768x368.png 768w, https://shiftmag.dev/wp-content/uploads/2026/05/Photo1-kubernetes.png 1618w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">With many push requests, the &#8220;dev&#8221; branch was in trouble. </figcaption></figure>



<p class="wp-block-paragraph">So how do we solve this problem? The answer came to me while experimenting with Kubernetes. I was in the process of migrating one of our projects from a traditional “Docker on a VM” setup into Kubernetes. The project in question, as you might guess, is Kazhoon, our internal developer portal (IDP).&nbsp;</p>



<p class="wp-block-paragraph">Before explaining the solution, it helps to first understand how we deploy applications to Kubernetes.</p>



<h2 id="ScalingMultiContributorDevelopmentwithKubernetes&amp;EphemeralEnvironments-HowdowedeploytoKubernetes" class="wp-block-heading"><span id="how-do-we-deploy-to-kubernetes"><strong>How do we deploy to Kubernetes</strong></span></h2>



<p class="wp-block-paragraph">Apart from having standard Kubernetes clusters, Infobip also leverages ArgoCD. A continuous delivery/deployment tool that sits between developers and Kubernetes. It uses GitOps principles to continuously sync infrastructure configuration stored in a Git repository with the actual cluster state. </p>



<p class="wp-block-paragraph">This way,&nbsp;developers don&#8217;t need to directly interact with a Kubernetes cluster but instead just tweak and update the files in git and ArgoCD will make sure the changes get reflected in the actual Kubernetes cluster.</p>



<p class="wp-block-paragraph">Also, it gives us a nice UI to monitor the changes. </p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="313" src="https://shiftmag.dev/wp-content/uploads/2026/06/image-2025-11-21_12-46-571-1024x313.png?x32039" alt="" class="wp-image-10289" srcset="https://shiftmag.dev/wp-content/uploads/2026/06/image-2025-11-21_12-46-571-1024x313.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/06/image-2025-11-21_12-46-571-300x92.png 300w, https://shiftmag.dev/wp-content/uploads/2026/06/image-2025-11-21_12-46-571-768x235.png 768w, https://shiftmag.dev/wp-content/uploads/2026/06/image-2025-11-21_12-46-571.png 1660w" sizes="(max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">ArgoCD UI</figcaption></figure>



<p class="wp-block-paragraph"><strong>So how do we deploy?</strong></p>



<p class="wp-block-paragraph">Well&#8230;, let&#8217;s say we want to deploy a &#8220;simple&#8221; application. In this case it is our IDP Kazhoon. Keep in mind that our app has do be dockerized for it to be deployed to Kubernetes.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><em>P.S. The files below are created in a separate git repository that is linked with our ArgoCD application. For more information check out&nbsp;<a href="https://argo-cd.readthedocs.io/en/stable/getting_started/" target="_blank" rel="noreferrer noopener nofollow">ArgoCD docs</a>. Parts of the files have also been cut out for clarity.</em></p>
</blockquote>



<p class="wp-block-paragraph">We will first create a file which describes how our application should be deployed. Important parts have comments.</p>



<p class="wp-block-paragraph"></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" style="font-size:16px"><summary><strong><em>Deployment &#8211; CLICK TO EXPAND</em></strong></summary>
<pre style="font-family:monospace;color: rgb(68, 68, 68); background-color: rgb(243, 243, 243); font-weight: 400; "><span style="color: rgb(68, 68, 68); font-weight: 400;">apiVersion:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">apps/v1</span>
<span style="color: rgb(68, 68, 68); font-weight: 400;">kind:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">Deployment</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># Type of resource in Kubernetes that manages our pods (aka. instances)</span>
<span style="color: rgb(68, 68, 68); font-weight: 400;">metadata:</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">backstage-deployment</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># Name of the resources</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">namespace:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">infobip-kazhoon</span>
<span style="color: rgb(68, 68, 68); font-weight: 400;">spec:</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">replicas:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">2</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># Number of pods to create/manage</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">selector:</span>
    <span style="color: rgb(68, 68, 68); font-weight: 400;">matchLabels:</span>
      <span style="color: rgb(68, 68, 68); font-weight: 400;">app:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">backstage-deployment</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">template:</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># this is where we describe each deployment of our application</span>
    <span style="color: rgb(68, 68, 68); font-weight: 400;">metadata:</span>
      <span style="color: rgb(68, 68, 68); font-weight: 400;">labels:</span>
        <span style="color: rgb(68, 68, 68); font-weight: 400;">app:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">backstage-deployment</span>
    <span style="color: rgb(68, 68, 68); font-weight: 400;">spec:</span>
      <span style="color: rgb(68, 68, 68); font-weight: 400;">imagePullSecrets:</span>
        <span style="color: rgb(57, 115, 0); font-weight: 400;">-</span> <span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">image-pull-secret</span>
      <span style="color: rgb(68, 68, 68); font-weight: 400;">containers:</span>
        <span style="color: rgb(57, 115, 0); font-weight: 400;">-</span> <span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">backstage-deployment</span>
          <span style="color: rgb(68, 68, 68); font-weight: 400;">image:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">docker.domain.com/infobip-backstage:1.175.9</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># dockerized image on internal artifact repository.</span>
          <span style="color: rgb(68, 68, 68); font-weight: 400;">imagePullPolicy:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">IfNotPresent</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># Yes, Kubernetes will cache your images, so if it already existed, it will reuse it based on the name/version.</span>
          <span style="color: rgb(68, 68, 68); font-weight: 400;">ports:</span>
            <span style="color: rgb(57, 115, 0); font-weight: 400;">-</span> <span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">http</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># new port called "http"</span>
              <span style="color: rgb(68, 68, 68); font-weight: 400;">containerPort:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">7007</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># port where our application is listening</span>
          <span style="color: rgb(68, 68, 68); font-weight: 400;">envFrom:</span>
            <span style="color: rgb(57, 115, 0); font-weight: 400;">-</span> <span style="color: rgb(68, 68, 68); font-weight: 400;">configMapRef:</span>
                <span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">backstage-config</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># Environment variables defined in a separate file</span>
            <span style="color: rgb(57, 115, 0); font-weight: 400;">-</span> <span style="color: rgb(68, 68, 68); font-weight: 400;">secretRef:</span>
                <span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">backstage-secrets</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f576.png" alt="🕶" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Secret Environment variable defined in a separate file</span>
          <span style="color: rgb(68, 68, 68); font-weight: 400;">resources:</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># We don't need to provision VMs for our application, we just tell Kubernetes how "heavy" our app is and Kubernetes will make sure to give us enough power.</span>
            <span style="color: rgb(68, 68, 68); font-weight: 400;">requests:</span>
              <span style="color: rgb(68, 68, 68); font-weight: 400;">cpu:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">"1"</span>
              <span style="color: rgb(68, 68, 68); font-weight: 400;">memory:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">"1Gi"</span>
            <span style="color: rgb(68, 68, 68); font-weight: 400;">limits:</span>
              <span style="color: rgb(68, 68, 68); font-weight: 400;">cpu:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">"1"</span>
              <span style="color: rgb(68, 68, 68); font-weight: 400;">memory:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">"1Gi"</span></pre>
</details>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">We also need some sort of internal load balancer for our pods to be able to connect to them. We need to create a Kubernetes<strong>&nbsp;Service.&nbsp;</strong></p>



<p class="wp-block-paragraph"></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" style="font-size:16px"><summary><strong><em>Service &#8211; CLICK TO EXPAND</em></strong></summary>
<pre style="font-family:monospace;color: rgb(68, 68, 68); background-color: rgb(243, 243, 243); font-weight: 400; "><span style="color: rgb(68, 68, 68); font-weight: 400;">apiVersion:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">v1</span>
<span style="color: rgb(68, 68, 68); font-weight: 400;">kind:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">Service</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># Type of Kubernetes resource that acts as an internal load balancer. It also allows other resources to communicate with out pods</span>
<span style="color: rgb(68, 68, 68); font-weight: 400;">metadata:</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">backstage-service</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">namespace:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">infobip-kazhoon</span>
<span style="color: rgb(68, 68, 68); font-weight: 400;">spec:</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">selector:</span>
    <span style="color: rgb(68, 68, 68); font-weight: 400;">app:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">backstage-deployment</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># We tell the service which pods it should find</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">ports:</span>
    <span style="color: rgb(57, 115, 0); font-weight: 400;">-</span> <span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">http</span> <span style="color: rgb(105, 112, 112); font-weight: 400;">#  we define a new port </span>
      <span style="color: rgb(68, 68, 68); font-weight: 400;">port:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">80</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># this port gets opened on the Service </span>
      <span style="color: rgb(68, 68, 68); font-weight: 400;">targetPort:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">http</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># resolves to the 7007 port of our pods (check the ports defined in the Deployment snippet)</span>
</pre>
</details>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">As a last step we also want to allow for people outside the Kubernetes cluster to connect to our application. We need an&nbsp;<strong>Ingress</strong></p>



<p class="wp-block-paragraph"></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" style="font-size:16px"><summary><strong><em>Ingress &#8211; CLICK TO EXPAND</em></strong></summary>
<pre style="font-family:monospace;color: rgb(68, 68, 68); background-color: rgb(243, 243, 243); font-weight: 400; "><span style="color: rgb(68, 68, 68); font-weight: 400;">apiVersion:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">networking.k8s.io/v1</span>
<span style="color: rgb(68, 68, 68); font-weight: 400;">kind:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">Ingress</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># Type of Kubernetes resource that allows us to expose our internal network to the outside. In Infobip we use Haproxy for this.</span>
<span style="color: rgb(68, 68, 68); font-weight: 400;">metadata:</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">backstage-ingress</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">namespace:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">infobip-kazhoon</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">annotations:</span>
    <span style="color: rgb(68, 68, 68); font-weight: 400;">iks-external-dns.k8s.infobip.com/hostname:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">infobip-kazhoon-iot1.domain.com</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># special Infobip annotations that create a DNS record for our network</span>
    <span style="color: rgb(68, 68, 68); font-weight: 400;">cert-manager.io/common-name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">infobip-kazhoon-iot1.domain.com</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># used for TLS</span>
    <span style="color: rgb(68, 68, 68); font-weight: 400;">cert-manager.io/cluster-issuer:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">vault-issuer</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># TLS</span>
<span style="color: rgb(68, 68, 68); font-weight: 400;">spec:</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">rules:</span>
    <span style="color: rgb(57, 115, 0); font-weight: 400;">-</span> <span style="color: rgb(68, 68, 68); font-weight: 400;">host:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">infobip-kazhoon-iot1.domain.com</span>
      <span style="color: rgb(68, 68, 68); font-weight: 400;">http:</span>
        <span style="color: rgb(68, 68, 68); font-weight: 400;">paths:</span>
          <span style="color: rgb(57, 115, 0); font-weight: 400;">-</span> <span style="color: rgb(68, 68, 68); font-weight: 400;">path:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">/</span>
            <span style="color: rgb(68, 68, 68); font-weight: 400;">pathType:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">Prefix</span>
            <span style="color: rgb(68, 68, 68); font-weight: 400;">backend:</span>
              <span style="color: rgb(68, 68, 68); font-weight: 400;">service:</span>
                <span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">backstage-service</span>
                <span style="color: rgb(68, 68, 68); font-weight: 400;">port:</span>
                  <span style="color: rgb(68, 68, 68); font-weight: 400;">number:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">80</span>
  <span style="color: rgb(68, 68, 68); font-weight: 400;">tls:</span>
    <span style="color: rgb(57, 115, 0); font-weight: 400;">-</span> <span style="color: rgb(68, 68, 68); font-weight: 400;">hosts:</span>
        <span style="color: rgb(57, 115, 0); font-weight: 400;">-</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">infobip-kazhoon-iot1.domain.com</span>
&nbsp; &nbsp; &nbsp; <span style="color: rgb(68, 68, 68); font-weight: 400;">secretName:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">minimal-ingress-cert</span> <span style="color: rgb(105, 112, 112); font-weight: 400;"># stores the certificate in this certificate resource</span></pre>
</details>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Once we create all the files, we push them to&nbsp;our git repository and hit &#8220;Sync&#8221; on ArgoCD. If we did everything okay we should see something like this:<br></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="133" src="https://shiftmag.dev/wp-content/uploads/2026/05/image-1-1024x133.png?x32039" alt="request → the ip of the haproxy service (which now has our config from the ingress) → ingress → service (load balancer) → pods" class="wp-image-10099" srcset="https://shiftmag.dev/wp-content/uploads/2026/05/image-1-1024x133.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/05/image-1-300x39.png 300w, https://shiftmag.dev/wp-content/uploads/2026/05/image-1-768x100.png 768w, https://shiftmag.dev/wp-content/uploads/2026/05/image-1.png 1734w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">From left to right:<br><strong>request → the ip of the haproxy service (which now has our config from the ingress) → ingress → service (load balancer) → pods</strong></figcaption></figure>



<p class="wp-block-paragraph"><em>Since this is a simplified &#8220;network&#8221; view we don&#8217;t see all resources (config files, certificates, etc&#8230;). The pods/instances on our right are the result of our &#8220;Deployment&#8221; resource which is not visible here.</em></p>



<p class="wp-block-paragraph">As you can see, with just a handful of files we managed to completely skip VM provisioning as well as avoid the need to click through yet another UI just to deploy.</p>



<h2 id="ScalingMultiContributorDevelopmentwithKubernetes&amp;EphemeralEnvironments-Revelation" class="wp-block-heading"><span id="and-then-it-clicked"><strong>And then, it clicked</strong></span></h2>



<p class="wp-block-paragraph">Kubernetes wasn’t just solving our infrastructure problems. Its design made it possible to solve the contributor problem I described at the beginning.</p>



<p class="wp-block-paragraph">Instead of coordinating deployments, managing shared environments, or deciding who gets to test changes and when, we could let the platform do the work for us.</p>



<p class="wp-block-paragraph">Everything could be automated.<br>Every change could have its own environment.<br>No coordination required.</p>



<p class="wp-block-paragraph"><em>The solution?</em></p>



<h2 id="ScalingMultiContributorDevelopmentwithKubernetes&amp;EphemeralEnvironments-EphemeralEnvironments" class="wp-block-heading"><span id="ephemeral-environments"><strong>Ephemeral Environments</strong></span></h2>



<p class="wp-block-paragraph">Whenever a contributor opens a new pull request, we can create an ephemeral (temporary) environment specifically for that branch, using the steps described in the example above.</p>



<p class="wp-block-paragraph">We deploy the contributor’s branch to this environment and provide a unique link where they can test their changes.</p>



<p class="wp-block-paragraph">This approach means contributors don’t need to think about deployment at all &#8211; everything happens automatically.</p>



<p class="wp-block-paragraph">Each temporary environment is fully isolated from the others. The only shared component is the database, which could be further improved by switching to an in-memory solution.</p>



<p class="wp-block-paragraph">Each environment has its own ingress, ensuring that every contributor receives a dedicated URL for their branch.</p>



<p class="wp-block-paragraph">Because of this, we can eliminate the need for a separate&nbsp;<code>dev</code>&nbsp;branch and deploy feature branches directly.</p>



<h2 id="ScalingMultiContributorDevelopmentwithKubernetes&amp;EphemeralEnvironments-Howitworks?" class="wp-block-heading"><span id="how-it-works"><strong>How it works?</strong></span></h2>



<h2 id="ScalingMultiContributorDevelopmentwithKubernetes&amp;EphemeralEnvironments-PullRequestHooks" class="wp-block-heading"><span id="step-1-pull-request-hooks">Step 1: Pull Request Hooks</span></h2>



<p class="wp-block-paragraph">Initially a contributor will just write some code, test it locally, and when happy, open up a pull request. This is where most developers would usually deploy to some IO environment that is close to a production setup for additional testing.</p>



<p class="wp-block-paragraph">Instead of having to worry about the deployment, a Bitbucket&nbsp;<a href="https://confluence.atlassian.com/bitbucketserver/manage-webhooks-938025878.html" target="_blank" rel="noreferrer noopener nofollow">webhook</a>&nbsp;which listens to the&nbsp;<strong>PR_OPENED&nbsp;</strong>event will trigger a Jenkins job. The same Jenkins job that is used to build the main Kazhoon application. This is done to prevent duplication of jenkins groovy code as well as archetypes, which I will explain later. Simple hook below.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="814" height="498" src="https://shiftmag.dev/wp-content/uploads/2026/05/photo-4-kubernetes.png?x32039" alt="" class="wp-image-10050" srcset="https://shiftmag.dev/wp-content/uploads/2026/05/photo-4-kubernetes.png 814w, https://shiftmag.dev/wp-content/uploads/2026/05/photo-4-kubernetes-300x184.png 300w, https://shiftmag.dev/wp-content/uploads/2026/05/photo-4-kubernetes-768x470.png 768w" sizes="auto, (max-width: 814px) 100vw, 814px" /></figure>



<p class="wp-block-paragraph"><em>Notice the additional hook at the top. This is the main hook used for the app. It triggers the same Jenkins jobs but under different conditions (PUSH_EVENT &#8211; when code is pushed).</em></p>



<p class="wp-block-paragraph"><em>All of this can also be achieved on&nbsp;Github as well, using workflows.</em></p>



<h2 id="ScalingMultiContributorDevelopmentwithKubernetes&amp;EphemeralEnvironments-ThemainJenkinsJob" class="wp-block-heading"><span id="step-2-the-main-jenkins-job">Step 2: The main Jenkins Job</span></h2>



<p class="wp-block-paragraph">Apart from the main logic for building Kazhoon and pushing a docker image (standard for most Infobip apps), we additionally need the setup for Kubernetes. Keep in mind that the Jenkins job runs every time new code gets pushed. We need a way to detect which code change is linked to a PR. In other words:&nbsp;<strong>Does the branch that is currently being built have an open PR linked to it?</strong></p>



<p class="wp-block-paragraph">Initially we figured we could just use the build parameter PR_ID as seen&nbsp;on the image above. This was working okay for testing, but then a problem appeared. How do we detect code changes?&nbsp;After all, we would like the most recent code to be deployed and not just the one that existed when we opened up the PR.</p>



<p class="wp-block-paragraph">Sadly, the PR_ID parameter does not exist on regular code changes. We could create another hook using the PR_SOURCE_RESCOPED event, but that would mean that we will have duplicate Jenkins jobs. One for the newly added hook and one for the original &#8220;main&#8221; hook that always gets triggered when we push code. We need a different solution.</p>



<p class="wp-block-paragraph">Luckily, it is possible to call APIs through Jenkins, so what we can do is call the&nbsp;<a href="https://developer.atlassian.com/server/bitbucket/rest/v1000/intro/#about" target="_blank" rel="noreferrer noopener nofollow">Bitbucket API</a>, check if the branch currently being built has an open pull request, and if so, initiate the Kubernetes setup. This logic can be done on the initial pull request open event as well as subsequent code change triggers. Here is how that would look like. Example written in a Jenkinsfile using scripted pipeline syntax.</p>



<p class="wp-block-paragraph"></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" style="font-size:16px"><summary><strong><em>Bitbucket API example &#8211; CLICK TO EXPAND</em></strong></summary>
<pre style="font-family:monospace;color: rgb(68, 68, 68); background-color: rgb(243, 243, 243); font-weight: 400; ">def <span style="color: rgb(136, 0, 0); font-weight: 700;">findPrForSourceBranch</span>(sourceBranch) {
    <span style="color: rgb(136, 0, 0); font-weight: 700;">withCredentials</span>([<span style="color: rgb(68, 68, 68); font-weight: 700;">string</span>(<span style="color: rgb(68, 68, 68); font-weight: 400;">credentialsId</span>: globconst.GIT_HTTPS_TAG_TOKEN, <span style="color: rgb(68, 68, 68); font-weight: 400;">variable</span>: <span style="color: rgb(136, 0, 0); font-weight: 400;">'GIT_TOKEN'</span>)]) {
        def response = <span style="color: rgb(136, 0, 0); font-weight: 700;">httpRequest</span>(
            <span style="color: rgb(68, 68, 68); font-weight: 400;">url</span>: <span style="color: rgb(136, 0, 0); font-weight: 400;">"${globconst.BITBUCKET_API_URL}projects/IAF/repos/infobip-backstage/pull-requests?state=OPEN&amp;at=refs/heads/${sourceBranch}&amp;direction=OUTGOING"</span>,
            <span style="color: rgb(68, 68, 68); font-weight: 400;">httpMode</span>: <span style="color: rgb(136, 0, 0); font-weight: 400;">'GET'</span>,
            <span style="color: rgb(68, 68, 68); font-weight: 400;">contentType</span>: <span style="color: rgb(136, 0, 0); font-weight: 400;">'APPLICATION_JSON'</span>,
            <span style="color: rgb(68, 68, 68); font-weight: 400;">customHeaders</span>: [[<span style="color: rgb(68, 68, 68); font-weight: 400;">name</span>: <span style="color: rgb(136, 0, 0); font-weight: 400;">'Authorization'</span>, <span style="color: rgb(68, 68, 68); font-weight: 400;">value</span>: <span style="color: rgb(136, 0, 0); font-weight: 400;">"Bearer ${GIT_TOKEN}"</span>]],
            <span style="color: rgb(68, 68, 68); font-weight: 400;">validResponseCodes</span>: <span style="color: rgb(136, 0, 0); font-weight: 400;">'200'</span>
        )
        def prList = <span style="color: rgb(68, 68, 68); font-weight: 700;">new</span> groovy.json.<span style="color: rgb(136, 0, 0); font-weight: 700;">JsonSlurper</span>().<span style="color: rgb(136, 0, 0); font-weight: 700;">parseText</span>(response.content)
        <span style="color: rgb(68, 68, 68); font-weight: 700;">if</span> (prList.size &gt; <span style="color: rgb(136, 0, 0); font-weight: 400;">0</span>) {
            <span style="color: rgb(68, 68, 68); font-weight: 700;">return</span> prList.values[<span style="color: rgb(136, 0, 0); font-weight: 400;">0</span>].id
        }
        <span style="color: rgb(68, 68, 68); font-weight: 700;">return</span> <span style="color: rgb(102, 153, 85); font-weight: 400;">null</span>
    }
}
<span style="color: rgb(136, 0, 0); font-weight: 700;">node</span>(<span style="color: rgb(136, 0, 0); font-weight: 400;">'infobip-docker-nodejs:v20-dev'</span>) {
    <span style="color: rgb(136, 0, 0); font-weight: 700;">LoadJenkinsPipelineLibrary</span>(this)
    <span style="color: rgb(68, 68, 68); font-weight: 700;">if</span> (!env.PR_ID &amp;&amp; env.BRANCH_NAME &amp;&amp; env.BRANCH_NAME != <span style="color: rgb(136, 0, 0); font-weight: 400;">'master'</span>) {
        def prId = <span style="color: rgb(136, 0, 0); font-weight: 700;">findPrForSourceBranch</span>(env.BRANCH_NAME)
        <span style="color: rgb(68, 68, 68); font-weight: 700;">if</span> (prId) {
            env.PR_ID = prId
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <span style="color: rgb(68, 68, 68); font-weight: 700;">echo</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">"Found PR_ID=${prId} for branch ${env.BRANCH_NAME}"</span>
        } <span style="color: rgb(68, 68, 68); font-weight: 700;">else</span> {
            <span style="color: rgb(68, 68, 68); font-weight: 700;">echo</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">"No PR found for branch ${env.BRANCH_NAME}"</span>
        }
    }</pre>
</details>



<p class="wp-block-paragraph">In the later stages we can just check for the env.PR_ID. If it exists then there is a PR linked to the branch. Here is how that would look like:</p>



<h3 class="wp-block-heading"><span id="stages">Stages:</span></h3>



<ol class="wp-block-list">
<li>Clone Kubernetes config repository which is linked to the ArgoCD application</li>



<li>Generate Kubernetes files from template &#8211; make sure we sanitize the values</li>



<li>Push to git</li>



<li>Trigger sync on ArgoCD and&nbsp;leave comment in PR with link to new deployment.</li>
</ol>



<p class="wp-block-paragraph"></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow" style="font-size:16px"><summary><strong><em>Jenkinsfile Kubernetes Logic &#8211; CLICK TO EXPAND</em></strong></summary>
<pre style="font-family:monospace;color: rgb(68, 68, 68); background-color: rgb(243, 243, 243); font-weight: 400; "><span style="color: rgb(68, 68, 68); font-weight: 700;">def</span> K8S_CONFIG_REPO = <span style="color: rgb(136, 0, 0); font-weight: 400;">'ssh://git-instance.com:7999/project-name/infobip-kazhoon.git'</span>
<span style="color: rgb(68, 68, 68); font-weight: 700;">def</span> K8S_CONFIG_BRANCH = <span style="color: rgb(136, 0, 0); font-weight: 400;">'master'</span>
<span style="color: rgb(68, 68, 68); font-weight: 700;">def</span> K8S_CONFIG_TARGET_DIR = <span style="color: rgb(136, 0, 0); font-weight: 400;">'k8s-config-repo'</span>
<span style="color: rgb(68, 68, 68); font-weight: 700;">def</span> featureName = env.BRANCH_NAME.toLowerCase().replaceAll(<span style="color: rgb(171, 86, 86); font-weight: 400;">/[^a-z0-9_-]/</span>, <span style="color: rgb(136, 0, 0); font-weight: 400;">"-"</span>) ?: <span style="color: rgb(136, 0, 0); font-weight: 400;">'feature-preview'</span>
<span style="color: rgb(68, 68, 68); font-weight: 700;">def</span> k8sSafeName = env.BRANCH_NAME.toLowerCase().replaceAll(<span style="color: rgb(171, 86, 86); font-weight: 400;">/[^a-z0-9-]/</span>, <span style="color: rgb(136, 0, 0); font-weight: 400;">"-"</span>).take(<span style="color: rgb(136, 0, 0); font-weight: 400;">40</span>).replaceAll(<span style="color: rgb(171, 86, 86); font-weight: 400;">/-+$/</span>, <span style="color: rgb(136, 0, 0); font-weight: 400;">""</span>) <span style="color: rgb(105, 112, 112); font-weight: 400;">// Kubernetes name limit</span>
<span style="color: rgb(68, 68, 68); font-weight: 700;">def</span> dnsSafeName = k8sSafeName.take(<span style="color: rgb(136, 0, 0); font-weight: 400;">24</span>).replaceAll(<span style="color: rgb(171, 86, 86); font-weight: 400;">/-+$/</span>, <span style="color: rgb(136, 0, 0); font-weight: 400;">""</span>) <span style="color: rgb(105, 112, 112); font-weight: 400;">// DNS name limit eg. infobip-kazhoon-iot1-feature-verylongbr.domain.com</span>

stage(<span style="color: rgb(136, 0, 0); font-weight: 400;">'Clone Kubernetes Config Repo'</span>) {
	<span style="color: rgb(105, 112, 112); font-weight: 400;">// A neat trick to skip jenkins stages. Left out in later stages for brevity</span>
    <span style="color: rgb(68, 68, 68); font-weight: 700;">if</span> (!env.PR_ID) { 
        Utils.markStageSkippedForConditional(STAGE_NAME)
        <span style="color: rgb(68, 68, 68); font-weight: 700;">return</span>
    }
	<span style="color: rgb(105, 112, 112); font-weight: 400;">// Clone the kubernetes repository where we put our initial files for deployment.</span>
    sshagent(<span style="color: rgb(68, 68, 68); font-weight: 400;">credentials:</span> [GIT_CREDENTIALS]) {
        globlib.git.checkout(<span style="color: rgb(68, 68, 68); font-weight: 400;">gitUrl:</span> K8S_CONFIG_REPO, <span style="color: rgb(68, 68, 68); font-weight: 400;">gitBranch:</span> K8S_CONFIG_BRANCH, <span style="color: rgb(68, 68, 68); font-weight: 400;">targetDir:</span> K8S_CONFIG_TARGET_DIR)
    }
}

stage(<span style="color: rgb(136, 0, 0); font-weight: 400;">'Generate Kubernetes Files from Template'</span>) {
    <span style="color: rgb(68, 68, 68); font-weight: 700;">def</span> featurePath = <span style="color: rgb(136, 0, 0); font-weight: 400;">"${K8S_CONFIG_TARGET_DIR}/feature/${featureName}"</span>
    <span style="color: rgb(105, 112, 112); font-weight: 400;">// Copy and customize template. The template looks similar to the 3 files we created for the deployment just with some parts containing placeholders instead of fixed values.</span>
    sh <span style="color: rgb(136, 0, 0); font-weight: 400;">"""
        mkdir -p ${featurePath}
        cp -r ${K8S_CONFIG_TARGET_DIR}/feature/template/* ${featurePath}/

        # Replace placeholders in template
        for f in ${featurePath}/deployment.yaml ${featurePath}/service.yaml ${featurePath}/ingress.yaml; do
            sed -i -e "s|{{IMAGE_TAG}}|${dockerLabel}|" \
                -e "s|{{K8S_SAFE_IMAGE_TAG}}|${k8sSafeName}|" \
                -e "s|{{DNS_NAME}}|${dnsSafeName}|" \"\$f"
        done
    """</span>
}

stage(<span style="color: rgb(136, 0, 0); font-weight: 400;">'Push Kubernetes Resources to Git'</span>) { 
    dir(<span style="color: rgb(136, 0, 0); font-weight: 400;">"${K8S_CONFIG_TARGET_DIR}"</span>) {
        sshagent(<span style="color: rgb(68, 68, 68); font-weight: 400;">credentials:</span> [GIT_CREDENTIALS]) {
            sh <span style="color: rgb(136, 0, 0); font-weight: 400;">"""
                git config user.email "infobip-ci@infobip.com"
                git config user.name "Infobip CI (Jenkins)"
                git add .
				// We don't want the script to fail if there are no changes
                if ! git diff --cached --quiet; then
                    git commit -m 'added feature ${featureName}'
                    git push -u origin ${K8S_CONFIG_BRANCH}
                else
                    echo 'No changes to commit'
                fi
            """</span>
        }
    }
}

stage(<span style="color: rgb(136, 0, 0); font-weight: 400;">'Trigger Sync on ArgoCD'</span>) {
    withCredentials([string(<span style="color: rgb(68, 68, 68); font-weight: 400;">credentialsId:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">'kazhoon_argocd_bearer_token'</span>, <span style="color: rgb(68, 68, 68); font-weight: 400;">variable:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">'BEARER_TOKEN'</span>)]) {
        <span style="color: rgb(68, 68, 68); font-weight: 700;">def</span> response = httpRequest(
            <span style="color: rgb(171, 86, 86); font-weight: 400;">url:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">"https://argo-instance.com/api/v1/applications/infobip-kazhoon-iot1/sync"</span>,
            <span style="color: rgb(171, 86, 86); font-weight: 400;">httpMode:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">'POST'</span>,
            <span style="color: rgb(171, 86, 86); font-weight: 400;">contentType:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">'APPLICATION_JSON'</span>,
            <span style="color: rgb(171, 86, 86); font-weight: 400;">customHeaders:</span> [[<span style="color: rgb(68, 68, 68); font-weight: 400;">name:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">'Authorization'</span>, <span style="color: rgb(68, 68, 68); font-weight: 400;">value:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">"Bearer ${BEARER_TOKEN}"</span>]],
            <span style="color: rgb(171, 86, 86); font-weight: 400;">validResponseCodes:</span> <span style="color: rgb(136, 0, 0); font-weight: 400;">'200'</span>
        )
    }
	<span style="color: rgb(105, 112, 112); font-weight: 400;">// Utility function for posting comments to Bitbucket PR.</span>
    postPrComment(env.PR_ID, <span style="color: rgb(136, 0, 0); font-weight: 400;">"finish"</span>, <span style="color: rgb(136, 0, 0); font-weight: 400;">"""<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Your **feature environment** is live!
    ![party](https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExcjFyczl4anFvZXYzNGY1aGM2amIyZTk5Y3B6ZWJ4dHM1Y2ExamptYSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/a0h7sAqON67nO/giphy.gif)
    <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f310.png" alt="🌐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Access it here: [Preview URL](https://infobip-kazhoon-iot1-${dnsSafeName}.domain.com)
    <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2139.png" alt="ℹ" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Note that it might take a few minutes for the DNS to propagate and the service to become fully operational.
    <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2757.png" alt="❗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> If you encounter any issues, please contact [#kazhoon_contributors](https://infobip-support.slack.com/archives/C098ETXLL2U) on slack."""</span>)
}</pre>
</details>



<p class="wp-block-paragraph"></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="576" src="https://shiftmag.dev/wp-content/uploads/2026/05/New-Project5-1024x576.png?x32039" alt="The flow for pushing requests through kubernetes " class="wp-image-10144" srcset="https://shiftmag.dev/wp-content/uploads/2026/05/New-Project5-1024x576.png 1024w, https://shiftmag.dev/wp-content/uploads/2026/05/New-Project5-300x169.png 300w, https://shiftmag.dev/wp-content/uploads/2026/05/New-Project5-768x432.png 768w, https://shiftmag.dev/wp-content/uploads/2026/05/New-Project5.png 1280w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Flow of creating new features</figcaption></figure>



<h2 id="ScalingMultiContributorDevelopmentwithKubernetes&amp;EphemeralEnvironments-Cleanup" class="wp-block-heading"><span id="step-3-cleanup">Step 3: Cleanup</span></h2>



<p class="wp-block-paragraph">After the PR is merged, the letfover deployment is automatically cleaned up using a separate webhook and Jenkins pipeline which I won&#8217;t go into detail for this blog post.</p>



<p class="wp-block-paragraph">In short, when a PR is merged or deleted, a&nbsp;<code>Jenkinsfile.cleanup</code>&nbsp; pipeline is executed to remove all previously created resources and trigger a sync in Argo CD.</p>



<p class="wp-block-paragraph">The same&nbsp;<code>Jenkinsfile.cleanup</code>&nbsp;pipeline also runs periodically to remove stale resources from old PRs, helping avoid unnecessary infrastructure and electricity usage.</p>



<h2 id="ScalingMultiContributorDevelopmentwithKubernetes&amp;EphemeralEnvironments-Conclusion" class="wp-block-heading"><span id="conclusion"><strong>Conclusion</strong></span></h2>



<p class="wp-block-paragraph">Kubernetes and Argo CD allowed us to significantly improve the developer experience for everyone contributing to Kazhoon. By adopting a GitOps-driven approach, we eliminated the need to interact with a UI and, with it, the mental overhead of figuring out how and where to test changes. The entire deployment process is now fully abstracted away from contributors.</p>



<p class="wp-block-paragraph">The original &#8220;dev&#8221; branch which was a major source of confusion and friction no longer exists. Developers work exclusively on their feature branches, each with its own isolated, production-like environment.</p>



<p class="wp-block-paragraph">This setup also removed a large portion of our support overhead. Everything is automatic, predictable, and clearly communicated directly through pull request comments, allowing contributors to focus on what actually matters: building features.</p>



<h2 id="ScalingMultiContributorDevelopmentwithKubernetes&amp;EphemeralEnvironments-Resources&amp;Shout-outs" class="wp-block-heading"><span id="resources-shout-outs"><strong>Resources &amp; Shout-outs</strong></span></h2>



<p class="wp-block-paragraph">Special shout-out to Matej Kern and his team for helping me throughout the entire process. From explaining Kubernetes concepts to setting up the infrastructure behind everything described in this post. This project would not have been possible without their support and guidance.</p>



<p class="wp-block-paragraph">A huge shout-out as well to the maintainers and contributors behind these amazing projects and resources that made this workflow possible:</p>



<ul class="wp-block-list">
<li><a href="https://kubernetes.io/">Kubernetes</a></li>



<li><a href="https://argo-cd.readthedocs.io">Argo CD</a></li>



<li><a href="https://backstage.io/">Backstage, where I got the&nbsp;inspiration from</a></li>



<li><a href="https://www.cncf.io/">The cloud-native computing foundation</a></li>
</ul>



<p class="wp-block-paragraph"><em>Hope you enjoyed this blog post. It&#8217;s my first official one (and definitely not the last). There’s a lot I didn’t manage to include, because it would’ve ended up being a whole novel, so if you have any questions or thoughts, I&#8217;m always happy to chat. Feel free to <a href="https://x.com/a_erajc" target="_blank" rel="noreferrer noopener nofollow">message me on X.</a></em></p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://shiftmag.dev/kubernetes-multi-contributor-challenge-infobip-9591/">Solving the Multi-Contributor Challenge with Kubernetes and Ephemeral Environments</a> appeared first on <a href="https://shiftmag.dev">ShiftMag</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: shiftmag.dev @ 2026-08-11 11:28:53 by W3 Total Cache
-->