<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Operators on Teknologi</title>
    <link>https://teknologi.nl/tags/operators/</link>
    <description>Recent content test in Operators on Teknologi</description>
    <language>en</language>
    <managingEditor>ashwin.sarimin@teknologi.nl (Ashwin Sarimin)</managingEditor>
    <webMaster>ashwin.sarimin@teknologi.nl (Ashwin Sarimin)</webMaster>
    <copyright>© 2026 Ashwin Sarimin</copyright>
    <lastBuildDate>Thu, 05 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://teknologi.nl/tags/operators/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Service Router Operator for multi-region DNS management on AKS</title>
      <link>https://teknologi.nl/posts/servicerouteroperator/</link>
      <pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate>
      <author>Ashwin Sarimin</author>
      <guid>https://teknologi.nl/posts/servicerouteroperator/</guid>
      <description>How I built a Kubernetes operator to automate multi-region DNS management on AKS with Istio and ExternalDNS</description>
      <content:encoded><![CDATA[
<h1 class="relative group">Introduction
    <div id="introduction" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#introduction" aria-label="Anchor">#</a>
    </span>
    
</h1>
<p>In this post, I walk through the Service Router Operator, a Kubernetes operator I built to automate multi-region DNS provisioning on AKS using Istio and ExternalDNS.</p>
<p>The code repository for the operator can be found here: <a href="https://github.com/AshwinSarimin/service-router-operator"  target="_blank" rel="noreferrer">service-router-operator</a></p>

<h1 class="relative group">Multi region DNS on AKS
    <div id="multi-region-dns-on-aks" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#multi-region-dns-on-aks" aria-label="Anchor">#</a>
    </span>
    
</h1>
<p>The AKS platform can operate across multiple Azure regions (in this case West Europe and North Europe) to provide high availability, disaster recovery capabilities and geographic proximity to users.
In this multi-region architecture, a critical requirement is the ability to route traffic seamlessy between regions. When an application or service becomes unavailable in one region, whether due to maintenance, failure, or regional issues, traffic must be automatically or manually redirected to a healthy instance in another region.</p>
<p>Managing DNS across multiple regions presents significant operational complexity: application teams must route traffic to the correct regional cluster, maintain DNS records across multiple Azure Private DNS zones, and handle both regional isolation and cross-region failover scenarios. <strong>This way DNS management becomes complex and error-prone</strong>.</p>
<p><figure><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="low"
    alt=""
    src="./images/multi-region.drawio.png"
    ></figure>
</p>

<h1 class="relative group">Service Router Operator
    <div id="service-router-operator" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#service-router-operator" aria-label="Anchor">#</a>
    </span>
    
</h1>
<p>To address these challenges, the Service Router Operator provides automated DNS management for multi-region deployments:</p>
<ul>
<li><strong>Automates DNS Record Creation</strong>: Automatically generates DNS records based on Custom Resources.</li>
<li><strong>Enables Regional Control</strong>: Supports both Active (multi-region) and RegionBound (single-region) operational modes.</li>
<li><strong>Prevents Conflicts</strong>: Uses label-based filtering to ensure each region&rsquo;s ExternalDNS instance only manages its designated DNS records.</li>
<li><strong>Simplifies Operations</strong>: application teams simply declare their services and desired routing behavior using Helm charts.</li>
</ul>
<p>Our AKS platform spans two Azure regions (North Europe and West Europe) each with its own private DNS zone. When an workload team wants to expose a service, they need DNS records in both zones. The record in the West Europe zone should point to the West Europe cluster&rsquo;s Istio ingress gateway, and the one in the North Europe zone should point to the North Europe gateway. Straightforward enough, until you account for:</p>
<ul>
<li><strong>Active-Active services</strong>: the service runs in both regions, each cluster manages its own DNS</li>
<li><strong>RegionBound services</strong>: the service only runs in one region, but DNS records still need to exist in all zones pointing to the active cluster</li>
<li><strong>Failover scenarios</strong>: if a cluster goes down, another cluster needs to take over DNS management for the failed region.</li>
</ul>

<h2 class="relative group">Why an Operator?
    <div id="why-an-operator" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#why-an-operator" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>If you&rsquo;ve read my previous post on <a href="https://teknologi.nl/posts/kubebuilder/"  target="_blank" rel="noreferrer">KubeBuilder</a>, you&rsquo;ll know I&rsquo;m a fan of the operator pattern for exactly this kind of problem. An operator runs a continuous reconciliation loop, watching the desired state defined in custom resources and continuously working to make reality match it. For DNS management, this is ideal:</p>
<ul>
<li>If a DNS record is manually deleted, the operator recreates it within seconds.</li>
<li>If a Gateway&rsquo;s LoadBalancer IP changes, the operator updates all CNAME records automatically.</li>
<li>application teams define simple, declarative resources without needing to understand the DNS internals.</li>
<li>Platform teams retain control over cluster-wide infrastructure through their own set of resources.</li>
</ul>

<h2 class="relative group">How It Works
    <div id="how-it-works" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#how-it-works" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The Service Router automates DNS provisioning and traffic routing for services deployed across multiple AKS clusters and regions.</p>

<h3 class="relative group">Components
    <div id="components" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#components" aria-label="Anchor">#</a>
    </span>
    
</h3>

<h4 class="relative group">Service Router Operator
    <div id="service-router-operator-1" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#service-router-operator-1" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>The operator manages the lifecycle of DNS records by continuously reconciling Custom Resource Definitions (CRDs) to ensure the desired state matches the actual state.</p>

<h4 class="relative group">ExternalDNS
    <div id="externaldns" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#externaldns" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>ExternalDNS is an AKS platform component that automatically synchronizes Kubernetes networking resources (Services, Ingresses, and DNSEndpoint CRs) with DNS providers like Azure Private DNS zones. It monitors DNSEndpoint custom resources created by the Service Router Operator and provisions the corresponding DNS records.</p>
<p>Each AKS cluster runs multiple ExternalDNS instances, one for each regional DNS zone. This enables seamless failover when a cluster becomes unavailable, as the healthy cluster&rsquo;s ExternalDNS instance can take over DNS management for the failed region.</p>

<h4 class="relative group">Label-Based Filtering
    <div id="label-based-filtering" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#label-based-filtering" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>The Service Router Operator uses labels to ensure region-specific ExternalDNS instances only manage their designated DNS records. When a DNSEndpoint is created with label app: external-dns-North Europe, only the North Europe ExternalDNS instance processes it and creates records in the North Europe Private DNS zone. This ensures:</p>
<ul>
<li>Prevention of DNS conflicts between regions</li>
<li>Independent region management</li>
<li>Clear ownership and responsibility</li>
<li>Support for both Active and RegionBound operational modes</li>
</ul>
<blockquote><p>Important: The Service Router Operator does not directly create DNS records. It creates DNSEndpoint Custom Resources that ExternalDNS watches and uses to provision DNS records in Azure Private DNS.</p></blockquote><p>By combining the Service Router Operator with ExternalDNS and Azure Private DNS, the platform achieves fully automated, conflict-free DNS management that enables seamless traffic routing between regions.</p>

<h4 class="relative group">Custom Resources
    <div id="custom-resources" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#custom-resources" aria-label="Anchor">#</a>
    </span>
    
</h4>
<p>The Service Router Operator defines <strong>five Custom Resource Definitions</strong> across two API groups. Platform teams manage the cluster-wide infrastructure resources, and application teams manage their own namespace-scoped resources.</p>
<p><figure><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="low"
    alt=""
    src="./images/service-router-components.png"
    ></figure>
</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">cluster.router.io/v1alpha1   → ClusterIdentity, DNSConfiguration
</span></span><span class="line"><span class="cl">routing.router.io/v1alpha1   → Gateway, DNSPolicy, ServiceRoute</span></span></code></pre></div></div>
<p><strong>ClusterIdentity</strong> (cluster-scoped, platform team): Defines the cluster&rsquo;s metadata region, cluster name, base domain, and environment letter. This is used to construct DNS names and to determine which ExternalDNS controllers are relevant for this cluster.</p>
<p><strong>DNSConfiguration</strong> (cluster-scoped, platform team): Lists all ExternalDNS controller instances available across the platform, mapping each controller name to its region. This is the single source of truth for which ExternalDNS controllers exist.</p>
<p><strong>Gateway</strong> (namespace-scoped, platform team): Wraps an Istio ingress gateway with DNS target information. The operator creates the Istio <code>Gateway</code> resource and keeps its <code>hosts</code> list synchronized with all <code>ServiceRoutes</code> referencing it.</p>
<p><strong>DNSPolicy</strong> (namespace-scoped, workload team): Defines how DNS records should be propagated for services in that namespace — either in Active mode (this cluster manages its own region only) or RegionBound mode (one cluster manages DNS for multiple regions).</p>
<p><strong>ServiceRoute</strong> (namespace-scoped, workload team): Links a Kubernetes service to a Gateway and triggers DNS record creation. When you create a <code>ServiceRoute</code>, the operator constructs the DNS name and creates the appropriate <code>DNSEndpoint</code> resources for ExternalDNS to pick up.</p>

<h2 class="relative group">DNS Provisioning Flow
    <div id="dns-provisioning-flow" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#dns-provisioning-flow" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The operator does <strong>not</strong> create DNS records directly. Instead, it creates <code>DNSEndpoint</code> custom resources (from the ExternalDNS CRD API), which ExternalDNS watches and uses to provision records in Azure Private DNS.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Workload team creates ServiceRoute
</span></span><span class="line"><span class="cl">         │
</span></span><span class="line"><span class="cl">         ▼
</span></span><span class="line"><span class="cl">Service Router Operator reconciles
</span></span><span class="line"><span class="cl">  ├── Reads ClusterIdentity (region, domain, cluster)
</span></span><span class="line"><span class="cl">  ├── Reads DNSPolicy (mode, active controllers)
</span></span><span class="line"><span class="cl">  ├── Reads Gateway (target postfix, Istio controller)
</span></span><span class="line"><span class="cl">  └── Creates DNSEndpoint CRDs (one per active ExternalDNS controller)
</span></span><span class="line"><span class="cl">         │
</span></span><span class="line"><span class="cl">         ▼
</span></span><span class="line"><span class="cl">ExternalDNS controller watches DNSEndpoints (filtered by label)
</span></span><span class="line"><span class="cl">         │
</span></span><span class="line"><span class="cl">         ▼
</span></span><span class="line"><span class="cl">ExternalDNS provisions CNAME record in Azure Private DNS:
</span></span><span class="line"><span class="cl">  api-ns-p-prod-myapp.example.com → aks-West Europe-internal.example.com
</span></span><span class="line"><span class="cl">         │
</span></span><span class="line"><span class="cl">         ▼
</span></span><span class="line"><span class="cl">A separate IngressDNS controller (part of the operator) watches
</span></span><span class="line"><span class="cl">the Gateway&#39;s LoadBalancer Service and creates an A record:
</span></span><span class="line"><span class="cl">  aks-West Europe-internal.example.com → 10.123.45.67</span></span></code></pre></div></div>
<p>The two-step CNAME + A record design means that if the gateway IP ever changes (for example, after a cluster recreation), only the A record needs to update — all service CNAME records automatically follow without any changes.</p>
<p>DNS names are constructed deterministically from <code>ServiceRoute</code> fields:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{serviceName}-ns-{envLetter}-{environment}-{application}.{domain}</span></span></code></pre></div></div>
<p>For example, a <code>ServiceRoute</code> with <code>serviceName: api</code>, <code>environment: prod</code>, <code>application: myapp</code> on a cluster with <code>environmentLetter: p</code> and <code>domain: example.com</code> produces <code>api-ns-p-prod-myapp.example.com</code>.</p>

<h2 class="relative group">Operational Modes
    <div id="operational-modes" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#operational-modes" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The <code>DNSPolicy</code> controls how DNS records are spread across regions.</p>
<p><strong>Active Mode</strong> — use this when your service runs in multiple regions. Each cluster manages DNS only for its own region. A client in West Europe queries the West Europe DNS zone and routes to the West Europe cluster; a client in North Europe does the same for its cluster.</p>
<p><figure><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="low"
    alt=""
    src="./images/Mode-Active.png"
    ></figure>
</p>
<p><strong>RegionBound Mode</strong> — use this when your service only runs in one region but you still want DNS records in all regional zones. You specify a <code>sourceRegion</code>, and only the cluster in that region becomes active. The active cluster creates <code>DNSEndpoint</code> resources for all ExternalDNS controllers (West Europe and North Europe), so both zones get records pointing to the single active cluster. Clusters in other regions are automatically inactive and do not create any DNS records.</p>
<p><figure><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="low"
    alt=""
    src="./images/Mode-RegionBound.png"
    ></figure>
</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Active Mode</th>
          <th>RegionBound Mode</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>DNS Scope</strong></td>
          <td>Each cluster manages its own region</td>
          <td>One cluster manages all regions</td>
      </tr>
      <tr>
          <td><strong>Traffic Pattern</strong></td>
          <td>Regional routing (low latency)</td>
          <td>Centralized routing (cross-region)</td>
      </tr>
      <tr>
          <td><strong>Best For</strong></td>
          <td>High availability, data residency</td>
          <td>Single-region services, cost optimization</td>
      </tr>
  </tbody>
</table>

<h3 class="relative group">Label-based conflict prevention
    <div id="label-based-conflict-prevention" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#label-based-conflict-prevention" aria-label="Anchor">#</a>
    </span>
    
</h3>
<p>To prevent ExternalDNS instances from interfering with each other, the operator sets a <code>router.io/region</code> label on every <code>DNSEndpoint</code>. Each ExternalDNS deployment is configured with <code>--label-filter=router.io/region=West Europe</code> (or <code>North Europe</code>), so it only processes records intended for it. This ensures West Europe ExternalDNS only writes to the West Europe DNS zone, and North Europe ExternalDNS only writes to the North Europe DNS zone — no conflicts, even in complex multi-cluster failover scenarios.</p>

<h1 class="relative group">Implementation
    <div id="implementation" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#implementation" aria-label="Anchor">#</a>
    </span>
    
</h1>
<p>The following guide walks through setting up a test environment on AKS with the Istio addon and External DNS, installing the operator, and deploying a service with automated DNS. Flux GitOps handles all cluster and workload configuration.</p>
<p><figure><img
    class="my-0 rounded-md"
    loading="lazy"
    decoding="async"
    fetchpriority="low"
    alt="aks-implementation"
    src="././images/AKS-Implementation.png"
    ></figure>
</p>
<p>The following steps are necessary to test the operator:</p>
<ul>
<li><a href="/posts/servicerouteroperator/#create-flux-managed-identity" >Create a Flux managed identity</a>
<ul>
<li>This managed identity will use workload identity and will be federated with the Flux controllers in the cluster to be able to retrieve artifacts from ACR.</li>
</ul>
</li>
<li><a href="/posts/servicerouteroperator/#create-an-aks-cluster" >Create an AKS cluster with the Istio addon &amp; Flux extension</a>
<ul>
<li>The AKS Istio Service Mesh addon provides a managed Istio installation, including the ingress gateway.</li>
<li>The AKS Flux extension provides a managed Flux v2 installation.</li>
</ul>
</li>
<li><a href="/posts/servicerouteroperator/#create-acr" >Create an Azure Container Registry</a>
<ul>
<li>An Azure Container Registry is needed to store the operator image and helm chart before deploying it to AKS.</li>
</ul>
</li>
<li><a href="/posts/servicerouteroperator/#configure-flux-identity" >Configure Workload Identity for Flux</a></li>
<li><a href="/posts/servicerouteroperator/#create-an-azure-private-dns-zone" >Create an Azure Private DNS Zone</a></li>
<li><a href="/posts/servicerouteroperator/#configure-workload-identity-for-externaldns" >Configure Workload Identity for ExternalDNS</a>
<ul>
<li>ExternalDNS needs a managed identity with permissions to write to the DNS zone. This will be configured using AKS Workload Identity.</li>
</ul>
</li>
<li><a href="/posts/servicerouteroperator/#build-and-push-the-operator-helm-chart" >Build and push the operator helm chart</a>
<ul>
<li>The helm chart will be pushed to the ACR so that it can be used by the Flux HelmRelease to deploy the service router operator.</li>
</ul>
</li>
<li><a href="/posts/servicerouteroperator/#build-and-push-the-operator-image" >Build and push the operator image</a>
<ul>
<li>The operator image will be pushed to the ACR so that it can be used by the AKS cluster as Kubernetes Operator.</li>
</ul>
</li>
<li><a href="/posts/servicerouteroperator/#configure-cluster-with-flux" >Configure cluster with Flux</a>
<ul>
<li>Flux will be used to configure the cluster with the GitOps manifests in the <a href="./gitops/" >gitops</a> folder.</li>
<li>Once configured, a <code>FluxConfig</code> is configured that that points at the Git repository and individual <code>Kustomization</code> resources that sync specific subfolders.</li>
<li>The <code>--kustomization</code> flags create two <code>Kustomization</code> resources in the <code>flux-system</code> namespace:</li>
</ul>
</li>
</ul>
<table>
  <thead>
      <tr>
          <th>Name</th>
          <th>Path</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>clusters</code></td>
          <td><code>gitops/clusters/base</code></td>
          <td>Installs External DNS, the Service Router and platform specific CRD&rsquo;s</td>
      </tr>
      <tr>
          <td><code>workloads</code></td>
          <td><code>gitops/workloads</code></td>
          <td>Configures the workload specific CRD&rsquo;s</td>
      </tr>
  </tbody>
</table>

<h2 class="relative group">Prerequisites
    <div id="prerequisites" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#prerequisites" aria-label="Anchor">#</a>
    </span>
    
</h2>
<ul>
<li>Azure CLI (<code>az</code>) with an active subscription
<ul>
<li>The following az extensions:
<ul>
<li><code>az extension add -n k8s-configuration</code></li>
<li><code>az extension add -n k8s-extension</code></li>
</ul>
</li>
</ul>
</li>
</ul>

<h2 class="relative group">Create Flux managed identity
    <div id="create-flux-managed-identity" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#create-flux-managed-identity" aria-label="Anchor">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Create managed identity for Flux</span>
</span></span><span class="line"><span class="cl">az identity create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="s2">&#34;id-flux&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">FLUX_CLIENT_ID</span><span class="o">=</span><span class="k">$(</span>az identity show <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="s2">&#34;id-flux&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --query clientId -o tsv<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">FLUX_TENANT_ID</span><span class="o">=</span><span class="k">$(</span>az identity show <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="s2">&#34;id-flux&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --query tenantId -o tsv<span class="k">)</span></span></span></code></pre></div></div>

<h2 class="relative group">Create an AKS Cluster
    <div id="create-an-aks-cluster" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#create-an-aks-cluster" aria-label="Anchor">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">RESOURCE_GROUP</span><span class="o">=</span><span class="s2">&#34;service-router-test-rg&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">CLUSTER_NAME</span><span class="o">=</span><span class="s2">&#34;aks-test&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">LOCATION</span><span class="o">=</span><span class="s2">&#34;westeurope&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">az group create --name <span class="nv">$RESOURCE_GROUP</span> --location <span class="nv">$LOCATION</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">az aks create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$CLUSTER_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --location <span class="nv">$LOCATION</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --node-count <span class="m">2</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --node-vm-size Standard_D2s_v3 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --enable-asm <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --enable-workload-identity <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --enable-oidc-issuer <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --generate-ssh-keys
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Enable the Istio Ingress gateway</span>
</span></span><span class="line"><span class="cl">az aks mesh enable-ingress-gateway <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$CLUSTER_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --ingress-gateway-type internal
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Install the Flux extension</span>
</span></span><span class="line"><span class="cl">az k8s-extension create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --cluster-name <span class="nv">$CLUSTER_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --cluster-type managedClusters <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name flux <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --extension-type microsoft.flux <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --config workloadIdentity.enable<span class="o">=</span><span class="nb">true</span> workloadIdentity.azureClientId<span class="o">=</span><span class="nv">$FLUX_CLIENT_ID</span> workloadIdentity.azureTenantId<span class="o">=</span><span class="nv">$FLUX_TENANT_ID</span></span></span></code></pre></div></div>
<p>Verify the Istio ingress gateway is running and has an IP address:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">az aks get-credentials --resource-group <span class="nv">$RESOURCE_GROUP</span> --name <span class="nv">$CLUSTER_NAME</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get svc -n aks-istio-ingress
</span></span><span class="line"><span class="cl"><span class="c1"># NAME                                TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># aks-istio-ingressgateway-internal   LoadBalancer   10.0.XXX.XXX   10.X.X.X      ...</span></span></span></code></pre></div></div>

<h2 class="relative group">Create ACR
    <div id="create-acr" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#create-acr" aria-label="Anchor">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">ACR_NAME</span><span class="o">=</span><span class="s2">&#34;serviceroutertestacr&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create the registry</span>
</span></span><span class="line"><span class="cl">az acr create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$ACR_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --sku Basic
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Get the ACR ID</span>
</span></span><span class="line"><span class="cl"><span class="nv">ACR_ID</span><span class="o">=</span><span class="k">$(</span>az acr show <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$ACR_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --query id -o tsv<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Attach the ACR to the AKS cluster</span>
</span></span><span class="line"><span class="cl">az aks update <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$CLUSTER_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --attach-acr <span class="nv">$ACR_NAME</span></span></span></code></pre></div></div>

<h2 class="relative group">Configure Flux identity
    <div id="configure-flux-identity" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#configure-flux-identity" aria-label="Anchor">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Get the AKS OIDC issuer</span>
</span></span><span class="line"><span class="cl"><span class="nv">OIDC_ISSUER</span><span class="o">=</span><span class="k">$(</span>az aks show <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$CLUSTER_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --query oidcIssuerProfile.issuerUrl -o tsv<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create federated credential for source-controller</span>
</span></span><span class="line"><span class="cl">az identity federated-credential create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name id-flux-source <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --identity-name id-flux <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --issuer <span class="nv">$OIDC_ISSUER</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --subject <span class="s2">&#34;system:serviceaccount:flux-system:source-controller&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --audience api://AzureADTokenExchange
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create federated credential for kustomize-controller</span>
</span></span><span class="line"><span class="cl">az identity federated-credential create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name id-flux-kustomize <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --identity-name id-flux <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --issuer <span class="nv">$OIDC_ISSUER</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --subject <span class="s2">&#34;system:serviceaccount:flux-system:kustomize-controller&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --audience api://AzureADTokenExchange
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Grant ACR Pull permissions to Flux identity</span>
</span></span><span class="line"><span class="cl">az role assignment create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --assignee <span class="nv">$FLUX_CLIENT_ID</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --role <span class="s2">&#34;AcrPull&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --scope <span class="nv">$ACR_ID</span></span></span></code></pre></div></div>

<h2 class="relative group">Create an Azure Private DNS Zone
    <div id="create-an-azure-private-dns-zone" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#create-an-azure-private-dns-zone" aria-label="Anchor">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">DNS_ZONE</span><span class="o">=</span><span class="s2">&#34;test-aks.nl&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">az network private-dns zone create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$DNS_ZONE</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Link the zone to the AKS VNet</span>
</span></span><span class="line"><span class="cl"><span class="nv">NODE_RESOURCE_GROUP</span><span class="o">=</span><span class="k">$(</span>az aks show <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$CLUSTER_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;nodeResourceGroup&#34;</span> -o tsv<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">VNET_ID</span><span class="o">=</span><span class="k">$(</span>az network vnet list <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$NODE_RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;[].id&#34;</span> -o tsv<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">az network private-dns link vnet create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --zone-name <span class="nv">$DNS_ZONE</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name aks-vnet-link <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --virtual-network <span class="nv">$VNET_ID</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --registration-enabled false</span></span></code></pre></div></div>

<h2 class="relative group">Configure Workload Identity for ExternalDNS
    <div id="configure-workload-identity-for-externaldns" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#configure-workload-identity-for-externaldns" aria-label="Anchor">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Get the AKS OIDC issuer</span>
</span></span><span class="line"><span class="cl"><span class="nv">OIDC_ISSUER</span><span class="o">=</span><span class="k">$(</span>az aks show <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$CLUSTER_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --query oidcIssuerProfile.issuerUrl -o tsv<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">SUBSCRIPTION_ID</span><span class="o">=</span><span class="k">$(</span>az account show --query id -o tsv<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">DNS_ZONE_ID</span><span class="o">=</span><span class="s2">&#34;/subscriptions/</span><span class="nv">$SUBSCRIPTION_ID</span><span class="s2">/resourceGroups/</span><span class="nv">$RESOURCE_GROUP</span><span class="s2">/providers/Microsoft.Network/privateDnsZones/</span><span class="nv">$DNS_ZONE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create managed identity for ExternalDNS</span>
</span></span><span class="line"><span class="cl">az identity create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="s2">&#34;id-external-dns-West Europe&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">CLIENT_ID</span><span class="o">=</span><span class="k">$(</span>az identity show <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="s2">&#34;id-external-dns-West Europe&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --query clientId -o tsv<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Grant DNS Zone Contributor on the private DNS zone</span>
</span></span><span class="line"><span class="cl">az role assignment create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --assignee <span class="nv">$CLIENT_ID</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --role <span class="s2">&#34;Private DNS Zone Contributor&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --scope <span class="nv">$DNS_ZONE_ID</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Create federated credential for Workload Identity</span>
</span></span><span class="line"><span class="cl">az identity federated-credential create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name external-dns-West Europe <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --identity-name id-external-dns-West Europe <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --issuer <span class="nv">$OIDC_ISSUER</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --subject <span class="s2">&#34;system:serviceaccount:ns-external-dns:external-dns-West Europe&#34;</span></span></span></code></pre></div></div>

<h2 class="relative group">Build and push the operator helm chart
    <div id="build-and-push-the-operator-helm-chart" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#build-and-push-the-operator-helm-chart" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The Helm chart for the operator can be found here: <a href="https://github.com/AshwinSarimin/service-router-operator/tree/main/charts/service-router-operator"  target="_blank" rel="noreferrer">service-router-operator</a></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Set chart version (update as needed)</span>
</span></span><span class="line"><span class="cl"><span class="nv">CHART_VERSION</span><span class="o">=</span><span class="s2">&#34;0.2.0&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Package the Helm chart from the charts directory</span>
</span></span><span class="line"><span class="cl">make helm-package
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Push the chart to ACR</span>
</span></span><span class="line"><span class="cl">helm push dist/service-router-operator-<span class="si">${</span><span class="nv">CHART_VERSION</span><span class="si">}</span>.tgz oci://<span class="si">${</span><span class="nv">ACR_NAME</span><span class="si">}</span>.azurecr.io/helm
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Verify the chart was pushed</span>
</span></span><span class="line"><span class="cl">az acr repository show <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$ACR_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --repository helm/service-router-operator
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Clean up the local package</span>
</span></span><span class="line"><span class="cl">rm service-router-operator-<span class="si">${</span><span class="nv">CHART_VERSION</span><span class="si">}</span>.tgz</span></span></code></pre></div></div>

<h2 class="relative group">Build and push the operator image
    <div id="build-and-push-the-operator-image" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#build-and-push-the-operator-image" aria-label="Anchor">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">az acr build <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --registry <span class="nv">$ACR_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --image service-router-operator:latest <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --file Dockerfile .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Verify the image is available in the registry:</span>
</span></span><span class="line"><span class="cl">az acr repository show-tags <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name <span class="nv">$ACR_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --repository service-router-operator <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --output table</span></span></code></pre></div></div>

<h2 class="relative group">Configure cluster with Flux
    <div id="configure-cluster-with-flux" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#configure-cluster-with-flux" aria-label="Anchor">#</a>
    </span>
    
</h2>
<p>The GitOps folders for the cluster can be found here: <a href="https://github.com/AshwinSarimin/service-router-operator/tree/main/gitops"  target="_blank" rel="noreferrer">gitops</a></p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">az k8s-configuration flux create <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --cluster-name <span class="nv">$CLUSTER_NAME</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --resource-group <span class="nv">$RESOURCE_GROUP</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --cluster-type managedClusters <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --name cluster-config <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --namespace flux-system <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --scope cluster <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --url https://github.com/AshwinSarimin/service-router-operator <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --branch main <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --kustomization <span class="nv">name</span><span class="o">=</span>clusters <span class="nv">path</span><span class="o">=</span>./gitops/clusters/base <span class="nv">prune</span><span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --kustomization <span class="nv">name</span><span class="o">=</span>workloads <span class="nv">path</span><span class="o">=</span>./gitops/workloads <span class="nv">prune</span><span class="o">=</span>true</span></span></code></pre></div></div>

<h2 class="relative group">Verify
    <div id="verify" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#verify" aria-label="Anchor">#</a>
    </span>
    
</h2>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check ClusterIdentity</span>
</span></span><span class="line"><span class="cl">kubectl get clusterIdentity -n ns-service-router clusteridentity -o yaml <span class="p">|</span> grep -A <span class="m">10</span> status
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check DNSConfiguration</span>
</span></span><span class="line"><span class="cl">kubectl get DNSConfiguration -n ns-service-router dnsconfiguration -o yaml <span class="p">|</span> grep -A <span class="m">10</span> status
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check Gateway</span>
</span></span><span class="line"><span class="cl">kubectl get gateway.routing.router.io -n ns-service-router app-gateway-ingress -o yaml <span class="p">|</span> grep -A <span class="m">10</span> status
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check ServiceRoute</span>
</span></span><span class="line"><span class="cl">kubectl get serviceroute -n ns-service-router test-workload-route -o yaml <span class="p">|</span> grep -A <span class="m">10</span> status
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check that the operator created DNSEndpoint resources</span>
</span></span><span class="line"><span class="cl">kubectl get dnsendpoints -n ns-service-router
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check what the DNSEndpoint contains</span>
</span></span><span class="line"><span class="cl">kubectl get dnsendpoint -n ns-service-router -o yaml
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Check ExternalDNS picked it up</span>
</span></span><span class="line"><span class="cl">kubectl logs -n ns-external-dns  -l app.kubernetes.io/name<span class="o">=</span>external-dns --tail<span class="o">=</span><span class="m">20</span></span></span></code></pre></div></div>
<p>Within about a minute, you should see the CNAME record in Azure Private DNS pointing to the Istio ingress gateway hostname, and the A record for the gateway hostname pointing to the LoadBalancer IP. Traffic from within the VNet to <code>test-workload-ns-service-router-workload-a.test-aks.nl</code> will now route through Istio to the test service.</p>

<h1 class="relative group">Copilot
    <div id="copilot" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#copilot" aria-label="Anchor">#</a>
    </span>
    
</h1>
<p>Building a production-grade Kubernetes operator requires expertise across multiple domains: Go, Kubernetes, DNS, Istio and more. By creating a persistent knowledge base that GitHub Copilot can reference throughout development, I transformed it into an expert pair programmer. This AI-assisted approach, combined with custom instructions and specialized agents, significantly accelerated the development process.</p>
<p>GitHub Copilot&rsquo;s custom instructions feature makes this possible, and I took it further by creating specialized AI agents for different roles:</p>
<p>The 3 agents each serve as expert consultants with distinct expertise:</p>
<ul>
<li><strong>Kubernetes Operator Agent</strong>: My primary development agent, used during active coding and code reviews. Lives and breathes Kubernetes operators.</li>
<li><strong>Devil&rsquo;s Advocate Agent</strong>: Consulted before major design decisions to stress-test ideas by challenging assumptions, focusing deeply on each concern, and summarizing both strong defenses and remaining vulnerabilities.</li>
<li><strong>Technical Writer Agent</strong>: Engaged when documenting features or writing tutorials. Provides step-by-step guidance and adapts to different writing styles (Docs, Tutorials, Architecture).</li>
</ul>
<p>The instructions are the knowledge base for domain-specific practices. These markdown files contain detailed guidelines that Copilot references automatically when working with specific file types:</p>
<ul>
<li><strong>go.instructions.md</strong>: Ensures idiomatic error handling, proper context usage, and consistent code structure</li>
<li><strong>go-operator.instructions.md</strong>: Defines controller standards and reconciliation patterns</li>
<li><strong>helm.instructions.md</strong>: Enforces security best practices and comprehensive health checks</li>
<li><strong>code-review.instructions.md</strong>: Provides a checklist that catches issues before they reach production</li>
</ul>
<p>Don&rsquo;t create agents until you have solid instruction files. Instructions are the knowledge base, agents are the interface. The key is starting with one good instruction file and building from there. Even a single go.instructions.md will dramatically improve AI code generation quality.</p>
<p>Order of development:</p>
<ul>
<li>Create basic instructions (go.instructions.md)</li>
<li>Add domain-specific instructions (go-operator.instructions.md)</li>
<li>Build specialized agents for specific workflows</li>
<li>Iterate based on actual usage patterns</li>
</ul>

<h1 class="relative group">Conclusion
    <div id="conclusion" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#conclusion" aria-label="Anchor">#</a>
    </span>
    
</h1>
<p>This solution modernized our DNS networking layer for the AKS Fleet, eliminating the operational burden of manual DNS management across regions. What once required careful coordination between platform and application teams is now a simple declarative resource.</p>
<p>With AI as a force multiplier, the project was completed faster than expected, though every line of code was reviewed and thoroughly tested before committing. The Devil&rsquo;s Advocate agent proved particularly valuable for validating design decisions before implementation.</p>
<p>If you&rsquo;re running AKS across multiple regions and DNS management is still a manual or scripted process, I hope this gives you some ideas.</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
