<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Alex</title>
    <description>The latest articles on Forem by Alex (@alex_coder19283).</description>
    <link>https://forem.com/alex_coder19283</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3874398%2F483685a5-f102-404b-8ac9-907d925c5bdf.png</url>
      <title>Forem: Alex</title>
      <link>https://forem.com/alex_coder19283</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/alex_coder19283"/>
    <language>en</language>
    <item>
      <title>We Had Secrets in Kubernetes. Then We Got Audited.</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Mon, 20 Apr 2026 08:45:00 +0000</pubDate>
      <link>https://forem.com/alex_coder19283/we-had-secrets-in-kubernetes-then-we-got-audited-4f63</link>
      <guid>https://forem.com/alex_coder19283/we-had-secrets-in-kubernetes-then-we-got-audited-4f63</guid>
      <description>&lt;p&gt;For the first two years of running workloads on AKS we stored secrets the way most teams do when they're moving fast. We created Kubernetes secrets, base64 encoded the values, committed the manifests to a private repo and told ourselves we'd clean it up later. Then a security audit flagged it and we had four weeks to fix it.&lt;/p&gt;

&lt;p&gt;This is the story of that migration and what we learned doing it under pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Kubernetes Secrets
&lt;/h2&gt;

&lt;p&gt;Kubernetes secrets are not actually secret in any meaningful security sense. They are base64 encoded which is encoding not encryption. Anyone with read access to the namespace can decode them in seconds. If your etcd is not encrypted at rest and someone gets access to a snapshot they have all your secrets in plaintext. And if a developer accidentally commits a secret manifest to a repo before the gitignore catches it the value is now in git history forever.&lt;/p&gt;

&lt;p&gt;We knew all of this. We just hadn't prioritised fixing it until the audit made it urgent.&lt;/p&gt;

&lt;p&gt;The audit finding was specific. We had database connection strings, third party API keys and internal service tokens stored as Kubernetes secrets across eight namespaces on three clusters. The auditor wanted to see secrets stored in a dedicated secrets manager with audit logging, rotation support and access policies tied to identities rather than cluster-level permissions.&lt;/p&gt;

&lt;p&gt;Azure Key Vault was the obvious answer. The question was how to get secrets from Key Vault into our pods without rebuilding how our applications consumed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Evaluated
&lt;/h2&gt;

&lt;p&gt;Our applications expected secrets either as environment variables or as files mounted into the container. We didn't want to change application code as part of this migration. That constraint ruled out a few approaches.&lt;/p&gt;

&lt;p&gt;The first option was to have applications call the Key Vault SDK directly. This would work but it meant changing code in a dozen services and introducing a new dependency and failure mode into each one. Not something we wanted to do under a four week deadline.&lt;/p&gt;

&lt;p&gt;The second option was to use the Azure Key Vault Provider for Secrets Store CSI Driver. This runs as a DaemonSet on your nodes and lets you define a SecretProviderClass resource that maps Key Vault secrets to files mounted into your pods. Optionally it can sync those secrets into Kubernetes secrets so applications that read environment variables still work without code changes. This was the right fit for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The setup has three components working together.&lt;/p&gt;

&lt;p&gt;The Secrets Store CSI Driver handles mounting secrets as volumes into pods. The Azure Key Vault Provider is the plugin that knows how to talk to Key Vault specifically. Workload Identity is how the pod authenticates to Key Vault without any credentials stored anywhere in the cluster.&lt;/p&gt;

&lt;p&gt;Workload Identity is worth pausing on. The way it works is that a Kubernetes service account is federated with an Azure Managed Identity. When a pod using that service account makes a request to Key Vault, Azure verifies the federation and grants access based on the Key Vault access policy attached to the Managed Identity. No secrets are involved in the authentication. No tokens to rotate. No credentials to leak.&lt;/p&gt;

&lt;p&gt;Setting it up looks like this.&lt;/p&gt;

&lt;p&gt;First you enable the CSI driver and Workload Identity on your cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; my-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-oidc-issuer&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-workload-identity&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-addons&lt;/span&gt; azure-keyvault-secrets-provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you create a Managed Identity and give it access to Key Vault.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az identity create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-workload-identity &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; my-rg

az keyvault set-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-keyvault &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--object-id&lt;/span&gt; &amp;lt;identity-principal-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-permissions&lt;/span&gt; get list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you federate the Kubernetes service account with the Managed Identity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az identity federated-credential create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-fed-credential &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--identity-name&lt;/span&gt; my-workload-identity &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; my-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--issuer&lt;/span&gt; &amp;lt;oidc-issuer-url&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subject&lt;/span&gt; system:serviceaccount:my-namespace:my-service-account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you define a SecretProviderClass that maps which Key Vault secrets you want and how they should appear in the pod.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secrets-store.csi.x-k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SecretProviderClass&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app-secrets&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-namespace&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;usePodIdentity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;
    &lt;span class="na"&gt;clientID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;managed-identity-client-id&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;keyvaultName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-keyvault&lt;/span&gt;
    &lt;span class="na"&gt;tenantID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;tenant-id&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;objects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;array:&lt;/span&gt;
        &lt;span class="s"&gt;- |&lt;/span&gt;
          &lt;span class="s"&gt;objectName: db-connection-string&lt;/span&gt;
          &lt;span class="s"&gt;objectType: secret&lt;/span&gt;
        &lt;span class="s"&gt;- |&lt;/span&gt;
          &lt;span class="s"&gt;objectName: api-key-external-service&lt;/span&gt;
          &lt;span class="s"&gt;objectType: secret&lt;/span&gt;
  &lt;span class="na"&gt;secretObjects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app-secrets&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;objectName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-connection-string&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DB_CONNECTION_STRING&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;objectName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-key-external-service&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EXTERNAL_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The secretObjects block is what creates the Kubernetes secret from the Key Vault values. Your pod can then reference it as an environment variable the same way it always did. No application changes required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke During Migration
&lt;/h2&gt;

&lt;p&gt;We migrated eight namespaces across three clusters over ten days. Here is what went wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sync delay we didn't know about&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The CSI driver syncs secret values from Key Vault on a polling interval. The default is two minutes. We discovered this when we rotated a Key Vault secret during testing and the running pods kept using the old value for two minutes after the rotation. This is expected behaviour but it meant our rotation procedure needed to account for this lag. If you have a hard dependency on immediate propagation after rotation you need to either reduce the sync interval or plan for a rolling restart of affected pods after rotation completes.&lt;/p&gt;

&lt;p&gt;We settled on a rotation runbook that updates the Key Vault secret, waits for the sync interval and then triggers a rollout restart on the affected deployments. Not fully automated yet but reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pods fail to start if Key Vault is unreachable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one caught us in staging. The CSI driver mounts the secret as a volume at pod startup. If Key Vault is unreachable when the pod starts the mount fails and the pod does not start at all. This is different from the old behaviour where the Kubernetes secret was already in the cluster and pod startup had no external dependency.&lt;/p&gt;

&lt;p&gt;During a brief Key Vault connectivity issue we had pods failing to restart after a node recycling event. The pods that were already running were fine. Any pod that needed to start fresh was stuck.&lt;/p&gt;

&lt;p&gt;The mitigation is to make sure your AKS clusters access Key Vault over a Private Endpoint rather than the public endpoint so the network path is more reliable and doesn't traverse the public internet. We had meant to do this from the start but hadn't gotten to it. This incident moved it up the priority list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access policy gaps we found late&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you grant a Managed Identity access to Key Vault you scope it to specific secret names or use a wildcard. We initially used wildcards to move fast. The auditor came back and asked us to scope permissions to specific secret names per workload. Going back and tightening those policies across all our Key Vault instances without breaking running workloads was tedious. We should have done it right the first time.&lt;/p&gt;

&lt;p&gt;The lesson is to treat Key Vault access policies like you treat Kubernetes RBAC. Least privilege from day one is much easier than retrofitting it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Audit Outcome Looked Like
&lt;/h2&gt;

&lt;p&gt;Four weeks after the finding we had all secrets migrated to Key Vault, Workload Identity configured on all clusters, Private Endpoints in place for Key Vault access and audit logging enabled in Azure Monitor so we could show exactly which identity accessed which secret and when. The auditor closed the finding.&lt;/p&gt;

&lt;p&gt;The audit log piece turned out to be more useful than expected. We surfaced a service account that was pulling a secret it had no business accessing because a developer had copy-pasted a service account name from another namespace. We wouldn't have caught that without the logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Tell Someone Starting This Today
&lt;/h2&gt;

&lt;p&gt;Don't wait for an audit. Set this up before you have secrets in Kubernetes at all. It takes a day to configure properly on a fresh cluster and it saves you the pain of migrating running workloads later.&lt;/p&gt;

&lt;p&gt;Enable Private Endpoints for Key Vault before you go to production. The public endpoint works but any network dependency at pod startup is a reliability risk you don't need.&lt;/p&gt;

&lt;p&gt;Scope access policies to specific secrets per workload from the beginning. The wildcard shortcut costs you later.&lt;/p&gt;

&lt;p&gt;Set up alerts on Key Vault diagnostic logs for denied access attempts. It's the fastest way to catch misconfigured identities and the occasional developer testing something they shouldn't be.&lt;/p&gt;

&lt;p&gt;And document your rotation procedure before you need it. The worst time to figure out how rotation works end-to-end across your clusters is when you're rotating a secret because it leaked.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>How We Set Up One Private Container Registry for 6 AKS Clusters Across 3 Regions and What Broke Along the Way</title>
      <dc:creator>Alex</dc:creator>
      <pubDate>Thu, 16 Apr 2026 03:52:30 +0000</pubDate>
      <link>https://forem.com/alex_coder19283/how-we-set-up-one-private-container-registry-for-6-aks-clusters-across-3-regions-and-what-broke-5h1j</link>
      <guid>https://forem.com/alex_coder19283/how-we-set-up-one-private-container-registry-for-6-aks-clusters-across-3-regions-and-what-broke-5h1j</guid>
      <description>&lt;p&gt;When our team started expanding from a single AKS cluster in East US to clusters across West Europe and Southeast Asia, the first thing we assumed was that container image management would be the easy part. It wasn't.&lt;/p&gt;

&lt;p&gt;This post walks through how we architected a single private container registry accessible by all six of our AKS clusters across three Azure regions. I'll cover what worked, what silently failed for weeks before we noticed, and the decisions I'd make differently today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup We Started With
&lt;/h2&gt;

&lt;p&gt;In the beginning we had one AKS cluster and one Azure Container Registry sitting in the same region. The CI/CD pipeline would build an image, push it to ACR, and AKS would pull it. Simple.&lt;/p&gt;

&lt;p&gt;Then we added a second cluster in West Europe for latency reasons. And a third in Southeast Asia for compliance reasons. Suddenly we had a problem that sounds simple but has a lot of moving parts: how does every cluster pull images securely without us managing a pile of credentials and without paying a fortune in cross-region egress fees.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Evaluated
&lt;/h2&gt;

&lt;p&gt;We looked at three approaches before landing on our final architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: One central ACR, all clusters pull from it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The simplest option and the one we tried first. Every cluster just pointed to the same ACR endpoint regardless of where it was running. This worked fine in testing. In production it caused two problems. First the pull latency during cluster upgrades was noticeable since every node was pulling large images over a long network path. Second when we had a brief connectivity blip in East US one night clusters in other regions couldn't pull images and pod restarts started failing. A registry in one region had become a single point of failure for the entire platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Separate ACR per region&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We considered running an independent ACR in each region and pushing images to all three from the pipeline. This solved the latency and availability problem but created a worse one. Now our pipeline had to push to three registries on every build. Image promotion between environments became a mess. And keeping digests consistent across registries turned out to be harder than expected since different push operations under load would sometimes result in slightly different metadata depending on timing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3: ACR Geo-Replication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is what we landed on. ACR's Premium SKU supports geo-replication where you maintain a single registry logically but Azure replicates your images automatically to replicas in whichever regions you choose. You push once and every regional replica gets the image. Clusters in each region pull from their nearest replica automatically with no changes to image references in your manifests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture We Run Today
&lt;/h2&gt;

&lt;p&gt;Here is the high level picture.&lt;/p&gt;

&lt;p&gt;Our CI/CD pipeline in Azure DevOps builds the image and pushes to a single ACR endpoint in East US. ACR handles replication to West Europe and Southeast Asia replicas in the background. Each AKS cluster is configured to authenticate using Managed Identity so there are no image pull secrets to rotate or manage. Azure handles the auth handshake between AKS and ACR natively.&lt;/p&gt;

&lt;p&gt;The command to wire up a cluster to ACR is straightforward.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az aks update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; my-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attach-acr&lt;/span&gt; my-registry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one command grants the cluster's managed identity the AcrPull role on the registry. Do this for each cluster and you're done with credentials.&lt;/p&gt;

&lt;p&gt;For geo-replication you add replicas through the portal or CLI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az acr replication create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--registry&lt;/span&gt; my-registry &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; westeurope

az acr replication create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--registry&lt;/span&gt; my-registry &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; southeastasia
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this any image you push to the registry replicates to both locations within a few minutes depending on image size.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Broke and When
&lt;/h2&gt;

&lt;p&gt;I want to be honest about the parts that didn't go smoothly because these are the things no architecture diagram ever shows you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replication lag during fast rollouts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our first incident with this setup was subtle. We pushed a new image and triggered a rollout within seconds of the push completing. The East US cluster picked up the new image fine since it was pulling from the local replica. But the West Europe cluster tried to pull before replication had completed and got an image not found error. Pods went into ImagePullBackoff and we spent 20 minutes confused about why the same rollout was failing in one region and succeeding in another.&lt;/p&gt;

&lt;p&gt;The fix was to add a replication wait check in our pipeline before triggering rollouts across all regions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az acr replication show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; westeurope &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--registry&lt;/span&gt; my-registry &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"status.displayStatus"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We poll this until it returns "Synced" before allowing the pipeline to proceed to the rollout stage. Simple but not something you'd think to add until you've been burned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Digest pinning across replicas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We use digest pinning in production so that what gets deployed is exactly what got scanned and approved. The assumption was that the same image pushed to ACR would have the same digest everywhere. That assumption held in our case but it is worth explicitly verifying in your setup because some replication tools and registry configurations can rewrite manifest metadata in ways that change the digest. If your digest changes between regions your GitOps tooling will treat them as different images and you'll have a very confusing debugging session ahead of you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node pool scaling pulling a lot at once&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cluster autoscaler adding multiple nodes simultaneously means many nodes pulling the same large image at the same time. During a traffic spike we scaled out 12 nodes in Southeast Asia within two minutes. Each node started pulling a 2.4GB image independently. The registry replica handled it but we saw throttling warnings in our ACR metrics that we hadn't seen before. We addressed this by implementing image pre-pulling using DaemonSets for our heaviest images and by tuning the parallel image pull settings in the kubelet config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Egress costs were not zero&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even with geo-replication you pay for replication traffic between regions. For small teams with small images this is negligible. For us with images averaging around 800MB across a dozen services the monthly replication cost was noticeable. It didn't break the budget but it was something we hadn't accounted for in our initial cost estimates. Factor this in before you go to your engineering manager with a cost projection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;If I were starting this from scratch today here's what I'd change.&lt;/p&gt;

&lt;p&gt;First I'd instrument the registry from day one. ACR exposes metrics for pull latency, error rates and replication lag. We didn't set up alerts on these until after our first incident. Add them before you need them.&lt;/p&gt;

&lt;p&gt;Second I'd enforce digest pinning in production from the start using something like Kyverno. We retrofitted this policy later and it was painful to roll out across six clusters without disrupting running workloads.&lt;/p&gt;

&lt;p&gt;Third I'd test regional failover before an actual outage forces you to. Simulate a replica going dark and confirm your clusters handle it gracefully. We did this six months in and found an edge case where one of our clusters had a stale DNS cache that caused it to keep trying the failed replica instead of failing over. Finding that in a drill at 2pm is much better than finding it during an actual incident at 2am.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;ACR geo-replication with Managed Identity auth is a genuinely good solution for multi-region AKS setups. The operational overhead is low compared to running your own Harbor instances or managing push-to-multiple-registries pipelines. But like everything in distributed systems the edge cases live in the timing and the assumptions.&lt;/p&gt;

&lt;p&gt;Watch your replication lag. Pin your digests. Alert on your registry metrics. And test your failover before production tests it for you.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>docker</category>
    </item>
  </channel>
</rss>
