<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://blog.glen-thomas.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.glen-thomas.com/" rel="alternate" type="text/html" /><updated>2026-05-14T13:37:18+01:00</updated><id>https://blog.glen-thomas.com/feed.xml</id><title type="html">Glen Thomas</title><subtitle>Personal tech blog for Glen Thomas</subtitle><author><name>Glen Thomas</name></author><entry><title type="html">When AAD Pod Identity Breaks Flux: A Deep Dive into a Hidden AKS Failure Mode</title><link href="https://blog.glen-thomas.com/platform%20engineering/2026/05/14/when-aad-pod-identity-breaks-flux.html" rel="alternate" type="text/html" title="When AAD Pod Identity Breaks Flux: A Deep Dive into a Hidden AKS Failure Mode" /><published>2026-05-14T09:18:00+01:00</published><updated>2026-05-14T09:18:00+01:00</updated><id>https://blog.glen-thomas.com/platform%20engineering/2026/05/14/when-aad-pod-identity-breaks-flux</id><content type="html" xml:base="https://blog.glen-thomas.com/platform%20engineering/2026/05/14/when-aad-pod-identity-breaks-flux.html"><![CDATA[<p>If you run AKS long enough, you eventually meet the ghosts of deprecated features past.<br />
This is the story of how <strong>AAD Pod Identity</strong>, long deprecated but still lurking in many clusters, silently broke <strong>Flux image automation</strong>, and how to diagnose and fix it when it happens to you.</p>

<p>This post documents the symptoms, the root cause, and the exact steps required to recover a cluster stuck in this half‑installed, half‑removed state.</p>

<hr />

<h2 id="the-setup">The Setup</h2>

<p>This cluster was:</p>

<ul>
  <li>Running <strong>AKS</strong> with the old <strong>AAD Pod Identity</strong> Helm chart still installed</li>
  <li>Missing <strong>MIC</strong> (Managed Identity Controller)</li>
  <li>Still running <strong>NMI</strong> (Node Managed Identity) on every node</li>
  <li>Running <strong>Flux v2</strong> with image automation enabled</li>
  <li>Using <strong>ACR</strong> with a <strong>node‑assigned managed identity</strong></li>
</ul>

<p>This combination is more common than you’d think—especially in clusters upgraded over several years.</p>

<hr />

<h2 id="the-symptoms">The Symptoms</h2>

<p>Flux’s <code class="language-plaintext highlighter-rouge">image-reflector-controller</code> began failing with IMDS authentication errors:</p>

<blockquote>
  <p>failed to configure authentication options: failed to create provider access token for the controller: ManagedIdentityCredential: context deadline exceeded</p>
</blockquote>

<p>NMI logs showed:</p>

<blockquote>
  <p>failed to get matching identities for pod: flux-system/image-reflector-controller…</p>
</blockquote>

<p>Flux could not:</p>

<ul>
  <li>Query ACR</li>
  <li>Populate its tag database</li>
  <li>Resolve image policies</li>
</ul>

<p>Image automation was effectively dead.</p>

<hr />

<h2 id="diagnosis-the-smoking-gun">Diagnosis: The Smoking Gun</h2>

<p>Running <code class="language-plaintext highlighter-rouge">kubectl get pods -n kube-system | grep nmi</code> revealed <strong>dozens of NMI pods</strong>, all running for hundreds of days.</p>

<p><code class="language-plaintext highlighter-rouge">kubectl get crd | grep aadpodidentity</code> revealed <strong>AAD Pod Identity CRDs still installed</strong>.</p>

<p>But running <code class="language-plaintext highlighter-rouge">kubectl get pods -n kube-system | grep mic</code> showed <strong>no MIC pods at all</strong>.</p>

<p>This is the broken state:</p>

<ul>
  <li><strong>NMI intercepts IMDS calls</strong></li>
  <li><strong>MIC is missing</strong>, so identities are never assigned</li>
  <li>Every IMDS call from a pod fails</li>
  <li>Flux cannot authenticate to ACR</li>
</ul>

<p>This is where the cluster was stuck.</p>

<h2 id="why-this-breaks-flux">Why This Breaks Flux</h2>

<p>Flux’s controllers authenticate to ACR using the <strong>node’s managed identity</strong> via IMDS.</p>

<p>But NMI intercepts IMDS calls and tries to look up a pod’s assigned identity.</p>

<p>With MIC missing, NMI always returns:</p>

<blockquote>
  <p>no AzureAssignedIdentity found</p>
</blockquote>

<p>Flux never reaches IMDS → never reaches ACR → never sees image tags.</p>

<hr />

<h2 id="the-fix-azurepodidentityexception">The Fix: AzurePodIdentityException</h2>

<p>The solution is to tell NMI:</p>

<blockquote>
  <p>“Do not intercept IMDS calls for Flux pods.”</p>
</blockquote>

<p>This is done using an <code class="language-plaintext highlighter-rouge">AzurePodIdentityException</code>.</p>

<p>But here’s the twist:<br />
<strong>AAD Pod Identity has multiple CRD schemas depending on the version</strong>, and this cluster was running a very old one.</p>

<p>Attempts using modern fields like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spec</span><span class="pi">:</span>
  <span class="na">podSelector</span><span class="pi">:</span>
</code></pre></div></div>

<p>or</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spec</span><span class="pi">:</span>
  <span class="na">PodLabels</span><span class="pi">:</span>
</code></pre></div></div>

<p>were rejected.</p>

<p>The correct schema, discovered by inspecting existing exceptions (<code class="language-plaintext highlighter-rouge">kubectl get azurepodidentityexception -n flux-system -o yaml</code>), was:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">spec</span><span class="pi">:</span>
  <span class="na">podLabels</span><span class="pi">:</span>
    <span class="na">&lt;label&gt;</span><span class="pi">:</span> <span class="s">&lt;value&gt;</span>
</code></pre></div></div>

<p>I discovered a couple of existing exception for Flux that didn’t cover the label used by the image-reflector-controller pod:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spec:
  podLabels:
    app.kubernetes.io/name: flux-extension
</code></pre></div></div>

<h3 id="creating-the-correct-exception">Creating the Correct Exception</h3>

<p>I inspected the labels of the pods in the flux-system namespace:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods -n flux-system --show-labels
</code></pre></div></div>

<p>Flux controllers all share this label:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>app.kubernetes.io/name=microsoft.flux
</code></pre></div></div>

<p>So the correct exception is:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">aadpodidentity.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">AzurePodIdentityException</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">flux-system-exception</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">flux-system</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">podLabels</span><span class="pi">:</span>
    <span class="na">app.kubernetes.io/name</span><span class="pi">:</span> <span class="s">microsoft.flux</span>
</code></pre></div></div>

<p>Apply it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply -f flux-system-exception.yaml
</code></pre></div></div>

<p>Then restart Flux:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl rollout restart deploy -n flux-system
</code></pre></div></div>

<h2 id="verifying-the-fix">Verifying the Fix</h2>

<p>Check NMI logs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl logs -n kube-system -l app.kubernetes.io/component=nmi --tail=200
</code></pre></div></div>

<p>Before the fix:</p>

<blockquote>
  <p>failed to get matching identities for pod flux-system/image-reflector-controller…</p>
</blockquote>

<p>After the fix no errors are shown.</p>

<p>Flux logs then showed healthy behaviour:</p>

<blockquote>
  <p>successful scan: found 1 tags</p>

  <p>Latest image tag … resolved to prod-8552309f0-20260512T090128</p>
</blockquote>

<p>Image automation was back.</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>This issue is obscure, poorly documented, and easy to miss.
If you’re running Flux on AKS and see IMDS or ACR authentication failures, check for:</p>

<ul>
  <li>NMI pods still running</li>
  <li>MIC missing</li>
  <li>Old AAD Pod Identity CRDs</li>
  <li>Missing or incorrect <code class="language-plaintext highlighter-rouge">AzurePodIdentityException</code> objects</li>
</ul>

<p>Hopefully this post saves someone else the hours of digging it took to unravel this.</p>]]></content><author><name>Glen Thomas</name></author><category term="Platform Engineering" /><category term="Kubernetes" /><category term="AKS" /><summary type="html"><![CDATA[If you run AKS long enough, you eventually meet the ghosts of deprecated features past. This is the story of how AAD Pod Identity, long deprecated but still lurking in many clusters, silently broke Flux image automation, and how to diagnose and fix it when it happens to you.]]></summary></entry><entry><title type="html">Signing Container Images with Cosign (and Notation) + Storing Signatures, SBOMs, and Attestations in Azure Container Registry</title><link href="https://blog.glen-thomas.com/software%20engineering/security/2026/01/20/signing-container-images-with-cosign.html" rel="alternate" type="text/html" title="Signing Container Images with Cosign (and Notation) + Storing Signatures, SBOMs, and Attestations in Azure Container Registry" /><published>2026-01-20T09:10:00+00:00</published><updated>2026-01-20T09:10:00+00:00</updated><id>https://blog.glen-thomas.com/software%20engineering/security/2026/01/20/signing-container-images-with-cosign</id><content type="html" xml:base="https://blog.glen-thomas.com/software%20engineering/security/2026/01/20/signing-container-images-with-cosign.html"><![CDATA[<p>Signing container images is one of the highest-leverage things you can do to improve your software supply chain. A signature gives you a cryptographic way to answer “who produced this image?” and “has it been tampered with?”. Attestations (for example: provenance and SBOMs) help you answer deeper questions like “how was it built?” and “what’s inside it?”.</p>

<p>This post is a practical reference for:</p>

<ul>
  <li>Signing OCI container images with <code class="language-plaintext highlighter-rouge">cosign</code> (Sigstore)</li>
  <li>Generating and attaching attestations (including SBOM-related examples)</li>
  <li>Storing signatures/attestations alongside images in Azure Container Registry (ACR)</li>
  <li>Understanding how the OCI Referrers API organises “artifacts about artifacts”</li>
  <li>Briefly comparing Cosign to Notation (Notary Project)</li>
</ul>

<h2 id="key-concepts-what-are-we-actually-storing">Key concepts (what are we actually storing?)</h2>

<p>When people say “sign the image” they often mean a few related, but distinct things.</p>

<h3 id="signatures">Signatures</h3>

<p>A signature is a statement like:</p>

<blockquote>
  <p>“I (an identity) signed this immutable image digest.”</p>
</blockquote>

<p>The important part is that you sign the image <em>digest</em> (<code class="language-plaintext highlighter-rouge">@sha256:...</code>), not the <em>tag</em> (<code class="language-plaintext highlighter-rouge">:latest</code>), because tags are mutable.</p>

<h3 id="attestations">Attestations</h3>

<p>An attestation is a signed statement about the image (or about another artifact attached to the image). Examples:</p>

<ul>
  <li>Provenance: where the build ran, what sources were used, what inputs were consumed</li>
  <li>SBOM: a machine-readable list of packages/components contained in the image</li>
  <li>Vulnerability scan results (careful: these go stale quickly)</li>
</ul>

<p>Cosign signs attestations using DSSE envelopes and commonly uses the in-toto attestation format.</p>

<h3 id="sboms">SBOMs</h3>

<p>An SBOM is a document — it’s valuable when you:</p>

<ul>
  <li>Generate it consistently</li>
  <li>Store it alongside the exact digest you shipped</li>
  <li>Sign it (so people can trust its origin)</li>
  <li>Make it discoverable by automation</li>
</ul>

<h2 id="why-sign-images-and-generate-attestations">Why sign images and generate attestations?</h2>

<p>If you’re already scanning images, signatures and attestations provide the missing chain of custody.</p>

<ul>
  <li><strong>Integrity</strong>: detect tampering (a signature ties to an immutable digest)</li>
  <li><strong>Authenticity</strong>: verify “this came from our pipeline/team”, not just “it exists in our registry”</li>
  <li><strong>Policy enforcement</strong>: gate deployments on verified signatures/attestations (Kubernetes admission, CI/CD checks)</li>
  <li><strong>Auditability</strong>: prove what was deployed and when, and how it was built</li>
  <li><strong>Better incident response</strong>: quickly answer “which builds include vulnerable component X?” when you have SBOMs attached to digests</li>
</ul>

<h2 id="where-do-signaturesattestations-live-oci-artifacts--the-referrers-api">Where do signatures/attestations live? OCI Artifacts + the Referrers API</h2>

<p>Modern container signing doesn’t embed signatures “inside” an image. Instead, registries store <em>additional OCI artifacts</em> that refer to the image.</p>

<p>The OCI Distribution Spec defines a <strong>Referrers API</strong> (OCI v1.1) that lets a client ask:</p>

<blockquote>
  <p>“List all artifacts that refer to this subject digest.”</p>
</blockquote>

<p>That makes signatures, SBOMs, provenance, scan reports, etc. first-class, discoverable artifacts associated with an image.</p>

<p>Azure Container Registry supports storing and discovering these supply-chain artifacts. Tools like ORAS can show the attached artifact graph, and ACR supports the OCI Referrers API in most scenarios.</p>

<p>Note: some environments may fall back to the older “referrers tag schema” approach instead of the Referrers API. For example, ACR’s documentation notes that some features (notably customer-managed key encrypted registries) may require fallback behavior.</p>

<h2 id="cosign-vs-notation-quick-comparison">Cosign vs Notation (quick comparison)</h2>

<p>Both Cosign and Notation ultimately store “signatures as OCI artifacts” in registries, but they’re optimised for different workflows.</p>

<h3 id="cosign-sigstore">Cosign (Sigstore)</h3>

<ul>
  <li>Excellent developer experience for <strong>keyless signing</strong> using OIDC identities (Fulcio certificates) and transparency logging (Rekor)</li>
  <li>Strong support for <strong>attestations</strong> (provenance, SBOM predicates, etc.)</li>
  <li>Works well in modern CI/CD environments where identity is provided by an OIDC issuer (GitHub Actions, etc.)</li>
</ul>

<h3 id="notation-notary-project">Notation (Notary Project)</h3>

<ul>
  <li>Industry standardisation effort for OCI signing with an emphasis on <strong>X.509 / trust policies</strong></li>
  <li>Strong fit for <strong>enterprise key management</strong>, including KMS/HSM-backed keys (for example via an Azure Key Vault plugin)</li>
  <li>Focused primarily on signing/verifying artifacts; attestations are commonly handled via separate OCI artifacts and tooling</li>
</ul>

<p>If you’re building a modern “provenance + SBOM + policy” workflow, Cosign is often the fastest path. If you need a standardised signature format and strong enterprise policy/trust-store modeling (and/or a vendor-supported path), Notation is worth serious consideration.</p>

<h2 id="walkthrough-build-push-sign-and-attach-attestations-in-acr">Walkthrough: build, push, sign, and attach attestations in ACR</h2>

<p>This walkthrough uses:</p>

<ul>
  <li>Azure Container Registry (ACR)</li>
  <li><code class="language-plaintext highlighter-rouge">cosign</code> for signing and attestations</li>
  <li><code class="language-plaintext highlighter-rouge">oras</code> to <em>discover</em> and reason about the artifact graph (Referrers API)</li>
</ul>

<h3 id="prerequisites">Prerequisites</h3>

<ul>
  <li>Azure CLI (<code class="language-plaintext highlighter-rouge">az</code>) authenticated to your subscription</li>
  <li>ACR with permissions to push images and artifacts</li>
  <li>Tools:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">cosign</code></li>
      <li><code class="language-plaintext highlighter-rouge">oras</code></li>
      <li><code class="language-plaintext highlighter-rouge">jq</code> (optional, but helpful)</li>
      <li>SBOM generator such as <code class="language-plaintext highlighter-rouge">syft</code> (optional)</li>
    </ul>
  </li>
</ul>

<p>On macOS you can typically install these with Homebrew (adjust as needed):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>cosign oras jq syft
</code></pre></div></div>

<h3 id="1-create-an-acr-and-sign-in">1) Create an ACR and sign in</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az login

<span class="nb">export </span><span class="nv">LOCATION</span><span class="o">=</span>uksouth
<span class="nb">export </span><span class="nv">RG</span><span class="o">=</span>rg-acr-signing-demo
<span class="nb">export </span><span class="nv">ACR_NAME</span><span class="o">=</span>&lt;your-unique-acr-name&gt;

az group create <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$RG</span><span class="s2">"</span> <span class="nt">-l</span> <span class="s2">"</span><span class="nv">$LOCATION</span><span class="s2">"</span>
az acr create <span class="nt">-g</span> <span class="s2">"</span><span class="nv">$RG</span><span class="s2">"</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$ACR_NAME</span><span class="s2">"</span> <span class="nt">--sku</span> Standard

<span class="nb">export </span><span class="nv">REGISTRY</span><span class="o">=</span><span class="s2">"</span><span class="nv">$ACR_NAME</span><span class="s2">.azurecr.io"</span>
az acr login <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$ACR_NAME</span><span class="s2">"</span>
</code></pre></div></div>

<h3 id="2-build-and-push-an-image-and-capture-the-digest">2) Build and push an image (and capture the digest)</h3>

<p>You can build locally with Docker and push, but for a clean “works anywhere” demo, ACR Tasks is handy.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">REPO</span><span class="o">=</span>demo/net-monitor
<span class="nb">export </span><span class="nv">TAG</span><span class="o">=</span>v1

<span class="c"># Example source repo used in Microsoft docs; swap this for your own Dockerfile repo.</span>
<span class="nb">export </span><span class="nv">IMAGE_SOURCE</span><span class="o">=</span>https://github.com/wabbit-networks/net-monitor.git#main

<span class="nv">DIGEST</span><span class="o">=</span><span class="si">$(</span>az acr build <span class="se">\</span>
	<span class="nt">-r</span> <span class="s2">"</span><span class="nv">$ACR_NAME</span><span class="s2">"</span> <span class="se">\</span>
	<span class="nt">-t</span> <span class="s2">"</span><span class="nv">$REGISTRY</span><span class="s2">/</span><span class="k">${</span><span class="nv">REPO</span><span class="k">}</span><span class="s2">:</span><span class="nv">$TAG</span><span class="s2">"</span> <span class="se">\</span>
	<span class="s2">"</span><span class="nv">$IMAGE_SOURCE</span><span class="s2">"</span> <span class="se">\</span>
	<span class="nt">--no-logs</span> <span class="se">\</span>
	<span class="nt">--query</span> <span class="s2">"outputImages[0].digest"</span> <span class="nt">-o</span> tsv<span class="si">)</span>

<span class="nb">export </span><span class="nv">IMAGE_DIGEST</span><span class="o">=</span><span class="s2">"</span><span class="nv">$REGISTRY</span><span class="s2">/</span><span class="k">${</span><span class="nv">REPO</span><span class="k">}</span><span class="s2">@</span><span class="nv">$DIGEST</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span>
</code></pre></div></div>

<p>From here on, use <code class="language-plaintext highlighter-rouge">$IMAGE_DIGEST</code> (digest reference) for signing and attestations.</p>

<h3 id="3-sign-the-image-with-cosign-keyless">3) Sign the image with Cosign (keyless)</h3>

<p>Keyless signing is Cosign’s default “batteries included” path.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cosign sign <span class="nt">--yes</span> <span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span>
</code></pre></div></div>

<p>What to expect:</p>

<ul>
  <li>A browser-based OIDC flow (unless you’re already in a CI environment with an OIDC token)</li>
  <li>A short-lived signing certificate issued by Sigstore’s Fulcio</li>
  <li>A transparency log entry in Rekor (by default)</li>
  <li>A signature stored in your registry as an OCI artifact referring to the image</li>
</ul>

<h3 id="4-verify-the-cosign-signature">4) Verify the Cosign signature</h3>

<p>Verification should be part of your CI and/or cluster admission policy. The important idea is: verify not only “is it signed?” but “is it signed by the identity we trust?”.</p>

<p>For interactive keyless signing, the OIDC issuer is commonly:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">OIDC_ISSUER</span><span class="o">=</span><span class="s2">"https://oauth2.sigstore.dev/auth"</span>
</code></pre></div></div>

<p>And the identity is typically the OIDC subject (often your email). Example:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cosign verify <span class="se">\</span>
	<span class="nt">--certificate-identity</span> <span class="s2">"you@example.com"</span> <span class="se">\</span>
	<span class="nt">--certificate-oidc-issuer</span> <span class="s2">"</span><span class="nv">$OIDC_ISSUER</span><span class="s2">"</span> <span class="se">\</span>
	<span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span>
</code></pre></div></div>

<p>In CI (for example GitHub Actions), you’d instead verify against that workflow identity and issuer (for example <code class="language-plaintext highlighter-rouge">https://token.actions.githubusercontent.com</code>), often using the <code class="language-plaintext highlighter-rouge">--certificate-identity-regexp</code> flags to match your org/repo/workflow.</p>

<h3 id="5-generate-an-sbom-and-attach-it-as-an-attestation-cosign">5) Generate an SBOM and attach it as an attestation (Cosign)</h3>

<p>One straightforward pattern is:</p>

<p>1) Generate an SBOM file
2) Attest to it (sign a statement that includes it)</p>

<p>Generate an SBOM with Syft (SPDX JSON example):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>syft packages <span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span> <span class="nt">-o</span> spdx-json<span class="o">=</span>sbom.spdx.json
</code></pre></div></div>

<p>Create an attestation for that SBOM:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cosign attest <span class="nt">--yes</span> <span class="se">\</span>
	<span class="nt">--predicate</span> sbom.spdx.json <span class="se">\</span>
	<span class="nt">--type</span> spdxjson <span class="se">\</span>
	<span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span>
</code></pre></div></div>

<p>Verify the attestation:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cosign verify-attestation <span class="se">\</span>
	<span class="nt">--certificate-identity</span> <span class="s2">"you@example.com"</span> <span class="se">\</span>
	<span class="nt">--certificate-oidc-issuer</span> <span class="s2">"</span><span class="nv">$OIDC_ISSUER</span><span class="s2">"</span> <span class="se">\</span>
	<span class="nt">--type</span> spdxjson <span class="se">\</span>
	<span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span> <span class="se">\</span>
	| jq <span class="nt">-r</span> <span class="s1">'.[0].payload'</span> <span class="se">\</span>
	| <span class="nb">base64</span> <span class="nt">--decode</span>
</code></pre></div></div>

<h3 id="6-discover-whats-attached-oci-referrers-with-oras">6) Discover what’s attached (OCI Referrers) with ORAS</h3>

<p>This is the piece most people miss: once signatures/SBOMs/attestations are stored as OCI artifacts, you can query the graph.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oras discover <span class="nt">-o</span> tree <span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span>
</code></pre></div></div>

<p>You should see the subject image at the top, with child artifact types beneath it (signatures, attestations, and any other artifacts you attach).</p>

<h2 id="alternative-pattern-store-the-sbom-as-an-oci-artifact-then-sign-that">Alternative pattern: store the SBOM as an OCI artifact, then sign <em>that</em></h2>

<p>Sometimes you want the SBOM itself stored as a first-class artifact in the registry (so it can be pulled directly), and then you sign/attest to <em>that</em> artifact. OCI referrers support deep graphs: image → SBOM → signature.</p>

<p>Attach an SBOM file to the image as a referenced OCI artifact:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oras attach <span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span> <span class="se">\</span>
	<span class="nt">--artifact-type</span> sbom/example <span class="se">\</span>
	./sbom.spdx.json:application/json
</code></pre></div></div>

<p>Discover again:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oras discover <span class="nt">-o</span> tree <span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span>
</code></pre></div></div>

<p>This pattern is powerful when you want:</p>

<ul>
  <li>direct pull of SBOMs (<code class="language-plaintext highlighter-rouge">oras pull</code>), separate from verification tooling</li>
  <li>to sign the SBOM artifact independently (for example with Notation)</li>
</ul>

<h2 id="promoting-images-with-their-signaturesattestations">Promoting images <em>with</em> their signatures/attestations</h2>

<p>If you promote images across registries/environments, you should promote the <em>artifact graph</em>, not only the image manifest.</p>

<p>With ORAS you can copy an image and its referrers:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">TARGET_REPO</span><span class="o">=</span><span class="s2">"</span><span class="nv">$REGISTRY</span><span class="s2">/staging/</span><span class="nv">$REPO</span><span class="s2">"</span>
oras copy <span class="nt">-r</span> <span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$TARGET_REPO</span><span class="s2">:</span><span class="nv">$TAG</span><span class="s2">"</span>
</code></pre></div></div>

<p>Then:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oras discover <span class="nt">-o</span> tree <span class="s2">"</span><span class="nv">$TARGET_REPO</span><span class="s2">:</span><span class="nv">$TAG</span><span class="s2">"</span>
</code></pre></div></div>

<h2 id="notation-brief-practical-example-in-azure">Notation (brief practical example in Azure)</h2>

<p>If you’re evaluating Notation in Azure, Microsoft provides a walkthrough that signs images in ACR using keys held in Azure Key Vault. The high-level flow looks like:</p>

<p>1) Install Notation + the Azure Key Vault plugin
2) Build/push image and sign by digest
3) Add certs to a trust store and create a trust policy
4) Verify</p>

<p>Signing example (Key Vault plugin; simplified):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>notation sign <span class="se">\</span>
	<span class="nt">--signature-format</span> cose <span class="se">\</span>
	<span class="nt">--id</span> <span class="s2">"</span><span class="nv">$KEY_ID</span><span class="s2">"</span> <span class="se">\</span>
	<span class="nt">--plugin</span> azure-kv <span class="se">\</span>
	<span class="nt">--plugin-config</span> <span class="nv">self_signed</span><span class="o">=</span><span class="nb">true</span> <span class="se">\</span>
	<span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span>
</code></pre></div></div>

<p>And list signatures:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>notation <span class="nb">ls</span> <span class="s2">"</span><span class="nv">$IMAGE_DIGEST</span><span class="s2">"</span>
</code></pre></div></div>

<p>An important nuance called out in Microsoft’s guidance: Notation can store signatures using the referrers tag schema by default, and can also use the OCI Referrers API depending on configuration/registry capabilities.</p>

<h2 id="common-pitfalls">Common pitfalls</h2>

<ul>
  <li><strong>Always sign by digest</strong> (<code class="language-plaintext highlighter-rouge">@sha256:...</code>), not by tag.</li>
  <li><strong>Verification must include identity constraints</strong>, not only “is signed?”.</li>
  <li><strong>Plan where your trust roots live</strong>: developer keyless identity, CI workload identity, or enterprise PKI (Notation) are different trust models.</li>
  <li><strong>Think about artifact lifecycle</strong>: deleting an image doesn’t always automatically delete every related artifact in every registry implementation.</li>
  <li><strong>Referrers API vs tag schema</strong>: some environments may require fallback behavior; test your exact ACR configuration (including encryption features) if you’re standardising on Referrers API workflows.</li>
</ul>

<h2 id="suggested-good-default-pipeline-shape">Suggested “good default” pipeline shape</h2>

<p>If you’re starting from scratch, a practical baseline is:</p>

<p>1) Build image
2) Push image
3) Generate SBOM
4) Create provenance (if your build system supports it)
5) Sign image digest
6) Attest SBOM/provenance to image digest
7) Enforce verification at deploy time (admission policy)</p>

<p>If you want to go further, add:</p>

<ul>
  <li>environment-specific signatures (promotion gates)</li>
  <li><code class="language-plaintext highlighter-rouge">oras copy -r</code> promotion of artifact graphs</li>
  <li>key management hardening (HSM/KMS), timestamping, and policy-as-code</li>
</ul>]]></content><author><name>Glen Thomas</name></author><category term="Software Engineering" /><category term="Security" /><category term="Containers" /><category term="OCI" /><category term="Supply Chain" /><category term="Sigstore" /><category term="Cosign" /><category term="Notary" /><category term="Notation" /><category term="ACR" /><category term="SBOM" /><category term="Attestations" /><summary type="html"><![CDATA[Signing container images is one of the highest-leverage things you can do to improve your software supply chain. A signature gives you a cryptographic way to answer “who produced this image?” and “has it been tampered with?”. Attestations (for example: provenance and SBOMs) help you answer deeper questions like “how was it built?” and “what’s inside it?”.]]></summary></entry><entry><title type="html">Building a macOS System Monitor with Vibe Coding and GitHub Copilot</title><link href="https://blog.glen-thomas.com/software%20engineering/2025/12/06/building-a-macos-dystem-monitor-with-vibe-coding-and-github-copilot.html" rel="alternate" type="text/html" title="Building a macOS System Monitor with Vibe Coding and GitHub Copilot" /><published>2025-12-06T23:25:00+00:00</published><updated>2025-12-06T23:25:00+00:00</updated><id>https://blog.glen-thomas.com/software%20engineering/2025/12/06/building-a-macos-dystem-monitor-with-vibe-coding-and-github-copilot</id><content type="html" xml:base="https://blog.glen-thomas.com/software%20engineering/2025/12/06/building-a-macos-dystem-monitor-with-vibe-coding-and-github-copilot.html"><![CDATA[<p>Building software has fundamentally changed. Over the past two days, I built <a href="https://github.com/glenthomas/peep">Peep</a>, a native macOS system monitor application, using a development approach that would have seemed like science fiction just a few years ago. This post documents my experience combining “vibe coding” with GitHub Copilot Chat to create a production-ready Electron application with a Rust native backend.</p>

<h2 id="what-is-peep">What is Peep?</h2>

<p>Peep is a system monitoring application for macOS that displays real-time information about your computer’s performance. Think of it as a modern, visually appealing alternative to Activity Monitor. It features:</p>

<ul>
  <li><strong>Real-time CPU and memory monitoring</strong> with animated gauges</li>
  <li><strong>Network and disk I/O tracking</strong> with historical charts</li>
  <li><strong>Process management</strong> with tree view and thread filtering</li>
  <li><strong>Battery status</strong> with time remaining estimates</li>
  <li><strong>Machine information</strong> including model name and macOS version</li>
</ul>

<p>The tech stack combines Electron for the desktop shell, React and TypeScript for the UI, and Rust via Neon bindings for high-performance native system access.</p>

<p><a href="/assets/images/peep-dashboard.png" class="align-center"><img src="/assets/images/peep-dashboard.png" alt="Peep Dashboard" /></a>
<a href="/assets/images/peep-process-list.png" class="align-center"><img src="/assets/images/peep-process-list.png" alt="Process List" /></a></p>

<h2 id="vibe-coding-a-new-development-paradigm">Vibe Coding: A New Development Paradigm</h2>

<p>“Vibe coding” is an emerging term for a development style where you describe what you want in natural language and iterate rapidly with AI assistance. Rather than meticulously planning every implementation detail upfront, you:</p>

<ol>
  <li><strong>Describe the desired outcome</strong> - “Add battery information to the header”</li>
  <li><strong>Review the generated solution</strong> - AI provides working code</li>
  <li><strong>Refine through conversation</strong> - “The icon isn’t showing correctly for discharging state”</li>
  <li><strong>Iterate until satisfied</strong> - Continue the dialogue until it works perfectly</li>
</ol>

<p>This approach flips traditional development on its head. Instead of spending hours reading documentation and debugging, you spend time <em>describing</em> and <em>refining</em>. The AI handles the boilerplate, the API lookups, and often catches edge cases you hadn’t considered.</p>

<h2 id="the-development-journey">The Development Journey</h2>

<h3 id="starting-point-architecture-decisions">Starting Point: Architecture Decisions</h3>

<p>The project began with a clear vision: a system monitor that felt native to macOS while being built with web technologies. The architecture emerged through conversation:</p>

<p><strong>Me:</strong> “I want to build a system monitor for macOS using Electron”</p>

<p><strong>Copilot:</strong> Suggested the full stack; Electron for the shell, React for the UI, and importantly, recommended Rust with Neon bindings for the native module rather than pure Node.js. This was crucial for performance when polling system metrics every few seconds.</p>

<p>The initial scaffolding; webpack config, TypeScript setup, IPC handlers, Rust/Neon integration, was generated in minutes. Tasks that would typically take half a day of configuration wrestling were done before my coffee got cold.</p>

<p>Copilot opens a pull request with a thorough explanation of the work that has been done. At this point I can pull the branch to my machine and do some QA.</p>

<p>The initial version created by Copilot looked like so:</p>

<p><a href="/assets/images/peep-initial-version.png" class="align-center"><img src="/assets/images/peep-initial-version.png" alt="Process Initial Version" /></a></p>

<p>If I find any bugs or want to make some minor improvements I can add a comment to the pull request.</p>

<blockquote>
  <p>glenthomas commented</p>

  <p>@copilot the nodejs project is trying to import the Rust binary and is failing with error:</p>

  <p>Failed to load native module: Error: Cannot find module ‘../../native/index.node’</p>

  <p>Require stack:</p>

  <p>/Users/glen.thomas/git/peep/dist/main.js</p>

  <p>at Module._resolveFilename (node:internal/modules/cjs/loader:1390:15)</p>
</blockquote>

<p>Copilot add the eyes emoji (👀) to to my comment to acknowledge my request and gets straight to work in a new session. A new comment is added to the pull request to let me know when the next stage of the work has been completed.</p>

<blockquote>
  <p>Copilot AI commented</p>

  <p>Fixed in commit e1003c2. The issue was that the native module path was using a hardcoded relative require path that didn’t resolve correctly at runtime.</p>

  <p>Changes made:</p>

  <ol>
    <li>Updated src/main/main.ts to use path.join(__dirname, ‘..’, ‘native’, ‘index.node’) for proper path resolution</li>
    <li>Added @types/node and @types/electron as dev dependencies (required for TypeScript compilation)</li>
    <li>Updated build script to use npx cargo-cp-artifact to ensure the tool is available</li>
    <li>Enhanced README troubleshooting section</li>
  </ol>

  <p>The native module now loads correctly. From dist/main.js, it resolves to <project-root>/native/index.node. Verified with test that shows the module loads successfully and can retrieve system info.</project-root></p>
</blockquote>

<h3 id="feature-development-the-tree-view-example">Feature Development: The Tree View Example</h3>

<p>One of the more complex features was adding a process tree view that shows parent-child relationships between processes. Here’s how that conversation went:</p>

<p><strong>Me:</strong> “Could we have an option to view process parent-child relationships?”</p>

<p>Within moments, I had:</p>
<ul>
  <li>A <code class="language-plaintext highlighter-rouge">buildProcessTree()</code> function that constructs a hierarchy from the flat process list</li>
  <li>A <code class="language-plaintext highlighter-rouge">renderProcessTree()</code> function with proper indentation and expand/collapse</li>
  <li>Toggle buttons for switching between list and tree views</li>
  <li>State management for tracking expanded nodes</li>
</ul>

<p>The implementation used the <code class="language-plaintext highlighter-rouge">ppid</code> (parent process ID) field that was already being collected by the Rust module. Copilot recognised the data was available and used it appropriately—something that would have required me to trace through the codebase manually.</p>

<h3 id="debugging-the-battery-icon-bug">Debugging: The Battery Icon Bug</h3>

<p>Not everything worked perfectly on the first try, and that’s where the iterative nature of vibe coding shines. After adding battery status to the header, I noticed the icon wasn’t correct:</p>

<p><strong>Me:</strong> “The battery state icon is not showing the correct icon. When the battery state is ‘discharging’, it shows the charging icon.”</p>

<p>Copilot identified the issue immediately: the <code class="language-plaintext highlighter-rouge">getStateIcon()</code> function was using <code class="language-plaintext highlighter-rouge">includes()</code> to match states, so “discharging” was matching the “charging” case first. The fix was simple—use exact matching with <code class="language-plaintext highlighter-rouge">===</code> and <code class="language-plaintext highlighter-rouge">startsWith()</code> instead.</p>

<p>This kind of bug could easily waste 20 minutes of console.log debugging. Instead, describing the symptom led directly to the fix.</p>

<h3 id="the-ui-polish-phase">The UI Polish Phase</h3>

<p>As the core functionality stabilised, I shifted to UI refinements. This is where vibe coding particularly excels - making small adjustments is as easy as asking:</p>

<ul>
  <li>“The columns, view type and show threads controls are stacked vertically, can they be stacked horizontally?” → Segmented button groups</li>
  <li>“On the CPU monitor and Memory monitor there is a bit of a vertical gap” → SVG viewport adjustments</li>
  <li>“Can we decrease the size of the gauges a little? Maybe 20%” → Proportional scaling of all gauge dimensions</li>
  <li>“The battery info is not properly centered in the header” → CSS Grid with <code class="language-plaintext highlighter-rouge">1fr auto 1fr</code></li>
</ul>

<p>Each of these would typically involve researching CSS properties, testing various approaches, and iterating. With Copilot, I described the visual problem and received targeted solutions.</p>

<h3 id="native-integration-challenges">Native Integration Challenges</h3>

<p>Some features required deeper system integration. When I asked about displaying the machine model name, the response included:</p>

<ol>
  <li>Using <code class="language-plaintext highlighter-rouge">sysctl</code> to read <code class="language-plaintext highlighter-rouge">hw.model</code> (e.g., “MacBookPro18,1”)</li>
  <li>A mapping table translating hardware identifiers to friendly names</li>
  <li>Integration with the existing <code class="language-plaintext highlighter-rouge">get_os_info()</code> Rust function</li>
</ol>

<p>Similarly, adding the macOS marketing name (Sonoma, Sequoia, etc.) required mapping Darwin version numbers to release names—knowledge that would require research but was immediately available through Copilot.</p>

<h3 id="when-ai-knowledge-falls-short">When AI Knowledge Falls Short</h3>

<p>It wasn’t all smooth sailing. One notable friction point came when working with the Rust <code class="language-plaintext highlighter-rouge">sysinfo</code> crate. Copilot confidently suggested methods and function calls that simply didn’t exist in the current version of the library.</p>

<p>For example, when implementing disk I/O monitoring per process, the AI suggested using methods that had either been renamed, deprecated, or never existed in the version I was using (0.37.x). The code would compile with errors like “method not found” or “no field named X on type Y”.</p>

<p>This is a fundamental limitation of AI coding assistants: their training data has a cutoff date, and libraries evolve. The <code class="language-plaintext highlighter-rouge">sysinfo</code> crate in particular has undergone significant API changes between versions, with methods being renamed and structs being reorganised.</p>

<p>The solution? I had to do things the old-fashioned way—pull up the <a href="https://docs.rs/sysinfo/latest/sysinfo/">sysinfo documentation on docs.rs</a>, find the correct method names for my version, and either correct the AI’s suggestions or implement the fix myself. In some cases, I’d share the compiler error and the relevant documentation snippet, and Copilot would adjust. In others, it was faster to just fix it manually.</p>

<p>This experience reinforced an important lesson: <strong>AI assistants are collaborators, not oracles</strong>. They’re incredibly useful for getting 80% of the way there quickly, but you still need domain knowledge and the ability to debug when their information is stale or incorrect.</p>

<h2 id="self-review-getting-copilot-to-improve-its-own-code">Self-Review: Getting Copilot to Improve Its Own Code</h2>

<p>One powerful technique that addresses concerns about AI-generated code quality is using Copilot to review and improve the very code it has produced. Critics might argue that AI assistants lack sufficient context while working incrementally, potentially leading to suboptimal solutions. However, this misses a crucial capability: <strong>asking the AI to review the solution as a whole</strong>.</p>

<p>After Copilot had implemented the core functionality, I took a step back and asked:</p>

<p><strong>Me:</strong> “Review the entire codebase and identify any issues, inefficiencies, or areas that could be improved.”</p>

<p>This prompted a comprehensive analysis that surfaced real problems I hadn’t noticed during rapid feature development:</p>

<p><strong>🔴 High Priority Issues</strong></p>

<ol>
  <li><strong>Duplicate <code class="language-plaintext highlighter-rouge">formatBytes</code> function</strong> - Defined in 4 separate files (<code class="language-plaintext highlighter-rouge">MemoryMonitor.tsx</code>, <code class="language-plaintext highlighter-rouge">DiskMonitor.tsx</code>, <code class="language-plaintext highlighter-rouge">NetworkMonitor.tsx</code>, <code class="language-plaintext highlighter-rouge">ProcessList.tsx</code>), each with slightly different implementations</li>
  <li><strong>Type safety issues in App.tsx</strong> - Using <code class="language-plaintext highlighter-rouge">any</code> types for state variables like <code class="language-plaintext highlighter-rouge">systemInfo</code>, <code class="language-plaintext highlighter-rouge">processes</code>, and <code class="language-plaintext highlighter-rouge">batteryInfo</code> instead of proper interfaces</li>
  <li><strong>Preload types don’t match actual API</strong> - The <code class="language-plaintext highlighter-rouge">ProcessInfo</code> interface was missing many fields the Rust module actually returns (<code class="language-plaintext highlighter-rouge">ppid</code>, <code class="language-plaintext highlighter-rouge">runTime</code>, <code class="language-plaintext highlighter-rouge">cpuTime</code>, <code class="language-plaintext highlighter-rouge">status</code>, <code class="language-plaintext highlighter-rouge">command</code>, <code class="language-plaintext highlighter-rouge">diskRead</code>, <code class="language-plaintext highlighter-rouge">diskWrite</code>, <code class="language-plaintext highlighter-rouge">isThread</code>)</li>
</ol>

<p><strong>🟡 Medium Priority Issues</strong></p>

<ol>
  <li><strong>Native module type is <code class="language-plaintext highlighter-rouge">any</code></strong> - No TypeScript declarations for the Rust bindings</li>
  <li><strong>Duplicate <code class="language-plaintext highlighter-rouge">formatStorage</code> function</strong> - Defined in both <code class="language-plaintext highlighter-rouge">DiskMonitor.tsx</code> and <code class="language-plaintext highlighter-rouge">NetworkMonitor.tsx</code></li>
  <li><strong>ChartJS registered multiple times</strong> - Each monitor component was independently registering ChartJS components, should be done once at app level</li>
  <li><strong>Large inline styles in App.tsx header</strong> - The battery status section (188-275 lines) had extensive inline styles that could be extracted to CSS</li>
  <li><strong>ProcessList.tsx is 664 lines</strong> - Very large component that could be split into <code class="language-plaintext highlighter-rouge">ProcessTable.tsx</code>, <code class="language-plaintext highlighter-rouge">ProcessControls.tsx</code>, and <code class="language-plaintext highlighter-rouge">ConfirmDialog.tsx</code></li>
</ol>

<p><strong>🟢 Low Priority</strong></p>

<ol>
  <li><strong>CSS variables defined but not fully used</strong> - Variables like <code class="language-plaintext highlighter-rouge">--spacing-xs</code> and <code class="language-plaintext highlighter-rouge">--font-sm</code> were defined but many inline styles used hardcoded values</li>
  <li><strong>Missing React.memo on monitor components</strong> - Could prevent unnecessary re-renders on gauge/chart components</li>
  <li><strong>Console.error for non-critical failures</strong> - Should use proper logging utility or remove in production</li>
</ol>

<p>For each issue, Copilot provided context and suggested fixes. The conversation flowed naturally:</p>

<p><strong>Me:</strong> “Fix the duplicate formatBytes functions”</p>

<p><strong>Copilot:</strong> [Creates <code class="language-plaintext highlighter-rouge">src/utils/formatters.ts</code> with a single, comprehensive implementation and updates all 4 files to import it]</p>

<p><strong>Me:</strong> “Add proper TypeScript types for the system info state”</p>

<p><strong>Copilot:</strong> [Creates interface definitions matching the Rust module’s output and updates all <code class="language-plaintext highlighter-rouge">any</code> types to use them]</p>

<p>This self-review pattern is remarkably effective because:</p>

<ol>
  <li><strong>Full context awareness</strong> - During review, the AI can see all the pieces together, not just the immediate task</li>
  <li><strong>Pattern recognition</strong> - It can spot anti-patterns across the codebase that emerge from incremental development</li>
  <li><strong>Best practices application</strong> - It can suggest idiomatic improvements that might have been missed during rapid feature development</li>
  <li><strong>Consistency checking</strong> - It identifies places where similar functionality is handled differently</li>
</ol>

<p>The key is being explicit about the review scope. Rather than asking a vague “make it better,” specific prompts work best:</p>

<ul>
  <li>“Review the React components for performance issues”</li>
  <li>“Check the Rust module for error handling problems”</li>
  <li>“Analyze the IPC communication for potential race conditions”</li>
  <li>“Review accessibility and ensure proper ARIA labels”</li>
</ul>

<p>This approach transforms a potential weakness into a strength. Yes, AI might produce less-than-perfect code when working with limited context on individual features. But it excels at identifying those imperfections when given the full picture and asked to review holistically.</p>

<h2 id="setting-up-the-release-pipeline">Setting Up the Release Pipeline</h2>

<p>One area where AI assistance proved invaluable was configuring the build and release pipeline. CI/CD configuration is notoriously fiddly, with countless options and edge cases.</p>

<p><strong>Me:</strong> “I would like to allow people finding this GitHub repository to be able to download and install the application. What options do I have?”</p>

<p>Copilot outlined several distribution strategies (GitHub Releases, Homebrew, Mac App Store) and recommended GitHub Releases with electron-builder for an open-source project. It then generated:</p>

<ul>
  <li>A <code class="language-plaintext highlighter-rouge">build.yml</code> workflow for CI on every push</li>
  <li>A <code class="language-plaintext highlighter-rouge">release.yml</code> workflow triggered by version tags</li>
  <li>Electron-builder configuration for both Intel and Apple Silicon</li>
  <li>README updates with download instructions</li>
</ul>

<p>When the workflows hit issues; wrong action names, permission errors, DMG creation failures on CI runners, each problem was solved through conversation:</p>

<ul>
  <li>“The action <code class="language-plaintext highlighter-rouge">dtolnay/rust-action@stable</code> does not exist” → Switched to <code class="language-plaintext highlighter-rouge">actions-rust-lang/setup-rust-toolchain@v1</code></li>
  <li>“Package ‘electron’ is only allowed in devDependencies” → Moved the dependency</li>
  <li>“hdiutil detach failing on CI” → Added retry logic for flaky DMG creation</li>
  <li>“403 Forbidden when creating release” → Added <code class="language-plaintext highlighter-rouge">permissions: contents: write</code></li>
</ul>

<h2 id="lessons-learned">Lessons Learned</h2>

<h3 id="when-vibe-coding-excels">When Vibe Coding Excels</h3>

<ol>
  <li><strong>Boilerplate and configuration</strong> - Webpack configs, GitHub Actions, package.json scripts</li>
  <li><strong>API integration</strong> - Knowing which Electron or React APIs to use</li>
  <li><strong>Cross-referencing</strong> - Using data from one part of the app in another</li>
  <li><strong>Bug diagnosis</strong> - Describing symptoms and getting targeted fixes</li>
  <li><strong>Refactoring</strong> - “Can you change this from flex to grid layout?”</li>
</ol>

<h3 id="when-to-slow-down">When to Slow Down</h3>

<ol>
  <li><strong>Architecture decisions</strong> - Still worth thinking through yourself</li>
  <li><strong>Security-sensitive code</strong> - Review carefully, don’t blindly trust</li>
  <li><strong>Complex algorithms</strong> - Understand what’s generated, don’t just accept</li>
  <li><strong>Performance-critical paths</strong> - Verify the approach makes sense</li>
</ol>

<h3 id="the-collaboration-dynamic">The Collaboration Dynamic</h3>

<p>The most effective pattern I found was treating Copilot as a highly knowledgeable pair programmer who happens to type very fast. I would:</p>

<ol>
  <li><strong>Set context</strong> - Explain what exists and what I’m trying to achieve</li>
  <li><strong>Ask for options</strong> - “What are the ways to do X?”</li>
  <li><strong>Make decisions</strong> - Choose the approach that fits best</li>
  <li><strong>Request implementation</strong> - “Let’s go with option 2”</li>
  <li><strong>Iterate on details</strong> - “Can we also handle the edge case where…”</li>
</ol>

<p>This maintains human agency over the architecture and design while leveraging AI for implementation speed.</p>

<h2 id="the-results">The Results</h2>

<p>Peep went from concept to released application in a remarkably short time. The final product includes:</p>

<ul>
  <li><strong>5 monitoring components</strong> (CPU, Memory, Disk, Network, Processes)</li>
  <li><strong>Native Rust module</strong> with 15+ exported functions</li>
  <li><strong>Automated CI/CD</strong> with multi-architecture builds</li>
  <li><strong>GitHub Releases</strong> with DMG and ZIP downloads</li>
  <li><strong>Comprehensive documentation</strong> (README, CONTRIBUTING, CHANGELOG)</li>
</ul>

<p>More importantly, the codebase is clean and maintainable. The AI-generated code follows consistent patterns, includes appropriate comments, and handles edge cases I might have missed.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Building Peep reinforced my belief that we’re at an inflection point in software development. The combination of vibe coding (describing intent in natural language) with tools like GitHub Copilot Chat doesn’t replace programming skill; it amplifies it.</p>

<p>You still need to understand what you’re building, make architectural decisions, and verify the results. But the mechanical translation of ideas into code? That’s increasingly handled by AI, freeing developers to focus on the creative and strategic aspects of software creation.</p>

<p>If you haven’t tried this approach, I encourage you to experiment. Start with a side project, describe what you want, and see where the conversation takes you. You might be surprised how quickly your ideas become reality.</p>

<hr />

<p><em>Peep is open source and available on <a href="https://github.com/glenthomas/peep">GitHub</a>. Contributions welcome!</em></p>]]></content><author><name>Glen Thomas</name></author><category term="Software Engineering" /><category term="Electron" /><category term="Rust" /><category term="Typescript" /><category term="React" /><category term="AI" /><category term="GitHub Copilot" /><category term="Vibe Coding" /><summary type="html"><![CDATA[Building software has fundamentally changed. Over the past two days, I built Peep, a native macOS system monitor application, using a development approach that would have seemed like science fiction just a few years ago. This post documents my experience combining “vibe coding” with GitHub Copilot Chat to create a production-ready Electron application with a Rust native backend.]]></summary></entry><entry><title type="html">Mastering Docker Bake: Building Multi-Platform Images at Scale</title><link href="https://blog.glen-thomas.com/platform%20engineering/software%20engineering/2025/12/05/mastering-docker-bake-building-multi-platform-images-at-scale.html" rel="alternate" type="text/html" title="Mastering Docker Bake: Building Multi-Platform Images at Scale" /><published>2025-12-05T20:59:00+00:00</published><updated>2025-12-05T20:59:00+00:00</updated><id>https://blog.glen-thomas.com/platform%20engineering/software%20engineering/2025/12/05/mastering-docker-bake-building-multi-platform-images-at-scale</id><content type="html" xml:base="https://blog.glen-thomas.com/platform%20engineering/software%20engineering/2025/12/05/mastering-docker-bake-building-multi-platform-images-at-scale.html"><![CDATA[<p>Building Docker images has evolved far beyond simple <code class="language-plaintext highlighter-rouge">docker build</code> commands. Modern applications demand multi-platform support to run on both x86 and ARM architectures, multiple base images to support different runtime environments, and sophisticated tagging strategies to manage versions across development, staging, and production. Managing this complexity with shell scripts quickly becomes unwieldy, error-prone, and difficult to maintain.</p>

<p>Docker Bake solves these challenges with a declarative approach to building container images. It’s a high-level build orchestration tool that lets you define complex build configurations in a single file, then execute them consistently across local development and CI/CD pipelines. Instead of maintaining dozens of <code class="language-plaintext highlighter-rouge">docker build</code> commands with slightly different flags, you define your entire build matrix once and let Bake handle the orchestration.</p>

<p>In this comprehensive guide, I’ll show you how to use Docker Bake to build production-ready container images. We’ll start with the fundamentals, progress through real-world examples of multi-platform and multi-tag builds, and finish with GitHub Actions workflows that demonstrate how to integrate Bake into your CI/CD pipeline. By the end, you’ll have the knowledge to replace your complex build scripts with maintainable, declarative configuration.</p>

<h2 id="understanding-docker-bake">Understanding Docker Bake</h2>

<p>Before diving into examples, it’s worth understanding what Docker Bake actually does and why it exists. Docker Bake is part of Docker Buildx, the extended build capabilities that replaced the classic Docker builder. Whilst <code class="language-plaintext highlighter-rouge">docker buildx build</code> handles building a single image, <code class="language-plaintext highlighter-rouge">docker bake</code> orchestrates building multiple related images in one operation.</p>

<h3 id="why-docker-bake-exists">Why Docker Bake Exists</h3>

<p>Traditional Docker builds work well for simple use cases; a single Dockerfile producing a single image. But real world applications often need more complexity. You might need to build the same application for both AMD64 and ARM64 platforms. You might maintain separate images with different base operating systems. You might need to tag the same build with multiple version identifiers.</p>

<p>Without Bake, you’d write shell scripts that loop through platforms, base images, and tags, invoking <code class="language-plaintext highlighter-rouge">docker build</code> repeatedly with different arguments. These scripts become difficult to maintain, hard to test, and prone to subtle bugs where one combination of flags differs slightly from another.</p>

<p>Bake replaces these scripts with declarative configuration. You define what you want to build, not how to build it. The configuration format supports variables, inheritance, and composition, making it easier to maintain consistency across dozens of build targets.</p>

<h3 id="bake-configuration-format">Bake Configuration Format</h3>

<p>Docker Bake supports three configuration formats: HCL (HashiCorp Configuration Language), JSON, and Docker Compose files. HCL is the most expressive and the format I recommend. It supports variables, functions, and a clean syntax that reads naturally.</p>

<p>A Bake file consists of targets, groups, and variables. Targets define individual build operations; which Dockerfile to use, what tags to apply, which platforms to build for. Groups collect multiple targets so you can build them together. Variables parameterise your configuration, allowing the same file to work across different environments.</p>

<h3 id="how-bake-differs-from-regular-builds">How Bake Differs from Regular Builds</h3>

<p>When you run <code class="language-plaintext highlighter-rouge">docker buildx build</code>, you’re building a single image. When you run <code class="language-plaintext highlighter-rouge">docker buildx bake</code>, you can build dozens of images in parallel, each with different configurations, all defined in your Bake file. Bake also provides better defaults for multi-platform builds, automatically configuring builders and handling the complexity of cross-compilation.</p>

<p>Another key difference is composability. Bake files can reference other Bake files, allowing you to split configuration across multiple files and compose them at build time. This makes it possible to maintain shared configuration whilst allowing project-specific overrides.</p>

<h2 id="basic-docker-bake-configuration">Basic Docker Bake Configuration</h2>

<p>Let’s start with a simple example and build up complexity progressively. Create a file called <code class="language-plaintext highlighter-rouge">docker-bake.hcl</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"TAG"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"latest"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"docker.io/myorg"</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"default"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:${TAG}"</span><span class="p">]</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This configuration defines a single target called <code class="language-plaintext highlighter-rouge">app</code> that builds your Dockerfile and tags it with a registry prefix and version tag. The <code class="language-plaintext highlighter-rouge">default</code> group contains this target, so running <code class="language-plaintext highlighter-rouge">docker buildx bake</code> without arguments builds it.</p>

<p>Variables make this configuration flexible. Override them at runtime without modifying the file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker buildx bake <span class="nt">--set</span> <span class="s2">"*.TAG=1.0.0"</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">*</code> pattern applies the variable to all targets. You can also set variables through environment variables or by creating a <code class="language-plaintext highlighter-rouge">docker-bake.override.hcl</code> file that gets automatically merged with the main configuration.</p>

<h3 id="adding-context-and-arguments">Adding Context and Arguments</h3>

<p>Real applications need build arguments for things like version numbers, build timestamps, or configuration values. Add them to your target:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">target</span> <span class="s2">"app"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:${TAG}"</span><span class="p">]</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">VERSION</span>    <span class="o">=</span> <span class="s2">"${TAG}"</span>
    <span class="nx">BUILD_DATE</span> <span class="o">=</span> <span class="nx">timestamp</span><span class="p">()</span>
    <span class="nx">VCS_REF</span>    <span class="o">=</span> <span class="s2">"git-sha"</span>
  <span class="p">}</span>
  
  <span class="nx">labels</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"org.opencontainers.image.version"</span>    <span class="p">=</span> <span class="s2">"${TAG}"</span>
    <span class="s2">"org.opencontainers.image.created"</span>    <span class="p">=</span> <span class="nx">timestamp</span><span class="p">()</span>
    <span class="s2">"org.opencontainers.image.source"</span>     <span class="p">=</span> <span class="s2">"https://github.com/myorg/myapp"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">args</code> block passes build arguments to your Dockerfile, accessible via <code class="language-plaintext highlighter-rouge">ARG</code> instructions. The <code class="language-plaintext highlighter-rouge">labels</code> block adds OCI-compliant metadata to your image. The <code class="language-plaintext highlighter-rouge">timestamp()</code> function generates the current timestamp, ensuring each build has accurate creation metadata.</p>

<h2 id="multi-platform-builds">Multi-Platform Builds</h2>

<p>Multi-platform builds are where Docker Bake really shines. Building for multiple architectures with traditional Docker builds requires multiple commands, managing different builders, and complex scripting. Bake handles all of this automatically.</p>

<h3 id="configuring-platform-support">Configuring Platform Support</h3>

<p>Update your target to build for both AMD64 and ARM64:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"TAG"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"latest"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"docker.io/myorg"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"PLATFORMS"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">,</span> <span class="s2">"linux/arm64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:${TAG}"</span><span class="p">]</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="nx">PLATFORMS</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">BUILDPLATFORM</span> <span class="o">=</span> <span class="s2">""</span>
    <span class="nx">TARGETPLATFORM</span> <span class="o">=</span> <span class="s2">""</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When you run <code class="language-plaintext highlighter-rouge">docker buildx bake</code>, Bake builds your image for both platforms in parallel, creating a multi-architecture manifest that automatically serves the correct image based on the host architecture.</p>

<p>The <code class="language-plaintext highlighter-rouge">BUILDPLATFORM</code> and <code class="language-plaintext highlighter-rouge">TARGETPLATFORM</code> arguments are automatically available in your Dockerfile when building multi-platform images. They let you customise build behaviour based on the target architecture:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="w"> </span><span class="s">--platform=$BUILDPLATFORM golang:1.21</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>

<span class="k">ARG</span><span class="s"> TARGETPLATFORM</span>
<span class="k">ARG</span><span class="s"> BUILDPLATFORM</span>

<span class="k">RUN </span><span class="nb">echo</span> <span class="s2">"Building on </span><span class="nv">$BUILDPLATFORM</span><span class="s2"> for </span><span class="nv">$TARGETPLATFORM</span><span class="s2">"</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> . .</span>

<span class="c"># Use TARGETPLATFORM to set GOARCH appropriately</span>
<span class="k">RUN case</span> <span class="s2">"</span><span class="nv">$TARGETPLATFORM</span><span class="s2">"</span> <span class="k">in</span> <span class="se">\
</span>      <span class="s2">"linux/amd64"</span><span class="p">)</span> <span class="nv">GOARCH</span><span class="o">=</span>amd64 <span class="p">;;</span> <span class="se">\
</span>      <span class="s2">"linux/arm64"</span><span class="p">)</span> <span class="nv">GOARCH</span><span class="o">=</span>arm64 <span class="p">;;</span> <span class="se">\
</span>      <span class="s2">"linux/arm/v7"</span><span class="p">)</span> <span class="nv">GOARCH</span><span class="o">=</span>arm <span class="nv">GOARM</span><span class="o">=</span>7 <span class="p">;;</span> <span class="se">\
</span>    <span class="k">esac</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span>    <span class="nv">GOOS</span><span class="o">=</span>linux go build <span class="nt">-o</span> myapp

<span class="k">FROM</span><span class="s"> alpine:3.19</span>
<span class="k">COPY</span><span class="s"> --from=builder /app/myapp /usr/local/bin/</span>
<span class="k">ENTRYPOINT</span><span class="s"> ["/usr/local/bin/myapp"]</span>
</code></pre></div></div>

<h3 id="platform-specific-optimisations">Platform-Specific Optimisations</h3>

<p>Sometimes you need platform-specific behaviour. For ARM builds, you might want to use a different base image or enable specific compiler optimisations. Create separate targets for each platform:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"TAG"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"latest"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"docker.io/myorg"</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-amd64"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"app-common"</span><span class="p">]</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:${TAG}-amd64"</span><span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">BASE_IMAGE</span> <span class="o">=</span> <span class="s2">"alpine:3.19"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-arm64"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"app-common"</span><span class="p">]</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/arm64"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:${TAG}-arm64"</span><span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">BASE_IMAGE</span> <span class="o">=</span> <span class="s2">"arm64v8/alpine:3.19"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-common"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">VERSION</span> <span class="o">=</span> <span class="s2">"${TAG}"</span>
  <span class="p">}</span>
  
  <span class="nx">labels</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"org.opencontainers.image.version"</span> <span class="p">=</span> <span class="s2">"${TAG}"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"default"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-amd64"</span><span class="p">,</span> <span class="s2">"app-arm64"</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This configuration uses inheritance through the <code class="language-plaintext highlighter-rouge">inherits</code> field. The <code class="language-plaintext highlighter-rouge">app-common</code> target defines shared configuration, whilst platform-specific targets override only what differs. Building the default group builds both platforms with their specific optimisations.</p>

<h2 id="multi-tag-strategies">Multi-Tag Strategies</h2>

<p>Production container workflows often need multiple tags pointing to the same image. You might tag a release as <code class="language-plaintext highlighter-rouge">1.2.3</code>, <code class="language-plaintext highlighter-rouge">1.2</code>, <code class="language-plaintext highlighter-rouge">1</code>, and <code class="language-plaintext highlighter-rouge">latest</code> simultaneously. You might also tag builds with git commit SHAs for traceability. Bake makes this straightforward.</p>

<h3 id="semantic-versioning-tags">Semantic Versioning Tags</h3>

<p>Here’s a configuration that tags images with multiple semantic version levels:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"VERSION"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"1.2.3"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"docker.io/myorg"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"COMMIT_SHA"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"unknown"</span>
<span class="p">}</span>

<span class="nx">function</span> <span class="s2">"semver_tags"</span> <span class="p">{</span>
  <span class="nx">params</span> <span class="o">=</span> <span class="p">[</span><span class="nx">version</span><span class="p">]</span>
  <span class="nx">result</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${REGISTRY}/myapp:${version}"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:${regex("</span><span class="err">^</span><span class="p">[</span><span class="mi">0</span><span class="o">-</span><span class="mi">9</span><span class="p">]</span><span class="o">+</span><span class="p">\\.[</span><span class="mi">0</span><span class="o">-</span><span class="mi">9</span><span class="p">]</span><span class="o">+</span><span class="s2">", version)}"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:${regex("</span><span class="err">^</span><span class="p">[</span><span class="mi">0</span><span class="o">-</span><span class="mi">9</span><span class="p">]</span><span class="o">+</span><span class="s2">", version)}"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:latest"</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">,</span> <span class="s2">"linux/arm64"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="nx">semver_tags</span><span class="p">(</span><span class="nx">VERSION</span><span class="p">)</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">VERSION</span>    <span class="o">=</span> <span class="nx">VERSION</span>
    <span class="nx">COMMIT_SHA</span> <span class="o">=</span> <span class="nx">COMMIT_SHA</span>
  <span class="p">}</span>
  
  <span class="nx">labels</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"org.opencontainers.image.version"</span> <span class="p">=</span> <span class="nx">VERSION</span>
    <span class="s2">"org.opencontainers.image.revision"</span> <span class="o">=</span> <span class="nx">COMMIT_SHA</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">semver_tags</code> function takes a version like <code class="language-plaintext highlighter-rouge">1.2.3</code> and generates four tags: <code class="language-plaintext highlighter-rouge">1.2.3</code>, <code class="language-plaintext highlighter-rouge">1.2</code>, <code class="language-plaintext highlighter-rouge">1</code>, and <code class="language-plaintext highlighter-rouge">latest</code>. This ensures users can pin to specific versions or track major/minor releases.</p>

<h3 id="environment-specific-tags">Environment-Specific Tags</h3>

<p>Different environments often need different tagging strategies. Development builds might use branch names and commit SHAs, whilst production uses semantic versions:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"ENVIRONMENT"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"dev"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"VERSION"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"dev"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"BRANCH"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"main"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"COMMIT_SHA"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"unknown"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"docker.io/myorg"</span>
<span class="p">}</span>

<span class="nx">function</span> <span class="s2">"dev_tags"</span> <span class="p">{</span>
  <span class="nx">params</span> <span class="o">=</span> <span class="p">[</span><span class="nx">branch</span><span class="p">,</span> <span class="nx">sha</span><span class="p">]</span>
  <span class="nx">result</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${REGISTRY}/myapp:${branch}"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:${branch}-${sha}"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:dev"</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="nx">function</span> <span class="s2">"prod_tags"</span> <span class="p">{</span>
  <span class="nx">params</span> <span class="o">=</span> <span class="p">[</span><span class="nx">version</span><span class="p">]</span>
  <span class="nx">result</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${REGISTRY}/myapp:${version}"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:${regex("</span><span class="err">^</span><span class="p">[</span><span class="mi">0</span><span class="o">-</span><span class="mi">9</span><span class="p">]</span><span class="o">+</span><span class="p">\\.[</span><span class="mi">0</span><span class="o">-</span><span class="mi">9</span><span class="p">]</span><span class="o">+</span><span class="s2">", version)}"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:${regex("</span><span class="err">^</span><span class="p">[</span><span class="mi">0</span><span class="o">-</span><span class="mi">9</span><span class="p">]</span><span class="o">+</span><span class="s2">", version)}"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:latest"</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-dev"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="nx">dev_tags</span><span class="p">(</span><span class="nx">BRANCH</span><span class="p">,</span> <span class="nx">COMMIT_SHA</span><span class="p">)</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-prod"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">,</span> <span class="s2">"linux/arm64"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="nx">prod_tags</span><span class="p">(</span><span class="nx">VERSION</span><span class="p">)</span>
  
  <span class="nx">output</span>     <span class="o">=</span> <span class="p">[</span><span class="s2">"type=registry,push=true"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"dev"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-dev"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"prod"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-prod"</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Build development images with <code class="language-plaintext highlighter-rouge">docker buildx bake dev</code> and production images with <code class="language-plaintext highlighter-rouge">docker buildx bake prod</code>. Each group uses appropriate tags and platforms for its environment.</p>

<h2 id="multiple-base-images">Multiple Base Images</h2>

<p>Some applications need variants built from different base images. You might offer an Alpine-based image for minimal size and a Debian-based image for better compatibility. Or you might support multiple runtime versions—Node.js 18 and Node.js 20, for example.</p>

<h3 id="base-image-variants">Base Image Variants</h3>

<p>Here’s a configuration that builds the same application with multiple base images:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"VERSION"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"latest"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"docker.io/myorg"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"PLATFORMS"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">,</span> <span class="s2">"linux/arm64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-alpine"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile.alpine"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="nx">PLATFORMS</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${REGISTRY}/myapp:${VERSION}-alpine"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:alpine"</span>
  <span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">BASE_IMAGE</span> <span class="o">=</span> <span class="s2">"alpine:3.19"</span>
    <span class="nx">VERSION</span>    <span class="o">=</span> <span class="nx">VERSION</span>
  <span class="p">}</span>
  
  <span class="nx">labels</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"org.opencontainers.image.version"</span> <span class="p">=</span> <span class="nx">VERSION</span>
    <span class="s2">"org.opencontainers.image.base.name"</span> <span class="o">=</span> <span class="s2">"alpine:3.19"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-debian"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile.debian"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="nx">PLATFORMS</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${REGISTRY}/myapp:${VERSION}-debian"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:${VERSION}"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:latest"</span>
  <span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">BASE_IMAGE</span> <span class="o">=</span> <span class="s2">"debian:bookworm-slim"</span>
    <span class="nx">VERSION</span>    <span class="o">=</span> <span class="nx">VERSION</span>
  <span class="p">}</span>
  
  <span class="nx">labels</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"org.opencontainers.image.version"</span> <span class="p">=</span> <span class="nx">VERSION</span>
    <span class="s2">"org.opencontainers.image.base.name"</span> <span class="o">=</span> <span class="s2">"debian:bookworm-slim"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-ubuntu"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile.ubuntu"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="nx">PLATFORMS</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${REGISTRY}/myapp:${VERSION}-ubuntu"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:ubuntu"</span>
  <span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">BASE_IMAGE</span> <span class="o">=</span> <span class="s2">"ubuntu:22.04"</span>
    <span class="nx">VERSION</span>    <span class="o">=</span> <span class="nx">VERSION</span>
  <span class="p">}</span>
  
  <span class="nx">labels</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"org.opencontainers.image.version"</span> <span class="p">=</span> <span class="nx">VERSION</span>
    <span class="s2">"org.opencontainers.image.base.name"</span> <span class="o">=</span> <span class="s2">"ubuntu:22.04"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"default"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-alpine"</span><span class="p">,</span> <span class="s2">"app-debian"</span><span class="p">,</span> <span class="s2">"app-ubuntu"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"alpine"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-alpine"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"debian"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-debian"</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Running <code class="language-plaintext highlighter-rouge">docker buildx bake</code> builds all three variants. Running <code class="language-plaintext highlighter-rouge">docker buildx bake alpine</code> builds only the Alpine variant. Each variant has appropriate tags indicating its base image.</p>

<h3 id="single-dockerfile-with-multiple-bases">Single Dockerfile with Multiple Bases</h3>

<p>If your Dockerfiles differ only in the base image, you can use a single Dockerfile with build arguments:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"VERSION"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"latest"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"docker.io/myorg"</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-alpine"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"app-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:${VERSION}-alpine"</span><span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">BASE_IMAGE</span> <span class="o">=</span> <span class="s2">"alpine:3.19"</span>
    <span class="nx">IMAGE_VARIANT</span> <span class="o">=</span> <span class="s2">"alpine"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-debian"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"app-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:${VERSION}-debian"</span><span class="p">,</span> <span class="s2">"${REGISTRY}/myapp:${VERSION}"</span><span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">BASE_IMAGE</span> <span class="o">=</span> <span class="s2">"debian:bookworm-slim"</span>
    <span class="nx">IMAGE_VARIANT</span> <span class="o">=</span> <span class="s2">"debian"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-common"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">,</span> <span class="s2">"linux/arm64"</span><span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">VERSION</span> <span class="o">=</span> <span class="nx">VERSION</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"default"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-alpine"</span><span class="p">,</span> <span class="s2">"app-debian"</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Your Dockerfile uses the <code class="language-plaintext highlighter-rouge">BASE_IMAGE</code> argument:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> BASE_IMAGE=alpine:3.19</span>
<span class="k">FROM</span><span class="s"> ${BASE_IMAGE}</span>

<span class="k">ARG</span><span class="s"> VERSION</span>
<span class="k">ARG</span><span class="s"> IMAGE_VARIANT</span>

<span class="k">LABEL</span><span class="s"> org.opencontainers.image.version="${VERSION}"</span>
<span class="k">LABEL</span><span class="s"> org.opencontainers.image.variant="${IMAGE_VARIANT}"</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> . .</span>

<span class="k">RUN if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">IMAGE_VARIANT</span><span class="k">}</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"alpine"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> <span class="se">\
</span>      apk add <span class="nt">--no-cache</span> ca-certificates<span class="p">;</span> <span class="se">\
</span>    <span class="k">else</span> <span class="se">\
</span>      apt-get update <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> ca-certificates <span class="o">&amp;&amp;</span> <span class="nb">rm</span> <span class="nt">-rf</span> /var/lib/apt/lists/<span class="k">*</span><span class="p">;</span> <span class="se">\
</span>    <span class="k">fi</span>

<span class="k">ENTRYPOINT</span><span class="s"> ["/app/myapp"]</span>
</code></pre></div></div>

<p>This approach reduces duplication when the only difference between variants is the base image and package manager.</p>

<h2 id="runtime-version-matrix">Runtime Version Matrix</h2>

<p>Applications often need to support multiple runtime versions. A Python application might support Python 3.10, 3.11, and 3.12. A Node.js application might support Node 18, 20, and 21. Bake makes it easy to build a complete matrix.</p>

<h3 id="python-version-matrix-example">Python Version Matrix Example</h3>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"VERSION"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"latest"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"docker.io/myorg"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"PLATFORMS"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">,</span> <span class="s2">"linux/arm64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"PYTHON_VERSIONS"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"3.10"</span><span class="p">,</span> <span class="s2">"3.11"</span><span class="p">,</span> <span class="s2">"3.12"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-python-310"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"app-python-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${REGISTRY}/myapp:${VERSION}-python3.10"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:python3.10"</span>
  <span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">PYTHON_VERSION</span> <span class="o">=</span> <span class="s2">"3.10"</span>
    <span class="nx">PYTHON_IMAGE</span> <span class="o">=</span> <span class="s2">"python:3.10-slim"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-python-311"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"app-python-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${REGISTRY}/myapp:${VERSION}-python3.11"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:python3.11"</span>
  <span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">PYTHON_VERSION</span> <span class="o">=</span> <span class="s2">"3.11"</span>
    <span class="nx">PYTHON_IMAGE</span> <span class="o">=</span> <span class="s2">"python:3.11-slim"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-python-312"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"app-python-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${REGISTRY}/myapp:${VERSION}-python3.12"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:${VERSION}"</span><span class="p">,</span>
    <span class="s2">"${REGISTRY}/myapp:latest"</span>
  <span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">PYTHON_VERSION</span> <span class="o">=</span> <span class="s2">"3.12"</span>
    <span class="nx">PYTHON_IMAGE</span> <span class="o">=</span> <span class="s2">"python:3.12-slim"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-python-common"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="nx">PLATFORMS</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">VERSION</span> <span class="o">=</span> <span class="nx">VERSION</span>
  <span class="p">}</span>
  
  <span class="nx">labels</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"org.opencontainers.image.version"</span> <span class="p">=</span> <span class="nx">VERSION</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"default"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-python-310"</span><span class="p">,</span> <span class="s2">"app-python-311"</span><span class="p">,</span> <span class="s2">"app-python-312"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"python-latest"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-python-312"</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The corresponding Dockerfile:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> PYTHON_IMAGE=python:3.12-slim</span>
<span class="k">FROM</span><span class="s"> ${PYTHON_IMAGE}</span>

<span class="k">ARG</span><span class="s"> VERSION</span>
<span class="k">ARG</span><span class="s"> PYTHON_VERSION</span>

<span class="k">LABEL</span><span class="s"> org.opencontainers.image.version="${VERSION}"</span>
<span class="k">LABEL</span><span class="s"> org.opencontainers.image.python.version="${PYTHON_VERSION}"</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="k">COPY</span><span class="s"> requirements.txt .</span>
<span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--no-cache-dir</span> <span class="nt">-r</span> requirements.txt

<span class="k">COPY</span><span class="s"> . .</span>

<span class="k">CMD</span><span class="s"> ["python", "app.py"]</span>
</code></pre></div></div>

<h2 id="advanced-bake-patterns">Advanced Bake Patterns</h2>

<p>Once you’re comfortable with basic Bake configurations, several advanced patterns can make your builds even more powerful.</p>

<h3 id="matrix-builds-with-dynamic-targets">Matrix Builds with Dynamic Targets</h3>

<p>For large build matrices, manually defining every combination becomes tedious. Whilst Bake doesn’t support true matrix generation in HCL, you can use environment variables and multiple Bake files to achieve similar results.</p>

<p>Create a base configuration <code class="language-plaintext highlighter-rouge">docker-bake.hcl</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"VERSION"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"latest"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"docker.io/myorg"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"PYTHON_VERSION"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"3.12"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"BASE_VARIANT"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"slim"</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">,</span> <span class="s2">"linux/arm64"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${REGISTRY}/myapp:${VERSION}-python${PYTHON_VERSION}-${BASE_VARIANT}"</span>
  <span class="p">]</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">PYTHON_IMAGE</span> <span class="o">=</span> <span class="s2">"python:${PYTHON_VERSION}-${BASE_VARIANT}"</span>
    <span class="nx">VERSION</span>      <span class="o">=</span> <span class="nx">VERSION</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then use a shell script to invoke Bake multiple times with different variables:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">set</span> <span class="nt">-e</span>

<span class="nv">VERSIONS</span><span class="o">=(</span><span class="s2">"3.10"</span> <span class="s2">"3.11"</span> <span class="s2">"3.12"</span><span class="o">)</span>
<span class="nv">VARIANTS</span><span class="o">=(</span><span class="s2">"slim"</span> <span class="s2">"alpine"</span><span class="o">)</span>

<span class="k">for </span>version <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">VERSIONS</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
  for </span>variant <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">VARIANTS</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
    </span><span class="nb">echo</span> <span class="s2">"Building Python </span><span class="k">${</span><span class="nv">version</span><span class="k">}</span><span class="s2"> </span><span class="k">${</span><span class="nv">variant</span><span class="k">}</span><span class="s2">"</span>
    docker buildx bake <span class="se">\</span>
      <span class="nt">--set</span> <span class="s2">"*.PYTHON_VERSION=</span><span class="k">${</span><span class="nv">version</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
      <span class="nt">--set</span> <span class="s2">"*.BASE_VARIANT=</span><span class="k">${</span><span class="nv">variant</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
      app
  <span class="k">done
done</span>
</code></pre></div></div>

<h3 id="conditional-building">Conditional Building</h3>

<p>Sometimes you want to build certain targets only in specific conditions. Whilst Bake doesn’t support conditionals directly, you can use groups and selective building:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"BUILD_ARCH"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"all"</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-amd64"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"app-common"</span><span class="p">]</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:${VERSION}-amd64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-arm64"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"app-common"</span><span class="p">]</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/arm64"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:${VERSION}-arm64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-common"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"all"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-amd64"</span><span class="p">,</span> <span class="s2">"app-arm64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"amd64"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-amd64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"arm64"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-arm64"</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Build only AMD64 with <code class="language-plaintext highlighter-rouge">docker buildx bake amd64</code>, only ARM64 with <code class="language-plaintext highlighter-rouge">docker buildx bake arm64</code>, or both with <code class="language-plaintext highlighter-rouge">docker buildx bake all</code>.</p>

<h3 id="build-secrets-and-ssh-forwarding">Build Secrets and SSH Forwarding</h3>

<p>Production builds often need access to private dependencies or repositories. Bake supports BuildKit secrets and SSH forwarding:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"VERSION"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"latest"</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">,</span> <span class="s2">"linux/arm64"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"myapp:${VERSION}"</span><span class="p">]</span>
  
  <span class="nx">secret</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"id=npm_token,env=NPM_TOKEN"</span><span class="p">,</span>
    <span class="s2">"id=github_token,env=GITHUB_TOKEN"</span>
  <span class="p">]</span>
  
  <span class="nx">ssh</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"default"</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In your Dockerfile, mount secrets:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> node:20-alpine</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="k">COPY</span><span class="s"> package*.json ./</span>

<span class="c"># Mount npm token as secret for private registry access</span>
<span class="k">RUN </span><span class="nt">--mount</span><span class="o">=</span><span class="nb">type</span><span class="o">=</span>secret,id<span class="o">=</span>npm_token <span class="se">\
</span>    <span class="nb">echo</span> <span class="s2">"//registry.npmjs.org/:_authToken=</span><span class="si">$(</span><span class="nb">cat</span> /run/secrets/npm_token<span class="si">)</span><span class="s2">"</span> <span class="o">&gt;</span> .npmrc <span class="o">&amp;&amp;</span> <span class="se">\
</span>    npm ci <span class="o">&amp;&amp;</span> <span class="se">\
</span>    <span class="nb">rm</span> .npmrc

<span class="k">COPY</span><span class="s"> . .</span>

<span class="k">RUN </span>npm run build

<span class="k">CMD</span><span class="s"> ["npm", "start"]</span>
</code></pre></div></div>

<p>For SSH access to private Git repositories:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> node:20-alpine</span>

<span class="k">RUN </span>apk add <span class="nt">--no-cache</span> git openssh-client

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="c"># Mount SSH socket for private repo access</span>
<span class="k">RUN </span><span class="nt">--mount</span><span class="o">=</span><span class="nb">type</span><span class="o">=</span>ssh <span class="se">\
</span>    <span class="nb">mkdir</span> <span class="nt">-p</span> ~/.ssh <span class="o">&amp;&amp;</span> <span class="se">\
</span>    ssh-keyscan github.com <span class="o">&gt;&gt;</span> ~/.ssh/known_hosts <span class="o">&amp;&amp;</span> <span class="se">\
</span>    npm <span class="nb">install </span>git+ssh://git@github.com/myorg/private-package.git

<span class="k">COPY</span><span class="s"> . .</span>

<span class="k">CMD</span><span class="s"> ["npm", "start"]</span>
</code></pre></div></div>

<h2 id="github-actions-integration">GitHub Actions Integration</h2>

<p>Docker Bake integrates seamlessly with GitHub Actions, providing a clean way to build and push container images in CI/CD pipelines.</p>

<h3 id="basic-github-actions-workflow">Basic GitHub Actions Workflow</h3>

<p>Create <code class="language-plaintext highlighter-rouge">.github/workflows/docker-build.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Build and Push Docker Images</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>
      <span class="pi">-</span> <span class="s">develop</span>
    <span class="na">tags</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">v*'</span>
  <span class="na">pull_request</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>

<span class="na">env</span><span class="pi">:</span>
  <span class="na">REGISTRY</span><span class="pi">:</span> <span class="s">ghcr.io</span>
  <span class="na">IMAGE_NAME</span><span class="pi">:</span> <span class="s">$</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
      
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Docker Buildx</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Container Registry</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name != 'pull_request'</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Extract metadata</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">meta</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">VERSION=latest</span>
          <span class="s">if [[ $GITHUB_REF == refs/tags/v* ]]; then</span>
            <span class="s">VERSION=${GITHUB_REF#refs/tags/v}</span>
          <span class="s">elif [[ $GITHUB_REF == refs/heads/main ]]; then</span>
            <span class="s">VERSION=main</span>
          <span class="s">elif [[ $GITHUB_REF == refs/heads/* ]]; then</span>
            <span class="s">VERSION=${GITHUB_REF#refs/heads/}</span>
          <span class="s">fi</span>
          
          <span class="s">echo "version=${VERSION}" &gt;&gt; $GITHUB_OUTPUT</span>
          <span class="s">echo "commit_sha=${GITHUB_SHA::8}" &gt;&gt; $GITHUB_OUTPUT</span>
          <span class="s">echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" &gt;&gt; $GITHUB_OUTPUT</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build images</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/bake-action@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">files</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">./docker-bake.hcl</span>
          <span class="na">set</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">*.VERSION=$</span>
            <span class="s">*.COMMIT_SHA=$</span>
            <span class="s">*.REGISTRY=$/$</span>
          <span class="na">push</span><span class="pi">:</span> <span class="s">$</span>
</code></pre></div></div>

<p>This workflow builds your images on every push and pull request, pushing only when changes are merged to main or when tags are created.</p>

<h3 id="multi-platform-builds-in-github-actions">Multi-Platform Builds in GitHub Actions</h3>

<p>For multi-platform builds, configure QEMU emulation:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Build Multi-Platform Images</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">tags</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">v*'</span>

<span class="na">env</span><span class="pi">:</span>
  <span class="na">REGISTRY</span><span class="pi">:</span> <span class="s">ghcr.io</span>
  <span class="na">IMAGE_NAME</span><span class="pi">:</span> <span class="s">$</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
      
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up QEMU</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-qemu-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">platforms</span><span class="pi">:</span> <span class="s1">'</span><span class="s">arm64,arm'</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Docker Buildx</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Container Registry</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Extract version</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">version</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">VERSION=${GITHUB_REF#refs/tags/v}</span>
          <span class="s">echo "version=${VERSION}" &gt;&gt; $GITHUB_OUTPUT</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build and push multi-platform images</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/bake-action@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">files</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">./docker-bake.hcl</span>
          <span class="na">targets</span><span class="pi">:</span> <span class="s">default</span>
          <span class="na">set</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">*.VERSION=$</span>
            <span class="s">*.REGISTRY=$/$</span>
            <span class="s">*.PLATFORMS=linux/amd64,linux/arm64</span>
          <span class="na">push</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<h3 id="matrix-builds-in-github-actions">Matrix Builds in GitHub Actions</h3>

<p>For building multiple variants, use GitHub Actions matrix strategy:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Build Image Matrix</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>

<span class="na">env</span><span class="pi">:</span>
  <span class="na">REGISTRY</span><span class="pi">:</span> <span class="s">ghcr.io</span>
  <span class="na">IMAGE_NAME</span><span class="pi">:</span> <span class="s">$</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
    
    <span class="na">strategy</span><span class="pi">:</span>
      <span class="na">matrix</span><span class="pi">:</span>
        <span class="na">python-version</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">3.10'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">3.11'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">3.12'</span><span class="pi">]</span>
        <span class="na">base-variant</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">slim'</span><span class="pi">,</span> <span class="s1">'</span><span class="s">alpine'</span><span class="pi">]</span>
    
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Docker Buildx</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Container Registry</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build and push</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/bake-action@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">files</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">./docker-bake.hcl</span>
          <span class="na">targets</span><span class="pi">:</span> <span class="s">app</span>
          <span class="na">set</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">*.PYTHON_VERSION=$</span>
            <span class="s">*.BASE_VARIANT=$</span>
            <span class="s">*.REGISTRY=$/$</span>
          <span class="na">push</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p>This workflow builds all combinations of Python versions and base variants in parallel.</p>

<h3 id="build-caching-in-ci">Build Caching in CI</h3>

<p>BuildKit caching dramatically speeds up CI builds. Configure cache exports and imports:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Build with Caching</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>

<span class="na">env</span><span class="pi">:</span>
  <span class="na">REGISTRY</span><span class="pi">:</span> <span class="s">ghcr.io</span>
  <span class="na">IMAGE_NAME</span><span class="pi">:</span> <span class="s">$</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
      
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Docker Buildx</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Container Registry</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build and push with cache</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/bake-action@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">files</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">./docker-bake.hcl</span>
          <span class="na">set</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">*.REGISTRY=$/$</span>
            <span class="s">*.cache-from=type=registry,ref=$/$:buildcache</span>
            <span class="s">*.cache-to=type=registry,ref=$/$:buildcache,mode=max</span>
          <span class="na">push</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p>Update your <code class="language-plaintext highlighter-rouge">docker-bake.hcl</code> to support cache configuration:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"CACHE_FROM"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">""</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"CACHE_TO"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">""</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  
  <span class="nx">cache-from</span> <span class="o">=</span> <span class="nx">CACHE_FROM</span> <span class="o">!=</span> <span class="s2">""</span> <span class="o">?</span> <span class="p">[</span><span class="nx">CACHE_FROM</span><span class="p">]</span> <span class="o">:</span> <span class="p">[]</span>
  <span class="nx">cache-to</span>   <span class="o">=</span> <span class="nx">CACHE_TO</span> <span class="o">!=</span> <span class="s2">""</span> <span class="o">?</span> <span class="p">[</span><span class="nx">CACHE_TO</span><span class="p">]</span> <span class="o">:</span> <span class="p">[]</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="reusable-workflow-for-multiple-repositories">Reusable Workflow for Multiple Repositories</h3>

<p>Create a reusable workflow that can be used across multiple repositories. In your organisation’s <code class="language-plaintext highlighter-rouge">.github</code> repository, create <code class="language-plaintext highlighter-rouge">.github/workflows/docker-bake-reusable.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Reusable Docker Bake Workflow</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">workflow_call</span><span class="pi">:</span>
    <span class="na">inputs</span><span class="pi">:</span>
      <span class="na">bake-file</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Path</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">docker-bake.hcl</span><span class="nv"> </span><span class="s">file'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
        <span class="na">default</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./docker-bake.hcl'</span>
      <span class="na">bake-target</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Bake</span><span class="nv"> </span><span class="s">target</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">build'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
        <span class="na">default</span><span class="pi">:</span> <span class="s1">'</span><span class="s">default'</span>
      <span class="na">registry</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Container</span><span class="nv"> </span><span class="s">registry'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
        <span class="na">default</span><span class="pi">:</span> <span class="s1">'</span><span class="s">ghcr.io'</span>
      <span class="na">platforms</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Platforms</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">build</span><span class="nv"> </span><span class="s">for'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
        <span class="na">default</span><span class="pi">:</span> <span class="s1">'</span><span class="s">linux/amd64,linux/arm64'</span>
      <span class="na">push</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Push</span><span class="nv"> </span><span class="s">images</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">registry'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">boolean</span>
        <span class="na">default</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">secrets</span><span class="pi">:</span>
      <span class="na">registry-username</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Registry</span><span class="nv"> </span><span class="s">username'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
      <span class="na">registry-password</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Registry</span><span class="nv"> </span><span class="s">password'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
    
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up QEMU</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-qemu-action@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Docker Buildx</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Container Registry</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Extract metadata</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">meta</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">VERSION=latest</span>
          <span class="s">if [[ $GITHUB_REF == refs/tags/v* ]]; then</span>
            <span class="s">VERSION=${GITHUB_REF#refs/tags/v}</span>
          <span class="s">elif [[ $GITHUB_REF == refs/heads/main ]]; then</span>
            <span class="s">VERSION=main</span>
          <span class="s">fi</span>
          
          <span class="s">echo "version=${VERSION}" &gt;&gt; $GITHUB_OUTPUT</span>
          <span class="s">echo "commit_sha=${GITHUB_SHA::8}" &gt;&gt; $GITHUB_OUTPUT</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build and push</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/bake-action@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">files</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">targets</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">set</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">*.VERSION=$</span>
            <span class="s">*.COMMIT_SHA=$</span>
            <span class="s">*.REGISTRY=$/$</span>
            <span class="s">*.PLATFORMS=$</span>
          <span class="na">push</span><span class="pi">:</span> <span class="s">$</span>
</code></pre></div></div>

<p>Use it in your application repositories:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Build Docker Images</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>
    <span class="na">tags</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">v*'</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">uses</span><span class="pi">:</span> <span class="s">myorg/.github/.github/workflows/docker-bake-reusable.yml@main</span>
    <span class="na">with</span><span class="pi">:</span>
      <span class="na">platforms</span><span class="pi">:</span> <span class="s1">'</span><span class="s">linux/amd64,linux/arm64'</span>
      <span class="na">push</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">secrets</span><span class="pi">:</span>
      <span class="na">registry-password</span><span class="pi">:</span> <span class="s">$</span>
</code></pre></div></div>

<h2 id="complete-real-world-example">Complete Real-World Example</h2>

<p>Let’s put everything together with a complete example: a Node.js application that supports multiple Node versions, multiple platforms, and builds both development and production variants.</p>

<h3 id="project-structure">Project Structure</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.
├── docker-bake.hcl
├── docker-bake.override.hcl.example
├── Dockerfile
├── package.json
├── src/
│   └── app.js
└── .github/
    └── workflows/
        └── docker.yml
</code></pre></div></div>

<h3 id="docker-bakehcl">docker-bake.hcl</h3>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"VERSION"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"dev"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"ghcr.io/myorg"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"COMMIT_SHA"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"unknown"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"BUILD_DATE"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">""</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"NODE_VERSIONS"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"18"</span><span class="p">,</span> <span class="s2">"20"</span><span class="p">,</span> <span class="s2">"21"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"PLATFORMS"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">,</span> <span class="s2">"linux/arm64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">function</span> <span class="s2">"semver_tags"</span> <span class="p">{</span>
  <span class="nx">params</span> <span class="o">=</span> <span class="p">[</span><span class="nx">registry</span><span class="p">,</span> <span class="nx">app</span><span class="p">,</span> <span class="nx">version</span><span class="p">,</span> <span class="nx">node_version</span><span class="p">]</span>
  <span class="nx">result</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"${registry}/${app}:${version}-node${node_version}"</span><span class="p">,</span>
    <span class="s2">"${registry}/${app}:node${node_version}"</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="nx">function</span> <span class="s2">"prod_tags"</span> <span class="p">{</span>
  <span class="nx">params</span> <span class="o">=</span> <span class="p">[</span><span class="nx">registry</span><span class="p">,</span> <span class="nx">app</span><span class="p">,</span> <span class="nx">version</span><span class="p">,</span> <span class="nx">node_version</span><span class="p">]</span>
  <span class="nx">result</span> <span class="o">=</span> <span class="nx">concat</span><span class="p">(</span>
    <span class="nx">semver_tags</span><span class="p">(</span><span class="nx">registry</span><span class="p">,</span> <span class="nx">app</span><span class="p">,</span> <span class="nx">version</span><span class="p">,</span> <span class="nx">node_version</span><span class="p">),</span>
    <span class="nx">node_version</span> <span class="o">==</span> <span class="s2">"20"</span> <span class="o">?</span> <span class="p">[</span>
      <span class="s2">"${registry}/${app}:${version}"</span><span class="p">,</span>
      <span class="s2">"${registry}/${app}:latest"</span>
    <span class="p">]</span> <span class="o">:</span> <span class="p">[]</span>
  <span class="p">)</span>
<span class="p">}</span>

<span class="c1"># Node 18 targets</span>
<span class="nx">target</span> <span class="s2">"app-node18-dev"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"node-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:dev-node18"</span><span class="p">]</span>
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">NODE_VERSION</span> <span class="o">=</span> <span class="s2">"18"</span>
    <span class="nx">NODE_ENV</span> <span class="o">=</span> <span class="s2">"development"</span>
  <span class="p">}</span>
  <span class="nx">platforms</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-node18-prod"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"node-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="nx">prod_tags</span><span class="p">(</span><span class="nx">REGISTRY</span><span class="p">,</span> <span class="s2">"myapp"</span><span class="p">,</span> <span class="nx">VERSION</span><span class="p">,</span> <span class="s2">"18"</span><span class="p">)</span>
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">NODE_VERSION</span> <span class="o">=</span> <span class="s2">"18"</span>
    <span class="nx">NODE_ENV</span> <span class="o">=</span> <span class="s2">"production"</span>
  <span class="p">}</span>
  <span class="nx">output</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"type=registry,push=true"</span><span class="p">]</span>
<span class="p">}</span>

<span class="c1"># Node 20 targets</span>
<span class="nx">target</span> <span class="s2">"app-node20-dev"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"node-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:dev-node20"</span><span class="p">,</span> <span class="s2">"${REGISTRY}/myapp:dev"</span><span class="p">]</span>
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">NODE_VERSION</span> <span class="o">=</span> <span class="s2">"20"</span>
    <span class="nx">NODE_ENV</span> <span class="o">=</span> <span class="s2">"development"</span>
  <span class="p">}</span>
  <span class="nx">platforms</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-node20-prod"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"node-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="nx">prod_tags</span><span class="p">(</span><span class="nx">REGISTRY</span><span class="p">,</span> <span class="s2">"myapp"</span><span class="p">,</span> <span class="nx">VERSION</span><span class="p">,</span> <span class="s2">"20"</span><span class="p">)</span>
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">NODE_VERSION</span> <span class="o">=</span> <span class="s2">"20"</span>
    <span class="nx">NODE_ENV</span> <span class="o">=</span> <span class="s2">"production"</span>
  <span class="p">}</span>
  <span class="nx">output</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"type=registry,push=true"</span><span class="p">]</span>
<span class="p">}</span>

<span class="c1"># Node 21 targets</span>
<span class="nx">target</span> <span class="s2">"app-node21-dev"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"node-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="p">[</span><span class="s2">"${REGISTRY}/myapp:dev-node21"</span><span class="p">]</span>
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">NODE_VERSION</span> <span class="o">=</span> <span class="s2">"21"</span>
    <span class="nx">NODE_ENV</span> <span class="o">=</span> <span class="s2">"development"</span>
  <span class="p">}</span>
  <span class="nx">platforms</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">target</span> <span class="s2">"app-node21-prod"</span> <span class="p">{</span>
  <span class="nx">inherits</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"node-common"</span><span class="p">]</span>
  <span class="nx">tags</span>       <span class="o">=</span> <span class="nx">prod_tags</span><span class="p">(</span><span class="nx">REGISTRY</span><span class="p">,</span> <span class="s2">"myapp"</span><span class="p">,</span> <span class="nx">VERSION</span><span class="p">,</span> <span class="s2">"21"</span><span class="p">)</span>
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">NODE_VERSION</span> <span class="o">=</span> <span class="s2">"21"</span>
    <span class="nx">NODE_ENV</span> <span class="o">=</span> <span class="s2">"production"</span>
  <span class="p">}</span>
  <span class="nx">output</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"type=registry,push=true"</span><span class="p">]</span>
<span class="p">}</span>

<span class="c1"># Common target configuration</span>
<span class="nx">target</span> <span class="s2">"node-common"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"Dockerfile"</span>
  <span class="nx">platforms</span>  <span class="o">=</span> <span class="nx">PLATFORMS</span>
  
  <span class="nx">args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">VERSION</span>    <span class="o">=</span> <span class="nx">VERSION</span>
    <span class="nx">COMMIT_SHA</span> <span class="o">=</span> <span class="nx">COMMIT_SHA</span>
    <span class="nx">BUILD_DATE</span> <span class="o">=</span> <span class="nx">BUILD_DATE</span> <span class="o">!=</span> <span class="s2">""</span> <span class="o">?</span> <span class="nx">BUILD_DATE</span> <span class="o">:</span> <span class="nx">timestamp</span><span class="err">()</span>
  <span class="p">}</span>
  
  <span class="nx">labels</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s2">"org.opencontainers.image.version"</span>    <span class="p">=</span> <span class="nx">VERSION</span>
    <span class="s2">"org.opencontainers.image.revision"</span>   <span class="o">=</span> <span class="nx">COMMIT_SHA</span>
    <span class="s2">"org.opencontainers.image.created"</span>    <span class="o">=</span> <span class="nx">BUILD_DATE</span> <span class="o">!=</span> <span class="s2">""</span> <span class="o">?</span> <span class="nx">BUILD_DATE</span> <span class="o">:</span> <span class="nx">timestamp</span><span class="p">()</span>
    <span class="s2">"org.opencontainers.image.source"</span>     <span class="p">=</span> <span class="s2">"https://github.com/myorg/myapp"</span>
    <span class="s2">"org.opencontainers.image.vendor"</span>     <span class="p">=</span> <span class="s2">"MyOrg"</span>
  <span class="p">}</span>
  
  <span class="nx">cache-from</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"type=registry,ref=${REGISTRY}/myapp:buildcache"</span><span class="p">]</span>
  <span class="nx">cache-to</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"type=registry,ref=${REGISTRY}/myapp:buildcache,mode=max"</span><span class="p">]</span>
<span class="p">}</span>

<span class="c1"># Groups for different scenarios</span>
<span class="nx">group</span> <span class="s2">"dev"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-node18-dev"</span><span class="p">,</span> <span class="s2">"app-node20-dev"</span><span class="p">,</span> <span class="s2">"app-node21-dev"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"prod"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-node18-prod"</span><span class="p">,</span> <span class="s2">"app-node20-prod"</span><span class="p">,</span> <span class="s2">"app-node21-prod"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"node20"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-node20-dev"</span><span class="p">,</span> <span class="s2">"app-node20-prod"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">group</span> <span class="s2">"default"</span> <span class="p">{</span>
  <span class="nx">targets</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"app-node20-dev"</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="dockerfile">Dockerfile</h3>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> NODE_VERSION=20</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">node:${NODE_VERSION}-alpine</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</span>

<span class="k">ARG</span><span class="s"> VERSION</span>
<span class="k">ARG</span><span class="s"> COMMIT_SHA</span>
<span class="k">ARG</span><span class="s"> BUILD_DATE</span>
<span class="k">ARG</span><span class="s"> NODE_ENV=production</span>

<span class="k">LABEL</span><span class="s"> org.opencontainers.image.version="${VERSION}"</span>
<span class="k">LABEL</span><span class="s"> org.opencontainers.image.revision="${COMMIT_SHA}"</span>
<span class="k">LABEL</span><span class="s"> org.opencontainers.image.created="${BUILD_DATE}"</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="c"># Dependencies stage</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">dependencies</span>

<span class="k">COPY</span><span class="s"> package*.json ./</span>

<span class="k">RUN if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$NODE_ENV</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"production"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> <span class="se">\
</span>      npm ci <span class="nt">--only</span><span class="o">=</span>production<span class="p">;</span> <span class="se">\
</span>    <span class="k">else</span> <span class="se">\
</span>      npm ci<span class="p">;</span> <span class="se">\
</span>    <span class="k">fi</span>

<span class="c"># Build stage (for TypeScript or other build steps)</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">dependencies</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span>

<span class="k">COPY</span><span class="s"> . .</span>

<span class="k">RUN if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$NODE_ENV</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"production"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> <span class="se">\
</span>      npm run build 2&gt;/dev/null <span class="o">||</span> <span class="nb">true</span><span class="p">;</span> <span class="se">\
</span>    <span class="k">fi</span>

<span class="c"># Production stage</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">production</span>

<span class="k">ENV</span><span class="s"> NODE_ENV=production</span>

<span class="c"># Copy only production dependencies</span>
<span class="k">COPY</span><span class="s"> --from=dependencies /app/node_modules ./node_modules</span>
<span class="k">COPY</span><span class="s"> --from=build /app/dist ./dist 2&gt;/dev/null || true</span>
<span class="k">COPY</span><span class="s"> package*.json ./</span>
<span class="k">COPY</span><span class="s"> src ./src</span>

<span class="c"># Create non-root user</span>
<span class="k">RUN </span>addgroup <span class="nt">-g</span> 1001 <span class="nt">-S</span> nodejs <span class="o">&amp;&amp;</span> <span class="se">\
</span>    adduser <span class="nt">-S</span> nodejs <span class="nt">-u</span> 1001 <span class="o">&amp;&amp;</span> <span class="se">\
</span>    <span class="nb">chown</span> <span class="nt">-R</span> nodejs:nodejs /app

<span class="k">USER</span><span class="s"> nodejs</span>

<span class="k">EXPOSE</span><span class="s"> 3000</span>

<span class="k">HEALTHCHECK</span><span class="s"> --interval=30s --timeout=3s --start-period=5s --retries=3 \</span>
  CMD node -e "require('http').get('http://localhost:3000/health', (r) =&gt; {process.exit(r.statusCode === 200 ? 0 : 1)})"

<span class="k">CMD</span><span class="s"> ["node", "src/app.js"]</span>

<span class="c"># Development stage</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">dependencies</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">development</span>

<span class="k">ENV</span><span class="s"> NODE_ENV=development</span>

<span class="k">COPY</span><span class="s"> . .</span>

<span class="k">USER</span><span class="s"> node</span>

<span class="k">EXPOSE</span><span class="s"> 3000</span>

<span class="k">CMD</span><span class="s"> ["npm", "run", "dev"]</span>
</code></pre></div></div>

<h3 id="github-actions-workflow">GitHub Actions Workflow</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Build and Push Docker Images</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>
      <span class="pi">-</span> <span class="s">develop</span>
    <span class="na">tags</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">v*'</span>
  <span class="na">pull_request</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>

<span class="na">env</span><span class="pi">:</span>
  <span class="na">REGISTRY</span><span class="pi">:</span> <span class="s">ghcr.io</span>
  <span class="na">IMAGE_NAME</span><span class="pi">:</span> <span class="s">$</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build-dev</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name == 'pull_request' || github.ref == 'refs/heads/develop'</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
    
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Docker Buildx</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Container Registry</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name != 'pull_request'</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build development images</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/bake-action@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">files</span><span class="pi">:</span> <span class="s">./docker-bake.hcl</span>
          <span class="na">targets</span><span class="pi">:</span> <span class="s">dev</span>
          <span class="na">set</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">*.REGISTRY=$/$</span>
            <span class="s">*.COMMIT_SHA=$</span>
          <span class="na">push</span><span class="pi">:</span> <span class="s">$</span>
  
  <span class="na">build-prod</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
    
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up QEMU</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-qemu-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">platforms</span><span class="pi">:</span> <span class="s1">'</span><span class="s">arm64'</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Docker Buildx</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Container Registry</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Extract version</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">version</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">if [[ $GITHUB_REF == refs/tags/v* ]]; then</span>
            <span class="s">VERSION=${GITHUB_REF#refs/tags/v}</span>
          <span class="s">else</span>
            <span class="s">VERSION=main-${GITHUB_SHA::8}</span>
          <span class="s">fi</span>
          <span class="s">echo "version=${VERSION}" &gt;&gt; $GITHUB_OUTPUT</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build and push production images</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/bake-action@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">files</span><span class="pi">:</span> <span class="s">./docker-bake.hcl</span>
          <span class="na">targets</span><span class="pi">:</span> <span class="s">prod</span>
          <span class="na">set</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">*.VERSION=$</span>
            <span class="s">*.COMMIT_SHA=$</span>
            <span class="s">*.REGISTRY=$/$</span>
            <span class="s">*.BUILD_DATE=$</span>
          <span class="na">push</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<h2 id="best-practices-and-tips">Best Practices and Tips</h2>

<p>After working with Docker Bake across numerous projects, several patterns have emerged as particularly effective.</p>

<h3 id="keep-bake-files-declarative">Keep Bake Files Declarative</h3>

<p>Resist the temptation to add too much logic to your Bake files. They work best when they declare what you want to build, not how to build it. Complex conditionals and computations belong in CI scripts or separate tooling, not in Bake configuration.</p>

<h3 id="use-inheritance-extensively">Use Inheritance Extensively</h3>

<p>The <code class="language-plaintext highlighter-rouge">inherits</code> field is one of Bake’s most powerful features. Define common configuration once in a base target, then inherit from it in specific targets. This reduces duplication and ensures consistency across related builds.</p>

<h3 id="organise-targets-into-logical-groups">Organise Targets into Logical Groups</h3>

<p>Groups make it easy to build related targets together. Create groups for different scenarios: <code class="language-plaintext highlighter-rouge">dev</code> for development builds, <code class="language-plaintext highlighter-rouge">prod</code> for production, <code class="language-plaintext highlighter-rouge">ci</code> for continuous integration. Users can then run <code class="language-plaintext highlighter-rouge">docker buildx bake dev</code> without needing to know which specific targets that includes.</p>

<h3 id="version-your-bake-files">Version Your Bake Files</h3>

<p>Treat your <code class="language-plaintext highlighter-rouge">docker-bake.hcl</code> files as code. Version them in Git, review changes through pull requests, and test modifications before merging. Bake configuration changes can have significant impact on your build process.</p>

<h3 id="document-your-targets">Document Your Targets</h3>

<p>Add comments explaining what each target does and when to use it. Future maintainers (including yourself) will appreciate understanding the purpose of <code class="language-plaintext highlighter-rouge">app-python-311-alpine-optimised</code> without having to reverse-engineer the configuration.</p>

<h3 id="use-override-files-for-local-development">Use Override Files for Local Development</h3>

<p>Create a <code class="language-plaintext highlighter-rouge">docker-bake.override.hcl.example</code> file that developers can copy to <code class="language-plaintext highlighter-rouge">docker-bake.override.hcl</code> for local customisation. Bake automatically merges override files, allowing developers to adjust registry paths or platforms without modifying the main configuration.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># docker-bake.override.hcl.example</span>
<span class="nx">variable</span> <span class="s2">"REGISTRY"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="s2">"localhost:5000"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"PLATFORMS"</span> <span class="p">{</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"linux/amd64"</span><span class="p">]</span>  <span class="c1"># Build only local platform for speed</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="validate-your-bake-files">Validate Your Bake Files</h3>

<p>Before committing changes, validate your Bake files:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker buildx bake <span class="nt">--print</span>
</code></pre></div></div>

<p>This shows the resolved configuration without actually building anything, catching syntax errors and misconfigurations early.</p>

<h3 id="cache-aggressively-in-ci">Cache Aggressively in CI</h3>

<p>Build caching can reduce CI build times from minutes to seconds. Configure both <code class="language-plaintext highlighter-rouge">cache-from</code> and <code class="language-plaintext highlighter-rouge">cache-to</code> in your targets and ensure your CI environment pushes cache layers to a registry.</p>

<h3 id="monitor-build-times">Monitor Build Times</h3>

<p>Track how long your builds take and optimise the slow ones. Multi-stage builds with effective layer caching make the biggest difference. BuildKit’s build statistics show where time is spent.</p>

<h2 id="troubleshooting-common-issues">Troubleshooting Common Issues</h2>

<p>Even with careful configuration, you’ll occasionally encounter issues. Here are solutions to the most common problems.</p>

<h3 id="bake-cant-find-dockerfile">Bake Can’t Find Dockerfile</h3>

<p>Error: <code class="language-plaintext highlighter-rouge">failed to solve: failed to read dockerfile</code></p>

<p>This usually means the context or dockerfile path is wrong. Remember that paths in Bake files are relative to the Bake file’s location, not your current directory. Use <code class="language-plaintext highlighter-rouge">./</code> explicitly:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">target</span> <span class="s2">"app"</span> <span class="p">{</span>
  <span class="nx">context</span>    <span class="o">=</span> <span class="s2">"."</span>
  <span class="nx">dockerfile</span> <span class="o">=</span> <span class="s2">"./Dockerfile"</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="platforms-not-building">Platforms Not Building</h3>

<p>If multi-platform builds fail, ensure you have the correct builder:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker buildx create <span class="nt">--name</span> multiplatform <span class="nt">--driver</span> docker-container <span class="nt">--use</span>
docker buildx inspect <span class="nt">--bootstrap</span>
</code></pre></div></div>

<p>Then verify your builder supports the platforms you need:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker buildx inspect multiplatform
</code></pre></div></div>

<h3 id="cache-not-working">Cache Not Working</h3>

<p>If builds aren’t using cache despite configuration, check that:</p>

<ol>
  <li>Cache references are accessible (correct registry, authentication)</li>
  <li>You’re using <code class="language-plaintext highlighter-rouge">mode=max</code> for cache-to to cache all layers</li>
  <li>Your Dockerfile uses layer caching effectively (copy dependencies before code)</li>
</ol>

<h3 id="variables-not-resolving">Variables Not Resolving</h3>

<p>If variables show as literal strings instead of values, check that:</p>

<ol>
  <li>You’re using <code class="language-plaintext highlighter-rouge">${}</code> syntax, not <code class="language-plaintext highlighter-rouge">$</code> alone: <code class="language-plaintext highlighter-rouge">${VERSION}</code>, not <code class="language-plaintext highlighter-rouge">$VERSION</code></li>
  <li>Variables are defined before use</li>
  <li>You’re not mixing environment variables with Bake variables (prefix env vars with <code class="language-plaintext highlighter-rouge">$</code>)</li>
</ol>

<h3 id="build-fails-in-ci-but-works-locally">Build Fails in CI But Works Locally</h3>

<p>This often indicates environmental differences:</p>

<ol>
  <li>Check platform architecture (CI might be different from local)</li>
  <li>Verify secrets and SSH keys are available in CI</li>
  <li>Ensure network access for downloading dependencies</li>
  <li>Check resource limits (CI runners might have less memory/CPU)</li>
</ol>

<h2 id="conclusion">Conclusion</h2>

<p>Docker Bake transforms container image builds from imperative scripts into declarative configuration. Instead of maintaining complex shell scripts that orchestrate dozens of <code class="language-plaintext highlighter-rouge">docker build</code> commands, you define your complete build matrix in a single file that serves as documentation, automation, and contract all at once.</p>

<p>The patterns we’ve explored—multi-platform builds, multiple base images, runtime version matrices, sophisticated tagging strategies—all become manageable through Bake’s inheritance, functions, and groups. Your build configuration becomes more maintainable, easier to understand, and more reliable.</p>

<p>For production container workflows, Bake isn’t just a convenience, it’s a force multiplier that lets you maintain complex build matrices without drowning in complexity. The GitHub Actions integrations show how Bake fits naturally into modern CI/CD pipelines, providing consistent builds from local development through production deployment.</p>

<p>As your containerisation needs grow, Docker Bake grows with you. Start simple with basic targets and groups, then progressively add platforms, variants, and optimisations as requirements evolve. The declarative configuration ensures that complexity remains manageable no matter how large your build matrix becomes.</p>

<p>The investment in learning Docker Bake pays dividends quickly. Your builds become faster through better caching, more reliable through consistent configuration, and more maintainable through clear declarative structure. You’ve built a foundation that will scale from a handful of images to hundreds of build variants without requiring fundamental rewrites.</p>]]></content><author><name>Glen Thomas</name></author><category term="Platform Engineering" /><category term="Software Engineering" /><category term="Docker" /><category term="Docker Bake" /><category term="Multi-Platform" /><category term="CI/CD" /><category term="GitHub Actions" /><category term="Container Images" /><summary type="html"><![CDATA[Building Docker images has evolved far beyond simple docker build commands. Modern applications demand multi-platform support to run on both x86 and ARM architectures, multiple base images to support different runtime environments, and sophisticated tagging strategies to manage versions across development, staging, and production. Managing this complexity with shell scripts quickly becomes unwieldy, error-prone, and difficult to maintain.]]></summary></entry><entry><title type="html">Container Security Fundamentals: Protecting Your Containerised Applications</title><link href="https://blog.glen-thomas.com/platform%20engineering/software%20engineering/2025/12/01/container-security-fundamentals-protecting-your-containerised-applications.html" rel="alternate" type="text/html" title="Container Security Fundamentals: Protecting Your Containerised Applications" /><published>2025-12-01T21:18:00+00:00</published><updated>2025-12-01T21:18:00+00:00</updated><id>https://blog.glen-thomas.com/platform%20engineering/software%20engineering/2025/12/01/container-security-fundamentals-protecting-your-containerised-applications</id><content type="html" xml:base="https://blog.glen-thomas.com/platform%20engineering/software%20engineering/2025/12/01/container-security-fundamentals-protecting-your-containerised-applications.html"><![CDATA[<p>Containers have fundamentally changed how we build and deploy applications, but they’ve also introduced new security considerations that many teams struggle to address comprehensively. The speed and convenience of containers can lull organisations into a false sense of security, treating them as lightweight virtual machines without understanding the fundamentally different security model they represent.</p>

<p>The reality is that containers share the host kernel with every other container on the same machine. A vulnerability in that kernel affects every container. A container escape gives an attacker access to the entire host and potentially every other workload running there. Understanding these risks—and the mechanisms Linux provides to mitigate them—is essential for anyone running containers in production.</p>

<p>In this comprehensive guide, I’ll walk you through the fundamental security concepts that underpin container isolation, the practical measures you can implement to harden your containerised applications, and the tools and workflows that make security a natural part of your development process rather than an afterthought bolted on at deployment time.</p>

<h2 id="understanding-container-isolation">Understanding Container Isolation</h2>

<p>Before we can secure containers effectively, we need to understand how they achieve isolation in the first place. Containers aren’t virtual machines—they don’t run separate kernels or emulate hardware. Instead, they use Linux kernel features to create isolated environments that share the same kernel whilst appearing to be separate systems.</p>

<h3 id="the-shared-kernel-model">The Shared Kernel Model</h3>

<p>Every container on a host shares the same Linux kernel. When you run <code class="language-plaintext highlighter-rouge">docker run ubuntu:22.04</code>, you’re not running an Ubuntu kernel—you’re running the host’s kernel with an Ubuntu userspace. This is fundamentally different from virtual machines, where each VM runs its own kernel instance.</p>

<p>This shared kernel model brings both efficiency and risk. Containers start in milliseconds because there’s no kernel to boot. They use less memory because there’s no kernel overhead per container. But a kernel vulnerability affects every container on the host, and a container escape potentially compromises every workload.</p>

<p>Understanding this model shapes how we think about container security. We’re not securing isolated systems; we’re securing processes that share resources with other processes whilst maintaining the illusion of isolation.</p>

<h3 id="linux-namespaces">Linux Namespaces</h3>

<p>Namespaces are the primary isolation mechanism for containers. They partition kernel resources so that one set of processes sees one set of resources whilst another set of processes sees a different set. Linux provides several namespace types, each isolating a different aspect of the system.</p>

<p>The PID namespace isolates process IDs. Inside a container, processes see themselves as PID 1, 2, 3, and so on, but from the host’s perspective, these same processes have entirely different PIDs. This prevents containers from seeing or signalling processes in other containers.</p>

<p>The network namespace isolates network interfaces, routing tables, and firewall rules. Each container gets its own network stack, appearing to have dedicated network interfaces even though the physical hardware is shared.</p>

<p>The mount namespace isolates filesystem mount points. A container sees its own root filesystem without access to the host’s filesystem or other containers’ filesystems.</p>

<p>The UTS namespace isolates hostname and domain name. Each container can have its own hostname without affecting others.</p>

<p>The IPC namespace isolates inter-process communication resources like shared memory segments and message queues. This prevents containers from communicating through these mechanisms unless explicitly allowed.</p>

<p>The user namespace maps user and group IDs inside the container to different IDs outside. Root inside the container can be mapped to an unprivileged user on the host, significantly reducing the impact of container escapes.</p>

<p>Here’s how you can inspect namespaces for a running container:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get the container's PID on the host</span>
<span class="nv">CONTAINER_PID</span><span class="o">=</span><span class="si">$(</span>docker inspect <span class="nt">--format</span> <span class="s1">''</span> my-container<span class="si">)</span>

<span class="c"># List the namespaces for this process</span>
<span class="nb">ls</span> <span class="nt">-la</span> /proc/<span class="nv">$CONTAINER_PID</span>/ns/

<span class="c"># Compare with host namespaces</span>
<span class="nb">ls</span> <span class="nt">-la</span> /proc/1/ns/
</code></pre></div></div>

<h3 id="control-groups-cgroups">Control Groups (cgroups)</h3>

<p>While namespaces provide isolation of what a container can see, cgroups control what resources a container can use. They limit and account for CPU, memory, disk I/O, and network bandwidth consumption.</p>

<p>Without cgroups, a single container could consume all available memory, causing the kernel’s out-of-memory killer to terminate other containers or even critical host processes. A container could monopolise CPU, starving other workloads. Cgroups prevent this resource starvation.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># View cgroup limits for a container</span>
docker inspect <span class="nt">--format</span> <span class="s1">''</span> my-container
docker inspect <span class="nt">--format</span> <span class="s1">''</span> my-container

<span class="c"># Set resource limits when running a container</span>
docker run <span class="nt">-d</span> <span class="se">\</span>
  <span class="nt">--memory</span><span class="o">=</span><span class="s2">"512m"</span> <span class="se">\</span>
  <span class="nt">--memory-swap</span><span class="o">=</span><span class="s2">"512m"</span> <span class="se">\</span>
  <span class="nt">--cpus</span><span class="o">=</span><span class="s2">"1.5"</span> <span class="se">\</span>
  <span class="nt">--pids-limit</span><span class="o">=</span>100 <span class="se">\</span>
  myapp:latest
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">--memory</code> flag limits total memory. The <code class="language-plaintext highlighter-rouge">--memory-swap</code> flag set equal to <code class="language-plaintext highlighter-rouge">--memory</code> disables swap, preventing the container from using swap space when memory is exhausted. The <code class="language-plaintext highlighter-rouge">--cpus</code> flag limits CPU usage to 1.5 cores. The <code class="language-plaintext highlighter-rouge">--pids-limit</code> flag prevents fork bombs by limiting the number of processes.</p>

<h3 id="linux-capabilities">Linux Capabilities</h3>

<p>Traditional Unix security divides the world into two categories: root, which can do anything, and everyone else, which is heavily restricted. This is too coarse-grained for containers. You might need a container that can bind to privileged ports but can’t load kernel modules.</p>

<p>Linux capabilities split root’s powers into distinct units that can be granted independently. Instead of running as root with all powers, you can run as root with only the specific capabilities needed.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run a container with minimal capabilities</span>
docker run <span class="nt">-d</span> <span class="se">\</span>
  <span class="nt">--cap-drop</span><span class="o">=</span>ALL <span class="se">\</span>
  <span class="nt">--cap-add</span><span class="o">=</span>NET_BIND_SERVICE <span class="se">\</span>
  myapp:latest

<span class="c"># List capabilities of a running container</span>
docker inspect <span class="nt">--format</span> <span class="s1">''</span> my-container
docker inspect <span class="nt">--format</span> <span class="s1">''</span> my-container
</code></pre></div></div>

<p>Key capabilities to understand:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">CAP_NET_BIND_SERVICE</code>: Bind to ports below 1024</li>
  <li><code class="language-plaintext highlighter-rouge">CAP_NET_RAW</code>: Use raw sockets (needed for ping)</li>
  <li><code class="language-plaintext highlighter-rouge">CAP_SYS_ADMIN</code>: A catch-all for various admin operations (avoid granting this)</li>
  <li><code class="language-plaintext highlighter-rouge">CAP_SYS_PTRACE</code>: Trace processes (needed for debugging)</li>
  <li><code class="language-plaintext highlighter-rouge">CAP_CHOWN</code>: Change file ownership</li>
  <li><code class="language-plaintext highlighter-rouge">CAP_SETUID</code> / <code class="language-plaintext highlighter-rouge">CAP_SETGID</code>: Change user/group IDs</li>
</ul>

<p>The principle of least privilege demands dropping all capabilities and adding back only those specifically required. Most applications need far fewer capabilities than Docker grants by default.</p>

<h3 id="seccomp-profiles">Seccomp Profiles</h3>

<p>Seccomp (secure computing mode) filters system calls that a process can make. The Linux kernel provides hundreds of syscalls, but most applications need only a fraction of them. Seccomp profiles whitelist or blacklist specific syscalls, reducing the kernel attack surface available to a container.</p>

<p>Docker applies a default seccomp profile that blocks around 44 syscalls considered dangerous, including <code class="language-plaintext highlighter-rouge">reboot</code>, <code class="language-plaintext highlighter-rouge">mount</code>, and <code class="language-plaintext highlighter-rouge">kexec_load</code>. You can create custom profiles for tighter restrictions:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"defaultAction"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SCMP_ACT_ERRNO"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"defaultErrnoRet"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"architectures"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"SCMP_ARCH_X86_64"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"SCMP_ARCH_AARCH64"</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"syscalls"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"names"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"accept"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"accept4"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"bind"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"clone"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"close"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"connect"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"dup"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"dup2"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"execve"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"exit"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"exit_group"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"fcntl"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"fstat"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"futex"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"getpid"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"getsockname"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"getsockopt"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"listen"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"mmap"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"mprotect"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"munmap"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"open"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"openat"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"read"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"recvfrom"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"rt_sigaction"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"rt_sigprocmask"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"sendto"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"setsockopt"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"socket"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"write"</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SCMP_ACT_ALLOW"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Apply a custom seccomp profile:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="se">\</span>
  <span class="nt">--security-opt</span> <span class="nv">seccomp</span><span class="o">=</span>/path/to/profile.json <span class="se">\</span>
  myapp:latest
</code></pre></div></div>

<p>To generate a seccomp profile for your application, you can use tools like <code class="language-plaintext highlighter-rouge">strace</code> to observe which syscalls your application actually makes, then create a whitelist profile.</p>

<h2 id="image-security">Image Security</h2>

<p>Container images are the foundation of your containerised applications. A vulnerable or malicious base image compromises everything built upon it. Securing your images starts with choosing the right base and extends through scanning, signing, and supply chain verification.</p>

<h3 id="choosing-secure-base-images">Choosing Secure Base Images</h3>

<p>The base image you choose dramatically affects your security posture. A full Ubuntu image contains thousands of packages, each potentially harbouring vulnerabilities. A minimal image contains only what’s necessary, reducing attack surface significantly.</p>

<p><strong>Distroless Images</strong></p>

<p>Google’s distroless images contain only your application and its runtime dependencies—no shell, no package manager, no unnecessary utilities. An attacker who gains code execution can’t spawn a shell because there isn’t one.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Build stage</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">golang:1.21</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">RUN </span><span class="nv">CGO_ENABLED</span><span class="o">=</span>0 go build <span class="nt">-o</span> myapp

<span class="c"># Runtime stage - distroless</span>
<span class="k">FROM</span><span class="s"> gcr.io/distroless/static-debian12</span>

<span class="k">COPY</span><span class="s"> --from=builder /app/myapp /</span>
<span class="k">USER</span><span class="s"> nonroot:nonroot</span>

<span class="k">ENTRYPOINT</span><span class="s"> ["/myapp"]</span>
</code></pre></div></div>

<p><strong>Alpine Linux</strong></p>

<p>Alpine uses musl libc instead of glibc and provides a minimal base around 5MB. It includes a package manager but far fewer default packages than traditional distributions.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> alpine:3.19</span>

<span class="k">RUN </span>apk add <span class="nt">--no-cache</span> ca-certificates tzdata

<span class="k">COPY</span><span class="s"> myapp /usr/local/bin/</span>
<span class="k">USER</span><span class="s"> nobody:nobody</span>

<span class="k">ENTRYPOINT</span><span class="s"> ["/usr/local/bin/myapp"]</span>
</code></pre></div></div>

<p><strong>Scratch Images</strong></p>

<p>The <code class="language-plaintext highlighter-rouge">scratch</code> image is literally empty; no files, no filesystem, nothing. It’s suitable for statically compiled binaries that include everything they need.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="w"> </span><span class="s">golang:1.21</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">RUN </span><span class="nv">CGO_ENABLED</span><span class="o">=</span>0 <span class="nv">GOOS</span><span class="o">=</span>linux go build <span class="nt">-a</span> <span class="nt">-ldflags</span> <span class="s1">'-extldflags "-static"'</span> <span class="nt">-o</span> myapp

<span class="k">FROM</span><span class="s"> scratch</span>

<span class="k">COPY</span><span class="s"> --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/</span>
<span class="k">COPY</span><span class="s"> --from=builder /app/myapp /</span>

<span class="k">ENTRYPOINT</span><span class="s"> ["/myapp"]</span>
</code></pre></div></div>

<h3 id="vulnerability-scanning">Vulnerability Scanning</h3>

<p>Vulnerability scanners analyse your images against databases of known vulnerabilities (CVEs). They identify vulnerable packages and provide severity ratings to help prioritise remediation.</p>

<p><strong>Trivy</strong></p>

<p>Trivy is a comprehensive, fast scanner that checks for OS package vulnerabilities, language-specific dependencies, misconfigurations, and secrets.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Scan an image</span>
trivy image myapp:latest

<span class="c"># Scan with specific severity threshold</span>
trivy image <span class="nt">--severity</span> HIGH,CRITICAL myapp:latest

<span class="c"># Scan and fail if vulnerabilities found (useful for CI)</span>
trivy image <span class="nt">--exit-code</span> 1 <span class="nt">--severity</span> CRITICAL myapp:latest

<span class="c"># Scan a Dockerfile for misconfigurations</span>
trivy config Dockerfile

<span class="c"># Generate SBOM while scanning</span>
trivy image <span class="nt">--format</span> spdx-json <span class="nt">--output</span> sbom.spdx.json myapp:latest
</code></pre></div></div>

<p><strong>Grype</strong></p>

<p>Grype from Anchore focuses specifically on vulnerability scanning with excellent accuracy and speed.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Scan an image</span>
grype myapp:latest

<span class="c"># Output as JSON for processing</span>
grype <span class="nt">-o</span> json myapp:latest <span class="o">&gt;</span> vulnerabilities.json

<span class="c"># Fail on specific severity</span>
grype myapp:latest <span class="nt">--fail-on</span> critical
</code></pre></div></div>

<p><strong>Snyk</strong></p>

<p>Snyk provides vulnerability scanning with remediation advice and integrates well with development workflows.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Scan an image</span>
snyk container <span class="nb">test </span>myapp:latest

<span class="c"># Monitor for new vulnerabilities</span>
snyk container monitor myapp:latest

<span class="c"># Scan and provide fix recommendations</span>
snyk container <span class="nb">test </span>myapp:latest <span class="nt">--file</span><span class="o">=</span>Dockerfile
</code></pre></div></div>

<h3 id="multi-stage-builds-for-security">Multi-Stage Builds for Security</h3>

<p>Multi-stage builds are a security feature as much as an optimisation. They ensure build tools, source code, test data, and secrets used during build don’t end up in your final image.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Stage 1: Dependencies</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">node:20-alpine</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">dependencies</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> package*.json ./</span>
<span class="k">RUN </span>npm ci <span class="nt">--only</span><span class="o">=</span>production

<span class="c"># Stage 2: Build</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">node:20-alpine</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> package*.json ./</span>
<span class="k">RUN </span>npm ci

<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">RUN </span>npm run build
<span class="k">RUN </span>npm run <span class="nb">test</span>

<span class="c"># Stage 3: Production - only runtime artifacts</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">node:20-alpine</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">production</span>

<span class="k">RUN </span>addgroup <span class="nt">-g</span> 1001 <span class="nt">-S</span> nodejs <span class="o">&amp;&amp;</span> <span class="se">\
</span>    adduser <span class="nt">-S</span> nodejs <span class="nt">-u</span> 1001

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="c"># Copy only production dependencies</span>
<span class="k">COPY</span><span class="s"> --from=dependencies --chown=nodejs:nodejs /app/node_modules ./node_modules</span>

<span class="c"># Copy only built artifacts</span>
<span class="k">COPY</span><span class="s"> --from=builder --chown=nodejs:nodejs /app/dist ./dist</span>
<span class="k">COPY</span><span class="s"> --from=builder --chown=nodejs:nodejs /app/package.json ./</span>

<span class="k">USER</span><span class="s"> nodejs</span>

<span class="k">EXPOSE</span><span class="s"> 3000</span>

<span class="k">CMD</span><span class="s"> ["node", "dist/server.js"]</span>
</code></pre></div></div>

<p>Notice what doesn’t appear in the final image: npm (we use node directly), the full node_modules with dev dependencies, source TypeScript files, test files, and any secrets used during the build process. The final image contains only what’s needed to run the application.</p>

<h3 id="image-signing-and-verification">Image Signing and Verification</h3>

<p>Image signing cryptographically proves that an image came from a trusted source and hasn’t been tampered with. Sigstore’s Cosign has become the de facto standard for container image signing.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generate a key pair (for traditional key-based signing)</span>
cosign generate-key-pair

<span class="c"># Sign an image with a key</span>
cosign sign <span class="nt">--key</span> cosign.key myregistry/myapp:v1.0.0

<span class="c"># Sign using keyless signing (recommended)</span>
<span class="c"># This uses OIDC identity from your CI provider</span>
cosign sign myregistry/myapp:v1.0.0

<span class="c"># Verify a signature</span>
cosign verify <span class="nt">--key</span> cosign.pub myregistry/myapp:v1.0.0

<span class="c"># Verify keyless signature</span>
cosign verify <span class="se">\</span>
  <span class="nt">--certificate-identity</span><span class="o">=</span>https://github.com/myorg/myapp/.github/workflows/build.yml@refs/tags/v1.0.0 <span class="se">\</span>
  <span class="nt">--certificate-oidc-issuer</span><span class="o">=</span>https://token.actions.githubusercontent.com <span class="se">\</span>
  myregistry/myapp:v1.0.0
</code></pre></div></div>

<p>Keyless signing with Sigstore is particularly powerful for CI/CD. It uses your CI provider’s OIDC identity to sign images, eliminating the need to manage signing keys. The signature includes claims about who built the image and in what context, providing strong provenance guarantees.</p>

<h3 id="software-bill-of-materials-sbom">Software Bill of Materials (SBOM)</h3>

<p>An SBOM lists all components in your software, including libraries, dependencies, and their versions. When a new vulnerability is discovered, an SBOM lets you quickly determine which of your images are affected.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Generate SBOM with Syft</span>
syft myapp:latest <span class="nt">-o</span> spdx-json <span class="o">&gt;</span> sbom.spdx.json

<span class="c"># Generate SBOM with Trivy</span>
trivy image <span class="nt">--format</span> spdx-json <span class="nt">--output</span> sbom.json myapp:latest

<span class="c"># Attach SBOM to image with Cosign</span>
cosign attach sbom <span class="nt">--sbom</span> sbom.spdx.json myregistry/myapp:v1.0.0

<span class="c"># Scan SBOM for vulnerabilities</span>
grype sbom:sbom.spdx.json
</code></pre></div></div>

<p>Store SBOMs alongside your images and keep them for the lifetime of the image. When Log4Shell or the next major vulnerability hits, you’ll be able to query your SBOMs to identify affected images within minutes rather than days.</p>

<h2 id="runtime-security">Runtime Security</h2>

<p>Securing images is essential but insufficient. Runtime security focuses on protecting containers while they execute, limiting what they can do even if compromised.</p>

<h3 id="running-as-non-root">Running as Non-Root</h3>

<p>By default, containers run as root. A vulnerability that allows code execution gives the attacker root privileges inside the container. Whilst namespaces and capabilities limit what root can do, defence in depth demands running as an unprivileged user.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> node:20-alpine</span>

<span class="c"># Create a non-root user</span>
<span class="k">RUN </span>addgroup <span class="nt">-g</span> 1001 <span class="nt">-S</span> appgroup <span class="o">&amp;&amp;</span> <span class="se">\
</span>    adduser <span class="nt">-S</span> appuser <span class="nt">-u</span> 1001 <span class="nt">-G</span> appgroup

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="c"># Change ownership of application files</span>
<span class="k">COPY</span><span class="s"> --chown=appuser:appgroup . .</span>

<span class="k">RUN </span>npm ci <span class="nt">--only</span><span class="o">=</span>production

<span class="c"># Switch to non-root user</span>
<span class="k">USER</span><span class="s"> appuser</span>

<span class="k">EXPOSE</span><span class="s"> 3000</span>

<span class="k">CMD</span><span class="s"> ["node", "server.js"]</span>
</code></pre></div></div>

<p>Some images require specific user IDs to match existing filesystem permissions or to integrate with orchestration platforms. Kubernetes, for example, can enforce that containers run as non-root:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">secure-pod</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">securityContext</span><span class="pi">:</span>
    <span class="na">runAsNonRoot</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">runAsUser</span><span class="pi">:</span> <span class="m">1001</span>
    <span class="na">runAsGroup</span><span class="pi">:</span> <span class="m">1001</span>
    <span class="na">fsGroup</span><span class="pi">:</span> <span class="m">1001</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">myapp:latest</span>
      <span class="na">securityContext</span><span class="pi">:</span>
        <span class="na">allowPrivilegeEscalation</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">readOnlyRootFilesystem</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">capabilities</span><span class="pi">:</span>
          <span class="na">drop</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">ALL</span>
</code></pre></div></div>

<h3 id="read-only-root-filesystem">Read-Only Root Filesystem</h3>

<p>A read-only root filesystem prevents attackers from modifying binaries, dropping malware, or tampering with configuration. Any write attempts fail, limiting post-exploitation options.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run with read-only filesystem</span>
docker run <span class="nt">-d</span> <span class="se">\</span>
  <span class="nt">--read-only</span> <span class="se">\</span>
  <span class="nt">--tmpfs</span> /tmp:rw,noexec,nosuid,size<span class="o">=</span>64m <span class="se">\</span>
  <span class="nt">--tmpfs</span> /var/run:rw,noexec,nosuid,size<span class="o">=</span>16m <span class="se">\</span>
  myapp:latest
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">--tmpfs</code> mounts provide writable temporary directories in memory for applications that need to write temporary files or PID files. The <code class="language-plaintext highlighter-rouge">noexec</code> option prevents execution of files in these directories.</p>

<p>Your Dockerfile should prepare for read-only operation:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> node:20-alpine</span>

<span class="k">RUN </span>adduser <span class="nt">-S</span> appuser

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> --chown=appuser . .</span>
<span class="k">RUN </span>npm ci <span class="nt">--only</span><span class="o">=</span>production

<span class="c"># Ensure no writes needed to root filesystem</span>
<span class="c"># Pre-create any directories the app expects to exist</span>
<span class="k">RUN </span><span class="nb">mkdir</span> <span class="nt">-p</span> /app/cache <span class="o">&amp;&amp;</span> <span class="nb">chown </span>appuser /app/cache

<span class="k">USER</span><span class="s"> appuser</span>

<span class="k">CMD</span><span class="s"> ["node", "server.js"]</span>
</code></pre></div></div>

<h3 id="resource-limits">Resource Limits</h3>

<p>Resource limits prevent denial-of-service attacks where a compromised container exhausts host resources. They also protect against accidental resource exhaustion from memory leaks or runaway processes.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="se">\</span>
  <span class="nt">--memory</span><span class="o">=</span><span class="s2">"256m"</span> <span class="se">\</span>
  <span class="nt">--memory-swap</span><span class="o">=</span><span class="s2">"256m"</span> <span class="se">\</span>
  <span class="nt">--memory-reservation</span><span class="o">=</span><span class="s2">"128m"</span> <span class="se">\</span>
  <span class="nt">--cpus</span><span class="o">=</span><span class="s2">"0.5"</span> <span class="se">\</span>
  <span class="nt">--cpu-shares</span><span class="o">=</span>512 <span class="se">\</span>
  <span class="nt">--pids-limit</span><span class="o">=</span>50 <span class="se">\</span>
  <span class="nt">--ulimit</span> <span class="nv">nofile</span><span class="o">=</span>1024:1024 <span class="se">\</span>
  <span class="nt">--ulimit</span> <span class="nb">nproc</span><span class="o">=</span>64:64 <span class="se">\</span>
  myapp:latest
</code></pre></div></div>

<p>In Kubernetes, specify resource requests and limits:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">resource-limited-pod</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">myapp:latest</span>
      <span class="na">resources</span><span class="pi">:</span>
        <span class="na">requests</span><span class="pi">:</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">128Mi"</span>
          <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">250m"</span>
        <span class="na">limits</span><span class="pi">:</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">256Mi"</span>
          <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">500m"</span>
</code></pre></div></div>

<h3 id="network-policies">Network Policies</h3>

<p>Network policies control which containers can communicate with each other and with external services. By default, all containers can communicate with all other containers—a compromised container can probe and attack anything on the network.</p>

<p>In Kubernetes, NetworkPolicy resources define allowed traffic:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">NetworkPolicy</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">api-network-policy</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">production</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">podSelector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">api</span>
  <span class="na">policyTypes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">Ingress</span>
    <span class="pi">-</span> <span class="s">Egress</span>
  <span class="na">ingress</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">from</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">podSelector</span><span class="pi">:</span>
            <span class="na">matchLabels</span><span class="pi">:</span>
              <span class="na">app</span><span class="pi">:</span> <span class="s">frontend</span>
        <span class="pi">-</span> <span class="na">namespaceSelector</span><span class="pi">:</span>
            <span class="na">matchLabels</span><span class="pi">:</span>
              <span class="na">name</span><span class="pi">:</span> <span class="s">monitoring</span>
      <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
          <span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
  <span class="na">egress</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">to</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">podSelector</span><span class="pi">:</span>
            <span class="na">matchLabels</span><span class="pi">:</span>
              <span class="na">app</span><span class="pi">:</span> <span class="s">database</span>
      <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
          <span class="na">port</span><span class="pi">:</span> <span class="m">5432</span>
    <span class="pi">-</span> <span class="na">to</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">namespaceSelector</span><span class="pi">:</span> <span class="pi">{}</span>
          <span class="na">podSelector</span><span class="pi">:</span>
            <span class="na">matchLabels</span><span class="pi">:</span>
              <span class="na">k8s-app</span><span class="pi">:</span> <span class="s">kube-dns</span>
      <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">protocol</span><span class="pi">:</span> <span class="s">UDP</span>
          <span class="na">port</span><span class="pi">:</span> <span class="m">53</span>
</code></pre></div></div>

<p>This policy allows the API pod to receive traffic only from the frontend application and monitoring namespace, and to send traffic only to the database and DNS.</p>

<p>For Docker without Kubernetes, use Docker networks to isolate containers:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create isolated networks</span>
docker network create <span class="nt">--internal</span> backend-net
docker network create frontend-net

<span class="c"># Database only on internal network (no external access)</span>
docker run <span class="nt">-d</span> <span class="nt">--network</span> backend-net <span class="nt">--name</span> db postgres:15

<span class="c"># API on both networks</span>
docker run <span class="nt">-d</span> <span class="nt">--network</span> backend-net <span class="nt">--network</span> frontend-net <span class="nt">--name</span> api myapi:latest

<span class="c"># Frontend only on frontend network</span>
docker run <span class="nt">-d</span> <span class="nt">--network</span> frontend-net <span class="nt">-p</span> 80:80 <span class="nt">--name</span> web nginx:alpine
</code></pre></div></div>

<h3 id="runtime-threat-detection">Runtime Threat Detection</h3>

<p>Runtime security tools monitor container behaviour and alert on or block suspicious activity. They detect attacks that static scanning can’t catch—exploits against zero-day vulnerabilities, malicious insider activity, or attacks using legitimate tools.</p>

<p><strong>Falco</strong></p>

<p>Falco is an open-source runtime security tool that monitors syscalls and alerts on suspicious behaviour.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example Falco rules</span>
<span class="pi">-</span> <span class="na">rule</span><span class="pi">:</span> <span class="s">Terminal Shell in Container</span>
  <span class="na">desc</span><span class="pi">:</span> <span class="s">Detect shell spawned in a container</span>
  <span class="na">condition</span><span class="pi">:</span> <span class="pi">&gt;</span>
    <span class="s">spawned_process and </span>
    <span class="s">container and </span>
    <span class="s">shell_procs and </span>
    <span class="s">proc.tty != 0</span>
  <span class="na">output</span><span class="pi">:</span> <span class="pi">&gt;</span>
    <span class="s">Shell spawned in container </span>
    <span class="s">(user=%user.name container=%container.name shell=%proc.name </span>
    <span class="s">parent=%proc.pname cmdline=%proc.cmdline)</span>
  <span class="na">priority</span><span class="pi">:</span> <span class="s">WARNING</span>
  <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">container</span><span class="pi">,</span> <span class="nv">shell</span><span class="pi">,</span> <span class="nv">mitre_execution</span><span class="pi">]</span>

<span class="pi">-</span> <span class="na">rule</span><span class="pi">:</span> <span class="s">Write Below Binary Dir</span>
  <span class="na">desc</span><span class="pi">:</span> <span class="s">Detect writes to binary directories</span>
  <span class="na">condition</span><span class="pi">:</span> <span class="pi">&gt;</span>
    <span class="s">open_write and </span>
    <span class="s">container and </span>
    <span class="s">bin_dir</span>
  <span class="na">output</span><span class="pi">:</span> <span class="pi">&gt;</span>
    <span class="s">Write to binary directory </span>
    <span class="s">(user=%user.name container=%container.name file=%fd.name)</span>
  <span class="na">priority</span><span class="pi">:</span> <span class="s">CRITICAL</span>
  <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">container</span><span class="pi">,</span> <span class="nv">filesystem</span><span class="pi">,</span> <span class="nv">mitre_persistence</span><span class="pi">]</span>

<span class="pi">-</span> <span class="na">rule</span><span class="pi">:</span> <span class="s">Outbound Connection to Unusual Port</span>
  <span class="na">desc</span><span class="pi">:</span> <span class="s">Detect outbound connections to non-standard ports</span>
  <span class="na">condition</span><span class="pi">:</span> <span class="pi">&gt;</span>
    <span class="s">outbound and </span>
    <span class="s">container and </span>
    <span class="s">not (fd.sport in (80, 443, 53, 8080, 8443, 5432, 6379, 3306))</span>
  <span class="na">output</span><span class="pi">:</span> <span class="pi">&gt;</span>
    <span class="s">Unusual outbound connection </span>
    <span class="s">(container=%container.name connection=%fd.name)</span>
  <span class="na">priority</span><span class="pi">:</span> <span class="s">NOTICE</span>
  <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">container</span><span class="pi">,</span> <span class="nv">network</span><span class="pi">,</span> <span class="nv">mitre_exfiltration</span><span class="pi">]</span>
</code></pre></div></div>

<p>Deploy Falco in Kubernetes:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">DaemonSet</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">falco</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">falco</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">falco</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">falco</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">serviceAccountName</span><span class="pi">:</span> <span class="s">falco</span>
      <span class="na">hostNetwork</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">hostPID</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">containers</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">falco</span>
          <span class="na">image</span><span class="pi">:</span> <span class="s">falcosecurity/falco:latest</span>
          <span class="na">securityContext</span><span class="pi">:</span>
            <span class="na">privileged</span><span class="pi">:</span> <span class="kc">true</span>
          <span class="na">volumeMounts</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">dev</span>
              <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/host/dev</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">proc</span>
              <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/host/proc</span>
              <span class="na">readOnly</span><span class="pi">:</span> <span class="kc">true</span>
            <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">etc</span>
              <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/host/etc</span>
              <span class="na">readOnly</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">volumes</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">dev</span>
          <span class="na">hostPath</span><span class="pi">:</span>
            <span class="na">path</span><span class="pi">:</span> <span class="s">/dev</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">proc</span>
          <span class="na">hostPath</span><span class="pi">:</span>
            <span class="na">path</span><span class="pi">:</span> <span class="s">/proc</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">etc</span>
          <span class="na">hostPath</span><span class="pi">:</span>
            <span class="na">path</span><span class="pi">:</span> <span class="s">/etc</span>
</code></pre></div></div>

<h2 id="secret-management">Secret Management</h2>

<p>Secrets in containers require careful handling. Environment variables, while convenient, can leak through logs, process listings, or debug endpoints. Build-time secrets must never persist in image layers. Runtime secrets should be injected securely and rotated regularly.</p>

<h3 id="build-time-secrets">Build-Time Secrets</h3>

<p>Docker BuildKit provides secure secret mounting during builds. Secrets are available only during the build step that mounts them and don’t persist in image layers.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># syntax=docker/dockerfile:1.4</span>
<span class="k">FROM</span><span class="s"> node:20-alpine</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> package*.json ./</span>

<span class="c"># Mount npm token as secret - only available during this RUN</span>
<span class="k">RUN </span><span class="nt">--mount</span><span class="o">=</span><span class="nb">type</span><span class="o">=</span>secret,id<span class="o">=</span>npm_token <span class="se">\
</span>    <span class="nv">NPM_TOKEN</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat</span> /run/secrets/npm_token<span class="si">)</span> <span class="se">\
</span>    <span class="nb">echo</span> <span class="s2">"//registry.npmjs.org/:_authToken=</span><span class="k">${</span><span class="nv">NPM_TOKEN</span><span class="k">}</span><span class="s2">"</span> <span class="o">&gt;</span> .npmrc <span class="o">&amp;&amp;</span> <span class="se">\
</span>    npm ci <span class="o">&amp;&amp;</span> <span class="se">\
</span>    <span class="nb">rm</span> .npmrc

<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">RUN </span>npm run build

<span class="k">CMD</span><span class="s"> ["node", "dist/server.js"]</span>
</code></pre></div></div>

<p>Build with secrets:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Pass secret from environment variable</span>
<span class="nb">export </span><span class="nv">NPM_TOKEN</span><span class="o">=</span><span class="s2">"your-token-here"</span>
docker buildx build <span class="nt">--secret</span> <span class="nb">id</span><span class="o">=</span>npm_token,env<span class="o">=</span>NPM_TOKEN <span class="nt">-t</span> myapp:latest <span class="nb">.</span>

<span class="c"># Pass secret from file</span>
<span class="nb">echo</span> <span class="s2">"your-token-here"</span> <span class="o">&gt;</span> npm_token.txt
docker buildx build <span class="nt">--secret</span> <span class="nb">id</span><span class="o">=</span>npm_token,src<span class="o">=</span>npm_token.txt <span class="nt">-t</span> myapp:latest <span class="nb">.</span>
</code></pre></div></div>

<p>For SSH access to private repositories:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># syntax=docker/dockerfile:1.4</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">golang:1.21</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="c"># Mount SSH agent for private repo access</span>
<span class="k">RUN </span><span class="nt">--mount</span><span class="o">=</span><span class="nb">type</span><span class="o">=</span>ssh <span class="se">\
</span>    <span class="nb">mkdir</span> <span class="nt">-p</span> ~/.ssh <span class="o">&amp;&amp;</span> <span class="se">\
</span>    ssh-keyscan github.com <span class="o">&gt;&gt;</span> ~/.ssh/known_hosts <span class="o">&amp;&amp;</span> <span class="se">\
</span>    go mod download

<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">RUN </span>go build <span class="nt">-o</span> myapp

<span class="k">FROM</span><span class="s"> gcr.io/distroless/static</span>
<span class="k">COPY</span><span class="s"> --from=builder /app/myapp /</span>
<span class="k">CMD</span><span class="s"> ["/myapp"]</span>
</code></pre></div></div>

<p>Build with SSH forwarding:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker buildx build <span class="nt">--ssh</span> default <span class="nt">-t</span> myapp:latest <span class="nb">.</span>
</code></pre></div></div>

<h3 id="runtime-secrets">Runtime Secrets</h3>

<p>For runtime secrets, avoid environment variables where possible. Use mounted files or integrate with secret management systems.</p>

<p><strong>Docker Secrets (Swarm)</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Create a secret</span>
<span class="nb">echo</span> <span class="s2">"supersecretpassword"</span> | docker secret create db_password -

<span class="c"># Use in service</span>
docker service create <span class="se">\</span>
  <span class="nt">--name</span> api <span class="se">\</span>
  <span class="nt">--secret</span> db_password <span class="se">\</span>
  myapp:latest
</code></pre></div></div>

<p>Inside the container, the secret appears at <code class="language-plaintext highlighter-rouge">/run/secrets/db_password</code>.</p>

<p><strong>Kubernetes Secrets</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Secret</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">app-secrets</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">Opaque</span>
<span class="na">stringData</span><span class="pi">:</span>
  <span class="na">database-password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">supersecretpassword"</span>
  <span class="na">api-key</span><span class="pi">:</span> <span class="s2">"</span><span class="s">sk-1234567890"</span>
<span class="nn">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Pod</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">containers</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">app</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">myapp:latest</span>
      <span class="na">volumeMounts</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">secrets</span>
          <span class="na">mountPath</span><span class="pi">:</span> <span class="s">/etc/secrets</span>
          <span class="na">readOnly</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">volumes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">secrets</span>
      <span class="na">secret</span><span class="pi">:</span>
        <span class="na">secretName</span><span class="pi">:</span> <span class="s">app-secrets</span>
</code></pre></div></div>

<p><strong>External Secret Stores</strong></p>

<p>For production, integrate with external secret stores like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Using External Secrets Operator with AWS Secrets Manager</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">external-secrets.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ExternalSecret</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">app-secrets</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">refreshInterval</span><span class="pi">:</span> <span class="s">1h</span>
  <span class="na">secretStoreRef</span><span class="pi">:</span>
    <span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterSecretStore</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">aws-secrets-manager</span>
  <span class="na">target</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">app-secrets</span>
  <span class="na">data</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">secretKey</span><span class="pi">:</span> <span class="s">database-password</span>
      <span class="na">remoteRef</span><span class="pi">:</span>
        <span class="na">key</span><span class="pi">:</span> <span class="s">production/database</span>
        <span class="na">property</span><span class="pi">:</span> <span class="s">password</span>
    <span class="pi">-</span> <span class="na">secretKey</span><span class="pi">:</span> <span class="s">api-key</span>
      <span class="na">remoteRef</span><span class="pi">:</span>
        <span class="na">key</span><span class="pi">:</span> <span class="s">production/api</span>
        <span class="na">property</span><span class="pi">:</span> <span class="s">key</span>
</code></pre></div></div>

<h2 id="cicd-security-integration">CI/CD Security Integration</h2>

<p>Security must be automated into your CI/CD pipeline. Manual security reviews don’t scale, and security gates that slow down deployment get bypassed. Build security into the pipeline so it happens automatically on every build.</p>

<h3 id="comprehensive-security-pipeline">Comprehensive Security Pipeline</h3>

<p>Here’s a GitHub Actions workflow that integrates vulnerability scanning, SBOM generation, and image signing:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Secure Container Build</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">main</span><span class="pi">]</span>
    <span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="s1">'</span><span class="s">v*'</span><span class="pi">]</span>
  <span class="na">pull_request</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">main</span><span class="pi">]</span>

<span class="na">env</span><span class="pi">:</span>
  <span class="na">REGISTRY</span><span class="pi">:</span> <span class="s">ghcr.io</span>
  <span class="na">IMAGE_NAME</span><span class="pi">:</span> <span class="s">$</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">security-scan</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">security-events</span><span class="pi">:</span> <span class="s">write</span>
    
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run Trivy vulnerability scanner (filesystem)</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">aquasecurity/trivy-action@master</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">scan-type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">fs'</span>
          <span class="na">scan-ref</span><span class="pi">:</span> <span class="s1">'</span><span class="s">.'</span>
          <span class="na">format</span><span class="pi">:</span> <span class="s1">'</span><span class="s">sarif'</span>
          <span class="na">output</span><span class="pi">:</span> <span class="s1">'</span><span class="s">trivy-fs-results.sarif'</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload Trivy scan results to GitHub Security</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">github/codeql-action/upload-sarif@v2</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">sarif_file</span><span class="pi">:</span> <span class="s1">'</span><span class="s">trivy-fs-results.sarif'</span>

  <span class="na">build-and-push</span><span class="pi">:</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">security-scan</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
      <span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>  <span class="c1"># For keyless signing</span>
      <span class="na">security-events</span><span class="pi">:</span> <span class="s">write</span>
    
    <span class="na">outputs</span><span class="pi">:</span>
      <span class="na">digest</span><span class="pi">:</span> <span class="s">$</span>
    
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Docker Buildx</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Container Registry</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name != 'pull_request'</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Extract metadata</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">meta</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/metadata-action@v5</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">images</span><span class="pi">:</span> <span class="s">$/$</span>
          <span class="na">tags</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">type=ref,event=branch</span>
            <span class="s">type=ref,event=pr</span>
            <span class="s">type=semver,pattern=</span>
            <span class="s">type=semver,pattern=.</span>
            <span class="s">type=sha</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build and push</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">build</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/build-push-action@v5</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
          <span class="na">push</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">tags</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">labels</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">cache-from</span><span class="pi">:</span> <span class="s">type=gha</span>
          <span class="na">cache-to</span><span class="pi">:</span> <span class="s">type=gha,mode=max</span>
          <span class="na">provenance</span><span class="pi">:</span> <span class="kc">true</span>
          <span class="na">sbom</span><span class="pi">:</span> <span class="kc">true</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run Trivy vulnerability scanner (image)</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">aquasecurity/trivy-action@master</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">image-ref</span><span class="pi">:</span> <span class="s">$/$:$</span>
          <span class="na">format</span><span class="pi">:</span> <span class="s1">'</span><span class="s">sarif'</span>
          <span class="na">output</span><span class="pi">:</span> <span class="s1">'</span><span class="s">trivy-image-results.sarif'</span>
          <span class="na">severity</span><span class="pi">:</span> <span class="s1">'</span><span class="s">CRITICAL,HIGH'</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name != 'pull_request'</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload Trivy scan results</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">github/codeql-action/upload-sarif@v2</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">sarif_file</span><span class="pi">:</span> <span class="s1">'</span><span class="s">trivy-image-results.sarif'</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name != 'pull_request'</span>

  <span class="na">sign-image</span><span class="pi">:</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">build-and-push</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name != 'pull_request'</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
      <span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>
    
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install Cosign</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">sigstore/cosign-installer@v3</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Container Registry</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Sign the image with Cosign</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">DIGEST</span><span class="pi">:</span> <span class="s">$</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">cosign sign --yes $/$@${DIGEST}</span>

  <span class="na">generate-sbom</span><span class="pi">:</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">build-and-push</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name != 'pull_request'</span>
    <span class="na">permissions</span><span class="pi">:</span>
      <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
      <span class="na">packages</span><span class="pi">:</span> <span class="s">write</span>
    
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install Syft</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">anchore/sbom-action/download-syft@v0</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Container Registry</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/login-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">registry</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">username</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">password</span><span class="pi">:</span> <span class="s">$</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Generate SBOM</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">DIGEST</span><span class="pi">:</span> <span class="s">$</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">syft $/$@${DIGEST} -o spdx-json &gt; sbom.spdx.json</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload SBOM as artifact</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-artifact@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">name</span><span class="pi">:</span> <span class="s">sbom</span>
          <span class="na">path</span><span class="pi">:</span> <span class="s">sbom.spdx.json</span>
</code></pre></div></div>

<h3 id="security-gates">Security Gates</h3>

<p>Define security thresholds that must be met before images can be deployed:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Add to your build job</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check vulnerability threshold</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">aquasecurity/trivy-action@master</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">image-ref</span><span class="pi">:</span> <span class="s">$/$:$</span>
    <span class="na">format</span><span class="pi">:</span> <span class="s1">'</span><span class="s">table'</span>
    <span class="na">exit-code</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1'</span>  <span class="c1"># Fail the build</span>
    <span class="na">severity</span><span class="pi">:</span> <span class="s1">'</span><span class="s">CRITICAL'</span>  <span class="c1"># Only fail on critical vulnerabilities</span>
    <span class="na">ignore-unfixed</span><span class="pi">:</span> <span class="kc">true</span>  <span class="c1"># Ignore vulnerabilities without fixes</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Verify image signature before deployment</span>
  <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">cosign verify \</span>
      <span class="s">--certificate-identity-regexp="https://github.com/$/*" \</span>
      <span class="s">--certificate-oidc-issuer=https://token.actions.githubusercontent.com \</span>
      <span class="s">$/$@$</span>
</code></pre></div></div>

<h3 id="admission-control">Admission Control</h3>

<p>In Kubernetes, use admission controllers to enforce security policies. Kyverno and OPA Gatekeeper can verify image signatures, enforce security contexts, and block non-compliant deployments.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Kyverno policy requiring signed images</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">kyverno.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterPolicy</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">require-signed-images</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">validationFailureAction</span><span class="pi">:</span> <span class="s">Enforce</span>
  <span class="na">background</span><span class="pi">:</span> <span class="kc">false</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">verify-signature</span>
      <span class="na">match</span><span class="pi">:</span>
        <span class="na">any</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">resources</span><span class="pi">:</span>
              <span class="na">kinds</span><span class="pi">:</span>
                <span class="pi">-</span> <span class="s">Pod</span>
      <span class="na">verifyImages</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">imageReferences</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s2">"</span><span class="s">ghcr.io/myorg/*"</span>
          <span class="na">attestors</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">entries</span><span class="pi">:</span>
                <span class="pi">-</span> <span class="na">keyless</span><span class="pi">:</span>
                    <span class="na">issuer</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://token.actions.githubusercontent.com"</span>
                    <span class="na">subject</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://github.com/myorg/*"</span>
<span class="nn">---</span>
<span class="c1"># Policy enforcing security contexts</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">kyverno.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterPolicy</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">require-security-context</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">validationFailureAction</span><span class="pi">:</span> <span class="s">Enforce</span>
  <span class="na">rules</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">require-non-root</span>
      <span class="na">match</span><span class="pi">:</span>
        <span class="na">any</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">resources</span><span class="pi">:</span>
              <span class="na">kinds</span><span class="pi">:</span>
                <span class="pi">-</span> <span class="s">Pod</span>
      <span class="na">validate</span><span class="pi">:</span>
        <span class="na">message</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Containers</span><span class="nv"> </span><span class="s">must</span><span class="nv"> </span><span class="s">run</span><span class="nv"> </span><span class="s">as</span><span class="nv"> </span><span class="s">non-root"</span>
        <span class="na">pattern</span><span class="pi">:</span>
          <span class="na">spec</span><span class="pi">:</span>
            <span class="na">securityContext</span><span class="pi">:</span>
              <span class="na">runAsNonRoot</span><span class="pi">:</span> <span class="kc">true</span>
            <span class="na">containers</span><span class="pi">:</span>
              <span class="pi">-</span> <span class="na">securityContext</span><span class="pi">:</span>
                  <span class="na">allowPrivilegeEscalation</span><span class="pi">:</span> <span class="kc">false</span>
                  <span class="na">capabilities</span><span class="pi">:</span>
                    <span class="na">drop</span><span class="pi">:</span>
                      <span class="pi">-</span> <span class="s">ALL</span>
</code></pre></div></div>

<h2 id="security-checklist">Security Checklist</h2>

<p>Here’s a comprehensive checklist for container security. Use it as a starting point and adapt it to your specific requirements:</p>

<h3 id="image-security-1">Image Security</h3>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Use minimal base images (distroless, Alpine, or scratch)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Pin base image versions with digests, not just tags</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Scan images for vulnerabilities in CI/CD</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Generate and store SBOMs for all images</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Sign images with Cosign or similar</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Use multi-stage builds to exclude build tools</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Remove unnecessary packages and files</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Don’t install debugging tools in production images</li>
</ul>

<h3 id="runtime-security-1">Runtime Security</h3>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Run containers as non-root users</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Drop all capabilities and add only what’s needed</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Use read-only root filesystems</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Set resource limits (memory, CPU, PIDs)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Apply seccomp profiles</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Disable privilege escalation</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Use network policies to restrict communication</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Deploy runtime threat detection (Falco)</li>
</ul>

<h3 id="secret-management-1">Secret Management</h3>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Never commit secrets to version control</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Use BuildKit secrets for build-time credentials</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Mount secrets as files, not environment variables</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Integrate with external secret stores</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Rotate secrets regularly</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Audit secret access</li>
</ul>

<h3 id="cicd-security">CI/CD Security</h3>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Scan code and dependencies before building</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Scan images after building</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Sign images in CI/CD pipeline</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Verify signatures before deployment</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Use admission controllers to enforce policies</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Implement security gates with severity thresholds</li>
</ul>

<h3 id="infrastructure-security">Infrastructure Security</h3>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Keep container runtime updated</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Keep orchestration platform updated</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Use private registries with authentication</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Enable audit logging</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Monitor and alert on security events</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Regularly review and update security policies</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Container security isn’t a single tool or practice—it’s a comprehensive approach that spans the entire lifecycle from base image selection through runtime monitoring. The shared kernel model means that container isolation, while effective, is not equivalent to the stronger isolation of virtual machines. Defence in depth is essential.</p>

<p>Start with the fundamentals: understand namespaces, cgroups, and capabilities. Choose minimal base images that reduce attack surface. Scan for vulnerabilities and generate SBOMs so you can respond quickly when new CVEs emerge. Run as non-root with minimal capabilities. Apply network policies to limit blast radius.</p>

<p>Automate security into your CI/CD pipeline so it happens consistently on every build. Sign your images to prove provenance. Use admission controllers to enforce policies at deployment time. Deploy runtime security tools to detect attacks that static analysis can’t catch.</p>

<p>Container security is an ongoing practice, not a one-time implementation. Vulnerabilities are discovered daily. Attackers evolve their techniques. Your security posture must evolve with them. Regular audits, updated policies, and continuous monitoring ensure your containerised applications remain protected as the threat landscape changes.</p>

<p>The investment in container security pays dividends in reduced risk, faster incident response, and confidence that your applications are protected by multiple layers of defence. You’ve built a foundation for secure containerised applications that will serve you well as your deployment grows.</p>]]></content><author><name>Glen Thomas</name></author><category term="Platform Engineering" /><category term="Software Engineering" /><category term="Docker" /><category term="Container Images" /><category term="Security" /><category term="Kubernetes" /><category term="DevSecOps" /><summary type="html"><![CDATA[Containers have fundamentally changed how we build and deploy applications, but they’ve also introduced new security considerations that many teams struggle to address comprehensively. The speed and convenience of containers can lull organisations into a false sense of security, treating them as lightweight virtual machines without understanding the fundamentally different security model they represent.]]></summary></entry><entry><title type="html">Building a Hub and Spoke Network Topology in Azure: A Comprehensive Guide</title><link href="https://blog.glen-thomas.com/platform%20engineering/2025/10/15/building-a-hub-and-spoke-network-topology-in-azure.html" rel="alternate" type="text/html" title="Building a Hub and Spoke Network Topology in Azure: A Comprehensive Guide" /><published>2025-10-15T22:51:00+01:00</published><updated>2025-10-15T22:51:00+01:00</updated><id>https://blog.glen-thomas.com/platform%20engineering/2025/10/15/building-a-hub-and-spoke-network-topology-in-azure</id><content type="html" xml:base="https://blog.glen-thomas.com/platform%20engineering/2025/10/15/building-a-hub-and-spoke-network-topology-in-azure.html"><![CDATA[<p>Network architecture is one of those foundational decisions that shapes everything you build on top of it. Get it right at the start, and you’ll have a scalable, secure platform that grows with your organisation. Get it wrong, and you’ll spend years fighting technical debt, security vulnerabilities, and operational complexity that compounds with every new workload you deploy.</p>

<p>For Azure-based platforms, the hub and spoke topology has emerged as the de facto standard for enterprise network design, and for good reason. It provides the perfect balance between isolation and connectivity, centralises security controls without creating bottlenecks, and scales elegantly as your organisation grows from a handful of applications to hundreds of workloads across multiple regions.</p>

<p>In this comprehensive guide, I’ll walk you through building a production-ready hub and spoke network topology in Azure using Terraform. We’ll explore the architectural principles that make this pattern so effective, implement the infrastructure with security best practices embedded at every layer, and address the operational considerations that separate toy examples from production-ready platforms.</p>

<h2 id="understanding-the-hub-and-spoke-pattern">Understanding the Hub and Spoke Pattern</h2>

<p>Before we write a single line of Terraform, it’s worth understanding why this architectural pattern exists and what problems it solves. The hub and spoke topology isn’t just about drawing circles and lines on architecture diagrams, it’s a deliberate approach to managing complexity whilst maintaining security and operational efficiency.</p>

<h3 id="the-problem-with-flat-networks">The Problem with Flat Networks</h3>

<p>In the early days of cloud adoption, many organisations built flat network topologies where every virtual network could communicate with every other virtual network through full-mesh peering. For three or four VNets, this works fine. By the time you reach ten VNets, you’re managing 45 peering connections. At twenty VNets, that number balloons to 190 peering connections.</p>

<p>Beyond the sheer management overhead, flat topologies create security challenges. Every VNet becomes a potential attack vector for every other VNet. Network security groups become impossibly complex as you try to maintain granular control over which applications can communicate. Audit trails become muddled as traffic flows directly between workloads without passing through central inspection points.</p>

<h3 id="the-hub-and-spoke-solution">The Hub and Spoke Solution</h3>

<p>The hub and spoke pattern solves these problems through centralisation. Instead of every VNet peering with every other VNet, spoke VNets peer only with a central hub VNet. The hub contains shared services; firewalls, VPN gateways, DNS servers, monitoring infrastructure, that all spokes can access. Traffic between spokes flows through the hub, where it can be inspected, logged, and controlled.</p>

<p>This topology dramatically reduces complexity. Twenty spoke VNets require only twenty peering connections to the hub, rather than 190 in a mesh topology. Security controls centralise in the hub, making them easier to maintain and audit. Network changes in one spoke don’t affect others unless you explicitly allow it.</p>

<p>The pattern also aligns beautifully with organisational structure. Each spoke can represent a different team, application, or environment. Development teams get their own isolated network space whilst platform engineering maintains central control over security policy, connectivity to on-premises networks, and shared infrastructure.</p>

<h3 id="when-to-use-this-pattern">When to Use This Pattern</h3>

<p>Hub and spoke topology makes sense when you have multiple workloads that need some degree of isolation from each other but also require shared services or connectivity to external networks. This describes most enterprise scenarios.</p>

<p>If you’re building a simple proof of concept with a single application in a single VNet, hub and spoke is overkill. But the moment you start thinking about multiple environments, multiple teams, or connections to on-premises networks, this pattern starts paying dividends. The initial investment in building the hub pays off quickly as you add spokes without increasing complexity.</p>

<h2 id="architectural-design-decisions">Architectural Design Decisions</h2>

<p>Every hub and spoke implementation requires making deliberate choices about how to structure the network, secure traffic, and handle connectivity. These decisions shape everything from IP address allocation to firewall rules to routing configuration.</p>

<h3 id="ip-address-planning">IP Address Planning</h3>

<p>IP address planning is one of those tasks that feels tedious until you get it wrong, at which point it becomes a nightmare. Once workloads are deployed and using IP addresses, changing them requires downtime, coordination, and significant risk.</p>

<p>The cardinal rule is to be generous with address space allocation. Azure supports RFC 1918 private address ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), and there’s no cost to reserving large blocks. I recommend allocating a /16 (65,536 addresses) for the hub and at least a /20 (4,096 addresses) for each spoke, with room to grow.</p>

<p>Within the hub, subdivide address space by function. Allocate a subnet for Azure Firewall, another for VPN Gateway, another for Azure Bastion, and so on. Each Azure service has specific subnet sizing requirements; Azure Firewall needs at least a /26, VPN Gateway needs at least a /27, Azure Bastion needs at least a /26. Factor in future growth when sizing these subnets.</p>

<p>For spoke VNets, organise subnets by tier or function. Web tier gets a subnet, application tier gets another, data tier gets a third. Keep subnet sizes consistent across spokes where possible—it makes automation easier and helps with capacity planning.</p>

<p>Document your IP address allocation scheme from the start. Future you, six months from now, will thank present you for writing down which address ranges are allocated to which environments and teams.</p>

<h3 id="hub-vnet-design">Hub VNet Design</h3>

<p>The hub VNet serves as the central point of connectivity for your entire network topology. It needs to accommodate several distinct functions, each with its own subnet requirements.</p>

<p>Azure Firewall sits at the heart of the hub, inspecting all traffic between spokes and controlling outbound internet access. It requires a dedicated subnet named <code class="language-plaintext highlighter-rouge">AzureFirewallSubnet</code> with a minimum size of /26, though /24 gives you more room for scale.</p>

<p>If you’re connecting to on-premises networks via VPN or ExpressRoute, you’ll need a Gateway subnet. VPN Gateway and ExpressRoute Gateway both require a subnet named <code class="language-plaintext highlighter-rouge">GatewaySubnet</code> with a minimum size of /27, though /26 is recommended for production deployments.</p>

<p>Azure Bastion provides secure RDP and SSH access to virtual machines without exposing them to the public internet. It needs a subnet named <code class="language-plaintext highlighter-rouge">AzureBastionSubnet</code> with a minimum size of /26.</p>

<p>For shared services like jump boxes, monitoring infrastructure, or centralised DNS servers, create additional subnets sized appropriately for your needs. A /27 or /28 often suffices for these management workloads.</p>

<h3 id="spoke-vnet-design">Spoke VNet Design</h3>

<p>Spoke VNets contain your actual workloads; the applications, databases, and services that deliver value to your organisation. Design them with isolation and security in mind.</p>

<p>Each spoke should map to a clear organisational or technical boundary. You might create spokes per environment (development, staging, production), per team (platform, data, mobile), per application tier (frontend, backend, data), or some combination. The key is consistency; establish a pattern and stick to it.</p>

<p>Within each spoke, use subnets to create security boundaries. Don’t put web servers and databases in the same subnet. Subnet boundaries allow you to apply network security groups that control which traffic is allowed. A compromised web server in its own subnet can be prevented from accessing database servers in a different subnet.</p>

<p>Consider creating separate subnets for private endpoints if you’re using Azure PaaS services like Storage Accounts or SQL Databases with private connectivity. This makes it easier to manage DNS resolution and network security group rules for these endpoints.</p>

<h3 id="routing-architecture">Routing Architecture</h3>

<p>In a hub and spoke topology, routing determines how traffic flows between spokes. By default, spoke-to-spoke traffic doesn’t work even with peering configured; Azure VNet peering is non-transitive. If Spoke A peers with the Hub, and Spoke B peers with the Hub, traffic from Spoke A cannot reach Spoke B without additional configuration.</p>

<p>This is actually a security feature. It means you explicitly control which spokes can communicate, rather than allowing all-to-all communication by default.</p>

<p>To enable spoke-to-spoke communication, you have two options. The simpler approach uses Azure Firewall as a router. Configure user-defined routes (UDRs) in each spoke that send traffic destined for other spoke address ranges to the Azure Firewall. The firewall inspects the traffic and forwards it to the destination spoke.</p>

<p>The alternative approach deploys a network virtual appliance (NVA) in the hub for routing, but for most scenarios, Azure Firewall provides sufficient routing capabilities alongside its security functions. Using a single service for both simplifies management and reduces cost.</p>

<h3 id="connectivity-options">Connectivity Options</h3>

<p>Your hub and spoke topology likely needs connectivity beyond Azure. On-premises datacentres, remote offices, and mobile workers all need secure access to cloud resources.</p>

<p>For site-to-site connectivity to on-premises networks, you’ll choose between VPN Gateway and ExpressRoute. VPN Gateway provides encrypted IPsec tunnels over the public internet and supports throughput up to 10 Gbps with the VpnGw5 SKU. It’s cost-effective and suitable for most scenarios.</p>

<p>ExpressRoute provides private connectivity that doesn’t traverse the public internet, with dedicated bandwidth from 50 Mbps to 100 Gbps. It costs more but provides better performance, reliability, and security for mission-critical workloads. Many organisations use both; ExpressRoute for primary connectivity and VPN Gateway as a failover path.</p>

<p>For remote user access, Azure Bastion provides secure RDP and SSH connectivity without exposing virtual machines to the internet. It eliminates the need for jump boxes with public IP addresses and provides audit logging of all remote access sessions.</p>

<h3 id="visual-architecture">Visual Architecture</h3>

<p>To help visualise how all these components fit together, here’s a diagram showing a typical hub and spoke topology with three spoke VNets:</p>

<div class="mermaid-container" style="background: white; padding: 20px; border-radius: 8px; margin: 2em 0; cursor: pointer;" onclick="this.requestFullscreen()">
<pre class="mermaid">
graph TB
    subgraph Internet["Internet"]
        OnPrem["On-Premises Network<br />192.168.0.0/16"]
        Users["Remote Users"]
    end

    subgraph Hub["Hub VNet<br />10.0.0.0/16"]
        subgraph FirewallSubnet["AzureFirewallSubnet<br />10.0.0.0/24"]
            AFW["Azure Firewall<br />10.0.0.4"]
        end
        
        subgraph GatewaySubnet["GatewaySubnet<br />10.0.1.0/24"]
            VPN["VPN Gateway"]
        end
        
        subgraph BastionSubnet["AzureBastionSubnet<br />10.0.2.0/24"]
            Bastion["Azure Bastion"]
        end
        
        subgraph ManagementSubnet["Management Subnet<br />10.0.3.0/24"]
            Monitor["Monitoring<br />Infrastructure"]
        end
    end

    subgraph Spoke1["Production Spoke<br />10.1.0.0/16"]
        subgraph WebSubnet1["Web Subnet<br />10.1.1.0/24"]
            Web1["Web Servers"]
        end
        subgraph AppSubnet1["App Subnet<br />10.1.2.0/24"]
            App1["Application<br />Servers"]
        end
        subgraph DataSubnet1["Data Subnet<br />10.1.3.0/24"]
            DB1["Databases"]
        end
        RT1["Route Table"]
    end

    subgraph Spoke2["Development Spoke<br />10.2.0.0/16"]
        subgraph WorkloadSubnet2["Workload Subnet<br />10.2.1.0/24"]
            Workload2["Development<br />Workloads"]
        end
        RT2["Route Table"]
    end

    subgraph Spoke3["Shared Services Spoke<br />10.3.0.0/16"]
        subgraph ServicesSubnet3["Services Subnet<br />10.3.1.0/24"]
            DNS["DNS Servers"]
            AD["Active Directory"]
        end
        RT3["Route Table"]
    end

    OnPrem -.-&gt;|"VPN Tunnel<br />IPsec"| VPN
    Users -.-&gt;|"RDP/SSH"| Bastion
    
    VPN --&gt;|"Peering"| AFW
    Bastion -.-&gt;|"Secure Access"| Web1
    Bastion -.-&gt;|"Secure Access"| Workload2
    
    AFW &lt;--&gt;|"VNet Peering<br />Allow Gateway Transit"| Spoke1
    AFW &lt;--&gt;|"VNet Peering<br />Allow Gateway Transit"| Spoke2
    AFW &lt;--&gt;|"VNet Peering<br />Allow Gateway Transit"| Spoke3
    
    RT1 -.-&gt;|"0.0.0.0/0 → Firewall<br />10.2.0.0/16 → Firewall<br />10.3.0.0/16 → Firewall"| AFW
    RT2 -.-&gt;|"0.0.0.0/0 → Firewall<br />10.1.0.0/16 → Firewall<br />10.3.0.0/16 → Firewall"| AFW
    RT3 -.-&gt;|"0.0.0.0/0 → Firewall<br />10.1.0.0/16 → Firewall<br />10.2.0.0/16 → Firewall"| AFW
    
    Web1 --&gt;|"App Traffic"| App1
    App1 --&gt;|"Database<br />Queries"| DB1
    
    Workload2 -.-&gt;|"Via Firewall"| DNS
    App1 -.-&gt;|"Via Firewall"| DNS
    
    AFW --&gt;|"Inspect &amp; Log"| Internet
    
    Monitor -.-&gt;|"Collect Logs"| AFW
    Monitor -.-&gt;|"Collect Logs"| VPN
    Monitor -.-&gt;|"Collect Logs"| Bastion

    classDef hubStyle fill:#0078d4,stroke:#003d7a,stroke-width:2px,color:#fff
    classDef spokeStyle fill:#50e6ff,stroke:#0078d4,stroke-width:2px,color:#000
    classDef securityStyle fill:#ff6b6b,stroke:#c92a2a,stroke-width:2px,color:#fff
    classDef gatewayStyle fill:#69db7c,stroke:#2b8a3e,stroke-width:2px,color:#000
    
    class AFW securityStyle
    class VPN,Bastion gatewayStyle
    class Spoke1,Spoke2,Spoke3 spokeStyle
```
</pre>
</div>

<p style="text-align: center; font-size: 0.9em; color: #666; margin-top: -1em;"><em>Click the diagram to view fullscreen</em></p>

<p>This diagram illustrates several key architectural points. The hub sits at the centre with four distinct subnets, each serving a specific purpose. Azure Firewall acts as the central inspection point for all inter-spoke traffic and internet-bound traffic. VPN Gateway provides the encrypted tunnel to on-premises networks, whilst Azure Bastion offers secure administrative access without exposing VMs to the internet.</p>

<p>Each spoke VNet peers directly with the hub but not with other spokes. The route tables in each spoke contain user-defined routes that send traffic destined for other spokes through the Azure Firewall’s private IP address. This ensures all spoke-to-spoke communication flows through the firewall where it can be inspected, logged, and controlled by centralised firewall policies.</p>

<p>Notice how the production spoke uses a traditional three-tier subnet design (web, application, data), whilst the development spoke has a simpler structure with a single workload subnet. The shared services spoke contains infrastructure used by multiple teams, like DNS servers and Active Directory. This flexibility in spoke design is one of the pattern’s strengths—each spoke can be structured according to its specific requirements whilst maintaining consistent connectivity and security through the hub.</p>

<h2 id="infrastructure-as-code-with-terraform">Infrastructure as Code with Terraform</h2>

<p>Now let’s translate these architectural principles into actual infrastructure. We’ll build this incrementally, starting with the hub and progressively adding spokes and connectivity.</p>

<h3 id="foundation-and-variables">Foundation and Variables</h3>

<p>Start by defining the variables that will drive your configuration. Create <code class="language-plaintext highlighter-rouge">variables.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"environment"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Environment name (e.g., prod, staging, dev)"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"location"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Primary Azure region"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="s2">"uksouth"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"organisation"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Organisation name for resource naming"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"hub_vnet_address_space"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Address space for hub VNet"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="p">[</span><span class="s2">"10.0.0.0/16"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"spoke_vnets"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Map of spoke VNets to create"</span>
  <span class="nx">type</span> <span class="o">=</span> <span class="nx">map</span><span class="p">(</span><span class="nx">object</span><span class="p">({</span>
    <span class="nx">address_space</span> <span class="o">=</span> <span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
    <span class="nx">subnets</span> <span class="o">=</span> <span class="nx">map</span><span class="p">(</span><span class="nx">object</span><span class="p">({</span>
      <span class="nx">address_prefix</span> <span class="o">=</span> <span class="nx">string</span>
      <span class="nx">service_endpoints</span> <span class="o">=</span> <span class="nx">optional</span><span class="p">(</span><span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">),</span> <span class="p">[])</span>
      <span class="nx">delegation</span> <span class="o">=</span> <span class="nx">optional</span><span class="p">(</span><span class="nx">object</span><span class="p">({</span>
        <span class="nx">name</span> <span class="o">=</span> <span class="nx">string</span>
        <span class="nx">service_delegation</span> <span class="o">=</span> <span class="nx">object</span><span class="p">({</span>
          <span class="nx">name</span>    <span class="o">=</span> <span class="nx">string</span>
          <span class="nx">actions</span> <span class="o">=</span> <span class="nx">optional</span><span class="p">(</span><span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">),</span> <span class="p">[])</span>
        <span class="p">})</span>
      <span class="p">}))</span>
    <span class="p">}))</span>
  <span class="p">}))</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"enable_vpn_gateway"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Whether to deploy VPN Gateway in hub"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">bool</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="kc">true</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"enable_bastion"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Whether to deploy Azure Bastion in hub"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">bool</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="kc">true</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"on_premises_address_spaces"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Address spaces of on-premises networks for VPN"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="p">[]</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"tags"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Tags to apply to all resources"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">map</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="p">{}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Create <code class="language-plaintext highlighter-rouge">locals.tf</code> for computed values:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locals</span> <span class="p">{</span>
  <span class="c1"># Common tags applied to all resources</span>
  <span class="nx">common_tags</span> <span class="o">=</span> <span class="nx">merge</span><span class="p">(</span>
    <span class="nx">var</span><span class="p">.</span><span class="nx">tags</span><span class="p">,</span>
    <span class="p">{</span>
      <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
      <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
      <span class="nx">pattern</span>     <span class="o">=</span> <span class="s2">"hub-and-spoke"</span>
    <span class="p">}</span>
  <span class="p">)</span>

  <span class="c1"># Resource naming convention</span>
  <span class="nx">hub_name</span> <span class="o">=</span> <span class="s2">"hub-${var.environment}-${var.location}"</span>
  
  <span class="c1"># Hub subnet configuration</span>
  <span class="nx">hub_subnets</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">firewall</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">name</span>           <span class="o">=</span> <span class="s2">"AzureFirewallSubnet"</span>
      <span class="nx">address_prefix</span> <span class="o">=</span> <span class="nx">cidrsubnet</span><span class="p">(</span><span class="nx">var</span><span class="p">.</span><span class="nx">hub_vnet_address_space</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nx">gateway</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">name</span>           <span class="o">=</span> <span class="s2">"GatewaySubnet"</span>
      <span class="nx">address_prefix</span> <span class="o">=</span> <span class="nx">cidrsubnet</span><span class="p">(</span><span class="nx">var</span><span class="p">.</span><span class="nx">hub_vnet_address_space</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nx">bastion</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">name</span>           <span class="o">=</span> <span class="s2">"AzureBastionSubnet"</span>
      <span class="nx">address_prefix</span> <span class="o">=</span> <span class="nx">cidrsubnet</span><span class="p">(</span><span class="nx">var</span><span class="p">.</span><span class="nx">hub_vnet_address_space</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="nx">management</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">name</span>           <span class="o">=</span> <span class="s2">"snet-management"</span>
      <span class="nx">address_prefix</span> <span class="o">=</span> <span class="nx">cidrsubnet</span><span class="p">(</span><span class="nx">var</span><span class="p">.</span><span class="nx">hub_vnet_address_space</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="hub-virtual-network">Hub Virtual Network</h3>

<p>Create the hub VNet with all its subnets. Create <code class="language-plaintext highlighter-rouge">hub.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Resource group for hub networking</span>
<span class="nx">resource</span> <span class="s2">"azurerm_resource_group"</span> <span class="s2">"hub"</span> <span class="p">{</span>
  <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"rg-network-${local.hub_name}"</span>
  <span class="nx">location</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">tags</span>     <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Hub virtual network</span>
<span class="nx">resource</span> <span class="s2">"azurerm_virtual_network"</span> <span class="s2">"hub"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"vnet-${local.hub_name}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">address_space</span>       <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">hub_vnet_address_space</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Hub subnets</span>
<span class="nx">resource</span> <span class="s2">"azurerm_subnet"</span> <span class="s2">"hub_firewall"</span> <span class="p">{</span>
  <span class="nx">name</span>                 <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">hub_subnets</span><span class="p">.</span><span class="nx">firewall</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">resource_group_name</span>  <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">virtual_network_name</span> <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">address_prefixes</span>     <span class="o">=</span> <span class="p">[</span><span class="nx">local</span><span class="p">.</span><span class="nx">hub_subnets</span><span class="p">.</span><span class="nx">firewall</span><span class="p">.</span><span class="nx">address_prefix</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_subnet"</span> <span class="s2">"hub_gateway"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_vpn_gateway</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span>

  <span class="nx">name</span>                 <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">hub_subnets</span><span class="p">.</span><span class="nx">gateway</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">resource_group_name</span>  <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">virtual_network_name</span> <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">address_prefixes</span>     <span class="o">=</span> <span class="p">[</span><span class="nx">local</span><span class="p">.</span><span class="nx">hub_subnets</span><span class="p">.</span><span class="nx">gateway</span><span class="p">.</span><span class="nx">address_prefix</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_subnet"</span> <span class="s2">"hub_bastion"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_bastion</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span>

  <span class="nx">name</span>                 <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">hub_subnets</span><span class="p">.</span><span class="nx">bastion</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">resource_group_name</span>  <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">virtual_network_name</span> <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">address_prefixes</span>     <span class="o">=</span> <span class="p">[</span><span class="nx">local</span><span class="p">.</span><span class="nx">hub_subnets</span><span class="p">.</span><span class="nx">bastion</span><span class="p">.</span><span class="nx">address_prefix</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_subnet"</span> <span class="s2">"hub_management"</span> <span class="p">{</span>
  <span class="nx">name</span>                 <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">hub_subnets</span><span class="p">.</span><span class="nx">management</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">resource_group_name</span>  <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">virtual_network_name</span> <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">address_prefixes</span>     <span class="o">=</span> <span class="p">[</span><span class="nx">local</span><span class="p">.</span><span class="nx">hub_subnets</span><span class="p">.</span><span class="nx">management</span><span class="p">.</span><span class="nx">address_prefix</span><span class="p">]</span>

  <span class="nx">service_endpoints</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"Microsoft.Storage"</span><span class="p">,</span>
    <span class="s2">"Microsoft.KeyVault"</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="c1"># Network security group for management subnet</span>
<span class="nx">resource</span> <span class="s2">"azurerm_network_security_group"</span> <span class="s2">"hub_management"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"nsg-${local.hub_name}-management"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># NSG association</span>
<span class="nx">resource</span> <span class="s2">"azurerm_subnet_network_security_group_association"</span> <span class="s2">"hub_management"</span> <span class="p">{</span>
  <span class="nx">subnet_id</span>                 <span class="o">=</span> <span class="nx">azurerm_subnet</span><span class="p">.</span><span class="nx">hub_management</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">network_security_group_id</span> <span class="o">=</span> <span class="nx">azurerm_network_security_group</span><span class="p">.</span><span class="nx">hub_management</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="azure-firewall">Azure Firewall</h3>

<p>Azure Firewall provides the central security control point for the topology. Create <code class="language-plaintext highlighter-rouge">firewall.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Public IP for Azure Firewall</span>
<span class="nx">resource</span> <span class="s2">"azurerm_public_ip"</span> <span class="s2">"firewall"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"pip-firewall-${local.hub_name}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">allocation_method</span>   <span class="o">=</span> <span class="s2">"Static"</span>
  <span class="nx">sku</span>                 <span class="o">=</span> <span class="s2">"Standard"</span>
  <span class="nx">zones</span>               <span class="o">=</span> <span class="p">[</span><span class="s2">"1"</span><span class="p">,</span> <span class="s2">"2"</span><span class="p">,</span> <span class="s2">"3"</span><span class="p">]</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Azure Firewall</span>
<span class="nx">resource</span> <span class="s2">"azurerm_firewall"</span> <span class="s2">"hub"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"afw-${local.hub_name}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">sku_name</span>            <span class="o">=</span> <span class="s2">"AZFW_VNet"</span>
  <span class="nx">sku_tier</span>            <span class="o">=</span> <span class="s2">"Standard"</span>
  <span class="nx">firewall_policy_id</span>  <span class="o">=</span> <span class="nx">azurerm_firewall_policy</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">zones</span>               <span class="o">=</span> <span class="p">[</span><span class="s2">"1"</span><span class="p">,</span> <span class="s2">"2"</span><span class="p">,</span> <span class="s2">"3"</span><span class="p">]</span>

  <span class="nx">ip_configuration</span> <span class="p">{</span>
    <span class="nx">name</span>                 <span class="o">=</span> <span class="s2">"configuration"</span>
    <span class="nx">subnet_id</span>            <span class="o">=</span> <span class="nx">azurerm_subnet</span><span class="p">.</span><span class="nx">hub_firewall</span><span class="p">.</span><span class="nx">id</span>
    <span class="nx">public_ip_address_id</span> <span class="o">=</span> <span class="nx">azurerm_public_ip</span><span class="p">.</span><span class="nx">firewall</span><span class="p">.</span><span class="nx">id</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Firewall Policy</span>
<span class="nx">resource</span> <span class="s2">"azurerm_firewall_policy"</span> <span class="s2">"hub"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"afwp-${local.hub_name}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">sku</span>                 <span class="o">=</span> <span class="s2">"Standard"</span>
  
  <span class="nx">threat_intelligence_mode</span> <span class="o">=</span> <span class="s2">"Alert"</span>

  <span class="nx">dns</span> <span class="p">{</span>
    <span class="nx">proxy_enabled</span> <span class="o">=</span> <span class="kc">true</span>
  <span class="p">}</span>

  <span class="nx">intrusion_detection</span> <span class="p">{</span>
    <span class="nx">mode</span> <span class="o">=</span> <span class="s2">"Alert"</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Firewall Policy Rule Collection Group - Network Rules</span>
<span class="nx">resource</span> <span class="s2">"azurerm_firewall_policy_rule_collection_group"</span> <span class="s2">"network_rules"</span> <span class="p">{</span>
  <span class="nx">name</span>               <span class="o">=</span> <span class="s2">"network-rules"</span>
  <span class="nx">firewall_policy_id</span> <span class="o">=</span> <span class="nx">azurerm_firewall_policy</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">priority</span>           <span class="o">=</span> <span class="mi">100</span>

  <span class="nx">network_rule_collection</span> <span class="p">{</span>
    <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"allow-spoke-to-spoke"</span>
    <span class="nx">priority</span> <span class="o">=</span> <span class="mi">100</span>
    <span class="nx">action</span>   <span class="o">=</span> <span class="s2">"Allow"</span>

    <span class="nx">rule</span> <span class="p">{</span>
      <span class="nx">name</span>                  <span class="o">=</span> <span class="s2">"allow-all-spoke-to-spoke"</span>
      <span class="nx">protocols</span>             <span class="o">=</span> <span class="p">[</span><span class="s2">"Any"</span><span class="p">]</span>
      <span class="nx">source_addresses</span>      <span class="o">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">k</span><span class="p">,</span> <span class="nx">v</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span> <span class="o">:</span> <span class="nx">v</span><span class="p">.</span><span class="nx">address_space</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
      <span class="nx">destination_addresses</span> <span class="o">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">k</span><span class="p">,</span> <span class="nx">v</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span> <span class="o">:</span> <span class="nx">v</span><span class="p">.</span><span class="nx">address_space</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
      <span class="nx">destination_ports</span>     <span class="o">=</span> <span class="p">[</span><span class="s2">"*"</span><span class="p">]</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nx">network_rule_collection</span> <span class="p">{</span>
    <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"allow-dns"</span>
    <span class="nx">priority</span> <span class="o">=</span> <span class="mi">110</span>
    <span class="nx">action</span>   <span class="o">=</span> <span class="s2">"Allow"</span>

    <span class="nx">rule</span> <span class="p">{</span>
      <span class="nx">name</span>                  <span class="o">=</span> <span class="s2">"allow-dns-outbound"</span>
      <span class="nx">protocols</span>             <span class="o">=</span> <span class="p">[</span><span class="s2">"UDP"</span><span class="p">]</span>
      <span class="nx">source_addresses</span>      <span class="o">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">k</span><span class="p">,</span> <span class="nx">v</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span> <span class="o">:</span> <span class="nx">v</span><span class="p">.</span><span class="nx">address_space</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
      <span class="nx">destination_addresses</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"*"</span><span class="p">]</span>
      <span class="nx">destination_ports</span>     <span class="o">=</span> <span class="p">[</span><span class="s2">"53"</span><span class="p">]</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Firewall Policy Rule Collection Group - Application Rules</span>
<span class="nx">resource</span> <span class="s2">"azurerm_firewall_policy_rule_collection_group"</span> <span class="s2">"application_rules"</span> <span class="p">{</span>
  <span class="nx">name</span>               <span class="o">=</span> <span class="s2">"application-rules"</span>
  <span class="nx">firewall_policy_id</span> <span class="o">=</span> <span class="nx">azurerm_firewall_policy</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">priority</span>           <span class="o">=</span> <span class="mi">200</span>

  <span class="nx">application_rule_collection</span> <span class="p">{</span>
    <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"allow-azure-services"</span>
    <span class="nx">priority</span> <span class="o">=</span> <span class="mi">100</span>
    <span class="nx">action</span>   <span class="o">=</span> <span class="s2">"Allow"</span>

    <span class="nx">rule</span> <span class="p">{</span>
      <span class="nx">name</span> <span class="o">=</span> <span class="s2">"allow-azure-management"</span>
      <span class="nx">protocols</span> <span class="p">{</span>
        <span class="nx">type</span> <span class="o">=</span> <span class="s2">"Https"</span>
        <span class="nx">port</span> <span class="o">=</span> <span class="mi">443</span>
      <span class="p">}</span>
      <span class="nx">source_addresses</span> <span class="o">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">k</span><span class="p">,</span> <span class="nx">v</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span> <span class="o">:</span> <span class="nx">v</span><span class="p">.</span><span class="nx">address_space</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
      <span class="nx">destination_fqdns</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s2">"*.azure.com"</span><span class="p">,</span>
        <span class="s2">"*.microsoft.com"</span><span class="p">,</span>
        <span class="s2">"*.windows.net"</span><span class="p">,</span>
        <span class="s2">"*.azure-automation.net"</span>
      <span class="p">]</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nx">application_rule_collection</span> <span class="p">{</span>
    <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"allow-ubuntu-updates"</span>
    <span class="nx">priority</span> <span class="o">=</span> <span class="mi">110</span>
    <span class="nx">action</span>   <span class="o">=</span> <span class="s2">"Allow"</span>

    <span class="nx">rule</span> <span class="p">{</span>
      <span class="nx">name</span> <span class="o">=</span> <span class="s2">"allow-apt-repositories"</span>
      <span class="nx">protocols</span> <span class="p">{</span>
        <span class="nx">type</span> <span class="o">=</span> <span class="s2">"Http"</span>
        <span class="nx">port</span> <span class="o">=</span> <span class="mi">80</span>
      <span class="p">}</span>
      <span class="nx">protocols</span> <span class="p">{</span>
        <span class="nx">type</span> <span class="o">=</span> <span class="s2">"Https"</span>
        <span class="nx">port</span> <span class="o">=</span> <span class="mi">443</span>
      <span class="p">}</span>
      <span class="nx">source_addresses</span> <span class="o">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">k</span><span class="p">,</span> <span class="nx">v</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span> <span class="o">:</span> <span class="nx">v</span><span class="p">.</span><span class="nx">address_space</span><span class="p">[</span><span class="mi">0</span><span class="p">]]</span>
      <span class="nx">destination_fqdns</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s2">"*.ubuntu.com"</span><span class="p">,</span>
        <span class="s2">"*.canonical.com"</span>
      <span class="p">]</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="vpn-gateway">VPN Gateway</h3>

<p>For hybrid connectivity to on-premises networks, deploy VPN Gateway. Create <code class="language-plaintext highlighter-rouge">vpn-gateway.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Public IP for VPN Gateway</span>
<span class="nx">resource</span> <span class="s2">"azurerm_public_ip"</span> <span class="s2">"vpn_gateway"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_vpn_gateway</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span>

  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"pip-vpngw-${local.hub_name}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">allocation_method</span>   <span class="o">=</span> <span class="s2">"Static"</span>
  <span class="nx">sku</span>                 <span class="o">=</span> <span class="s2">"Standard"</span>
  <span class="nx">zones</span>               <span class="o">=</span> <span class="p">[</span><span class="s2">"1"</span><span class="p">,</span> <span class="s2">"2"</span><span class="p">,</span> <span class="s2">"3"</span><span class="p">]</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># VPN Gateway</span>
<span class="nx">resource</span> <span class="s2">"azurerm_virtual_network_gateway"</span> <span class="s2">"hub"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_vpn_gateway</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span>

  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"vpngw-${local.hub_name}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>

  <span class="nx">type</span>     <span class="o">=</span> <span class="s2">"Vpn"</span>
  <span class="nx">vpn_type</span> <span class="o">=</span> <span class="s2">"RouteBased"</span>

  <span class="nx">active_active</span> <span class="o">=</span> <span class="kc">false</span>
  <span class="nx">enable_bgp</span>    <span class="o">=</span> <span class="kc">true</span>
  <span class="nx">sku</span>           <span class="o">=</span> <span class="s2">"VpnGw2AZ"</span>
  <span class="nx">generation</span>    <span class="o">=</span> <span class="s2">"Generation2"</span>

  <span class="nx">ip_configuration</span> <span class="p">{</span>
    <span class="nx">name</span>                          <span class="o">=</span> <span class="s2">"vnetGatewayConfig"</span>
    <span class="nx">public_ip_address_id</span>          <span class="o">=</span> <span class="nx">azurerm_public_ip</span><span class="p">.</span><span class="nx">vpn_gateway</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span>
    <span class="nx">private_ip_address_allocation</span> <span class="o">=</span> <span class="s2">"Dynamic"</span>
    <span class="nx">subnet_id</span>                     <span class="o">=</span> <span class="nx">azurerm_subnet</span><span class="p">.</span><span class="nx">hub_gateway</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span>
  <span class="p">}</span>

  <span class="nx">bgp_settings</span> <span class="p">{</span>
    <span class="nx">asn</span> <span class="o">=</span> <span class="mi">65515</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Local Network Gateway for on-premises</span>
<span class="nx">resource</span> <span class="s2">"azurerm_local_network_gateway"</span> <span class="s2">"onpremises"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_vpn_gateway</span> <span class="o">&amp;&amp;</span> <span class="nx">length</span><span class="p">(</span><span class="nx">var</span><span class="p">.</span><span class="nx">on_premises_address_spaces</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span>

  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"lng-onpremises-${local.hub_name}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>

  <span class="nx">gateway_address</span> <span class="o">=</span> <span class="s2">"0.0.0.0"</span> <span class="c1"># Replace with actual on-premises gateway IP</span>
  <span class="nx">address_space</span>   <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">on_premises_address_spaces</span>

  <span class="nx">bgp_settings</span> <span class="p">{</span>
    <span class="nx">asn</span>                 <span class="o">=</span> <span class="mi">65000</span>
    <span class="nx">bgp_peering_address</span> <span class="o">=</span> <span class="s2">"192.168.1.1"</span> <span class="c1"># Replace with actual on-premises BGP peer IP</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="azure-bastion">Azure Bastion</h3>

<p>For secure remote access to virtual machines, deploy Azure Bastion. Create <code class="language-plaintext highlighter-rouge">bastion.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Public IP for Azure Bastion</span>
<span class="nx">resource</span> <span class="s2">"azurerm_public_ip"</span> <span class="s2">"bastion"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_bastion</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span>

  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"pip-bastion-${local.hub_name}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">allocation_method</span>   <span class="o">=</span> <span class="s2">"Static"</span>
  <span class="nx">sku</span>                 <span class="o">=</span> <span class="s2">"Standard"</span>
  <span class="nx">zones</span>               <span class="o">=</span> <span class="p">[</span><span class="s2">"1"</span><span class="p">,</span> <span class="s2">"2"</span><span class="p">,</span> <span class="s2">"3"</span><span class="p">]</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Azure Bastion</span>
<span class="nx">resource</span> <span class="s2">"azurerm_bastion_host"</span> <span class="s2">"hub"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_bastion</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span>

  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"bastion-${local.hub_name}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">sku</span>                 <span class="o">=</span> <span class="s2">"Standard"</span>

  <span class="nx">copy_paste_enabled</span>     <span class="o">=</span> <span class="kc">true</span>
  <span class="nx">file_copy_enabled</span>      <span class="o">=</span> <span class="kc">true</span>
  <span class="nx">ip_connect_enabled</span>     <span class="o">=</span> <span class="kc">true</span>
  <span class="nx">shareable_link_enabled</span> <span class="o">=</span> <span class="kc">false</span>
  <span class="nx">tunneling_enabled</span>      <span class="o">=</span> <span class="kc">true</span>

  <span class="nx">ip_configuration</span> <span class="p">{</span>
    <span class="nx">name</span>                 <span class="o">=</span> <span class="s2">"configuration"</span>
    <span class="nx">subnet_id</span>            <span class="o">=</span> <span class="nx">azurerm_subnet</span><span class="p">.</span><span class="nx">hub_bastion</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span>
    <span class="nx">public_ip_address_id</span> <span class="o">=</span> <span class="nx">azurerm_public_ip</span><span class="p">.</span><span class="nx">bastion</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">common_tags</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="spoke-virtual-networks">Spoke Virtual Networks</h3>

<p>Create spoke VNets dynamically based on the input variable. Create <code class="language-plaintext highlighter-rouge">spokes.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Resource groups for spoke VNets</span>
<span class="nx">resource</span> <span class="s2">"azurerm_resource_group"</span> <span class="s2">"spokes"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span>

  <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"rg-network-spoke-${each.key}-${var.environment}"</span>
  <span class="nx">location</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">tags</span>     <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Spoke virtual networks</span>
<span class="nx">resource</span> <span class="s2">"azurerm_virtual_network"</span> <span class="s2">"spokes"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span>

  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"vnet-spoke-${each.key}-${var.environment}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">name</span>
  <span class="nx">address_space</span>       <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">address_space</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Spoke subnets</span>
<span class="nx">resource</span> <span class="s2">"azurerm_subnet"</span> <span class="s2">"spoke_subnets"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">merge</span><span class="p">([</span>
    <span class="nx">for</span> <span class="nx">spoke_key</span><span class="p">,</span> <span class="nx">spoke</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span> <span class="o">:</span> <span class="p">{</span>
      <span class="nx">for</span> <span class="nx">subnet_key</span><span class="p">,</span> <span class="nx">subnet</span> <span class="nx">in</span> <span class="nx">spoke</span><span class="p">.</span><span class="nx">subnets</span> <span class="o">:</span>
      <span class="s2">"${spoke_key}-${subnet_key}"</span> <span class="o">=&gt;</span> <span class="nx">merge</span><span class="p">(</span><span class="nx">subnet</span><span class="p">,</span> <span class="p">{</span>
        <span class="nx">spoke_key</span> <span class="o">=</span> <span class="nx">spoke_key</span>
        <span class="nx">vnet_name</span> <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">spoke_key</span><span class="p">].</span><span class="nx">name</span>
        <span class="nx">rg_name</span>   <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">spoke_key</span><span class="p">].</span><span class="nx">name</span>
      <span class="p">})</span>
    <span class="p">}</span>
  <span class="p">]...)</span>

  <span class="nx">name</span>                 <span class="o">=</span> <span class="s2">"snet-${each.value.spoke_key}-${split("</span><span class="o">-</span><span class="s2">", each.key)[1]}"</span>
  <span class="nx">resource_group_name</span>  <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">rg_name</span>
  <span class="nx">virtual_network_name</span> <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">vnet_name</span>
  <span class="nx">address_prefixes</span>     <span class="o">=</span> <span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">address_prefix</span><span class="p">]</span>
  <span class="nx">service_endpoints</span>    <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">service_endpoints</span>

  <span class="nx">dynamic</span> <span class="s2">"delegation"</span> <span class="p">{</span>
    <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">delegation</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">?</span> <span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">delegation</span><span class="p">]</span> <span class="o">:</span> <span class="p">[]</span>
    <span class="nx">content</span> <span class="p">{</span>
      <span class="nx">name</span> <span class="o">=</span> <span class="nx">delegation</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">name</span>

      <span class="nx">service_delegation</span> <span class="p">{</span>
        <span class="nx">name</span>    <span class="o">=</span> <span class="nx">delegation</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">service_delegation</span><span class="p">.</span><span class="nx">name</span>
        <span class="nx">actions</span> <span class="o">=</span> <span class="nx">delegation</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">service_delegation</span><span class="p">.</span><span class="nx">actions</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Network security groups for spoke subnets</span>
<span class="nx">resource</span> <span class="s2">"azurerm_network_security_group"</span> <span class="s2">"spoke_subnets"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">azurerm_subnet</span><span class="p">.</span><span class="nx">spoke_subnets</span>

  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"nsg-${each.key}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">resource_group_name</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># NSG associations</span>
<span class="nx">resource</span> <span class="s2">"azurerm_subnet_network_security_group_association"</span> <span class="s2">"spoke_subnets"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">azurerm_subnet</span><span class="p">.</span><span class="nx">spoke_subnets</span>

  <span class="nx">subnet_id</span>                 <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">network_security_group_id</span> <span class="o">=</span> <span class="nx">azurerm_network_security_group</span><span class="p">.</span><span class="nx">spoke_subnets</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="vnet-peering">VNet Peering</h3>

<p>Connect spokes to the hub through VNet peering. Create <code class="language-plaintext highlighter-rouge">peering.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Hub to spoke peering</span>
<span class="nx">resource</span> <span class="s2">"azurerm_virtual_network_peering"</span> <span class="s2">"hub_to_spoke"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span>

  <span class="nx">name</span>                      <span class="o">=</span> <span class="s2">"peer-hub-to-${each.key}"</span>
  <span class="nx">resource_group_name</span>       <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">virtual_network_name</span>      <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">remote_virtual_network_id</span> <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">id</span>

  <span class="nx">allow_virtual_network_access</span> <span class="o">=</span> <span class="kc">true</span>
  <span class="nx">allow_forwarded_traffic</span>      <span class="o">=</span> <span class="kc">true</span>
  <span class="nx">allow_gateway_transit</span>        <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_vpn_gateway</span>
  <span class="nx">use_remote_gateways</span>          <span class="o">=</span> <span class="kc">false</span>
<span class="p">}</span>

<span class="c1"># Spoke to hub peering</span>
<span class="nx">resource</span> <span class="s2">"azurerm_virtual_network_peering"</span> <span class="s2">"spoke_to_hub"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span>

  <span class="nx">name</span>                      <span class="o">=</span> <span class="s2">"peer-${each.key}-to-hub"</span>
  <span class="nx">resource_group_name</span>       <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">name</span>
  <span class="nx">virtual_network_name</span>      <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">name</span>
  <span class="nx">remote_virtual_network_id</span> <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">allow_virtual_network_access</span> <span class="o">=</span> <span class="kc">true</span>
  <span class="nx">allow_forwarded_traffic</span>      <span class="o">=</span> <span class="kc">true</span>
  <span class="nx">allow_gateway_transit</span>        <span class="o">=</span> <span class="kc">false</span>
  <span class="nx">use_remote_gateways</span>          <span class="o">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">enable_vpn_gateway</span>

  <span class="nx">depends_on</span> <span class="o">=</span> <span class="p">[</span>
    <span class="nx">azurerm_virtual_network_gateway</span><span class="p">.</span><span class="nx">hub</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="routing-configuration">Routing Configuration</h3>

<p>Configure user-defined routes to direct traffic through Azure Firewall. Create <code class="language-plaintext highlighter-rouge">routing.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Route table for spoke VNets</span>
<span class="nx">resource</span> <span class="s2">"azurerm_route_table"</span> <span class="s2">"spokes"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span>

  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"rt-spoke-${each.key}-${var.environment}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">name</span>

  <span class="nx">disable_bgp_route_propagation</span> <span class="o">=</span> <span class="kc">false</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Route to send internet traffic through firewall</span>
<span class="nx">resource</span> <span class="s2">"azurerm_route"</span> <span class="s2">"spoke_internet_via_firewall"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span>

  <span class="nx">name</span>                   <span class="o">=</span> <span class="s2">"route-internet-via-firewall"</span>
  <span class="nx">resource_group_name</span>    <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">name</span>
  <span class="nx">route_table_name</span>       <span class="o">=</span> <span class="nx">azurerm_route_table</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">].</span><span class="nx">name</span>
  <span class="nx">address_prefix</span>         <span class="o">=</span> <span class="s2">"0.0.0.0/0"</span>
  <span class="nx">next_hop_type</span>          <span class="o">=</span> <span class="s2">"VirtualAppliance"</span>
  <span class="nx">next_hop_in_ip_address</span> <span class="o">=</span> <span class="nx">azurerm_firewall</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">ip_configuration</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">private_ip_address</span>
<span class="p">}</span>

<span class="c1"># Routes to send spoke-to-spoke traffic through firewall</span>
<span class="nx">resource</span> <span class="s2">"azurerm_route"</span> <span class="s2">"spoke_to_spoke_via_firewall"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">merge</span><span class="p">([</span>
    <span class="nx">for</span> <span class="nx">spoke_key</span><span class="p">,</span> <span class="nx">spoke</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span> <span class="o">:</span> <span class="p">{</span>
      <span class="nx">for</span> <span class="nx">other_spoke_key</span><span class="p">,</span> <span class="nx">other_spoke</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">spoke_vnets</span> <span class="o">:</span>
      <span class="s2">"${spoke_key}-to-${other_spoke_key}"</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">source_spoke</span>      <span class="o">=</span> <span class="nx">spoke_key</span>
        <span class="nx">destination_spoke</span> <span class="o">=</span> <span class="nx">other_spoke_key</span>
        <span class="nx">address_prefix</span>    <span class="o">=</span> <span class="nx">other_spoke</span><span class="err">.</span><span class="nx">address_space</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
      <span class="p">}</span>
      <span class="nx">if</span> <span class="nx">spoke_key</span> <span class="o">!=</span> <span class="nx">other_spoke_key</span>
    <span class="p">}</span>
  <span class="p">]...)</span>

  <span class="nx">name</span>                   <span class="o">=</span> <span class="s2">"route-to-${each.value.destination_spoke}"</span>
  <span class="nx">resource_group_name</span>    <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">source_spoke</span><span class="p">].</span><span class="nx">name</span>
  <span class="nx">route_table_name</span>       <span class="o">=</span> <span class="nx">azurerm_route_table</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">source_spoke</span><span class="p">].</span><span class="nx">name</span>
  <span class="nx">address_prefix</span>         <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">address_prefix</span>
  <span class="nx">next_hop_type</span>          <span class="o">=</span> <span class="s2">"VirtualAppliance"</span>
  <span class="nx">next_hop_in_ip_address</span> <span class="o">=</span> <span class="nx">azurerm_firewall</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">ip_configuration</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">private_ip_address</span>
<span class="p">}</span>

<span class="c1"># Associate route tables with spoke subnets</span>
<span class="nx">resource</span> <span class="s2">"azurerm_subnet_route_table_association"</span> <span class="s2">"spoke_subnets"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">azurerm_subnet</span><span class="p">.</span><span class="nx">spoke_subnets</span>

  <span class="nx">subnet_id</span>      <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">route_table_id</span> <span class="o">=</span> <span class="nx">azurerm_route_table</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">virtual_network_name</span> <span class="o">==</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">spokes</span><span class="p">[</span><span class="nx">split</span><span class="p">(</span><span class="s2">"-"</span><span class="p">,</span> <span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">)[</span><span class="mi">0</span><span class="p">]].</span><span class="nx">name</span> <span class="o">?</span> <span class="nx">split</span><span class="p">(</span><span class="s2">"-"</span><span class="p">,</span> <span class="nx">each</span><span class="p">.</span><span class="nx">key</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="o">:</span> <span class="s2">""</span><span class="p">].</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="monitoring-and-diagnostics">Monitoring and Diagnostics</h3>

<p>Implement comprehensive monitoring for the network infrastructure. Create <code class="language-plaintext highlighter-rouge">monitoring.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Log Analytics workspace</span>
<span class="nx">resource</span> <span class="s2">"azurerm_log_analytics_workspace"</span> <span class="s2">"network"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"log-network-${var.environment}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">sku</span>                 <span class="o">=</span> <span class="s2">"PerGB2018"</span>
  <span class="nx">retention_in_days</span>   <span class="o">=</span> <span class="mi">30</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>

<span class="c1"># Diagnostic settings for Azure Firewall</span>
<span class="nx">resource</span> <span class="s2">"azurerm_monitor_diagnostic_setting"</span> <span class="s2">"firewall"</span> <span class="p">{</span>
  <span class="nx">name</span>                       <span class="o">=</span> <span class="s2">"firewall-diagnostics"</span>
  <span class="nx">target_resource_id</span>         <span class="o">=</span> <span class="nx">azurerm_firewall</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">log_analytics_workspace_id</span> <span class="o">=</span> <span class="nx">azurerm_log_analytics_workspace</span><span class="p">.</span><span class="nx">network</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">enabled_log</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"AzureFirewallApplicationRule"</span>
  <span class="p">}</span>

  <span class="nx">enabled_log</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"AzureFirewallNetworkRule"</span>
  <span class="p">}</span>

  <span class="nx">enabled_log</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"AzureFirewallDnsProxy"</span>
  <span class="p">}</span>

  <span class="nx">metric</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"AllMetrics"</span>
    <span class="nx">enabled</span>  <span class="o">=</span> <span class="kc">true</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Diagnostic settings for VPN Gateway</span>
<span class="nx">resource</span> <span class="s2">"azurerm_monitor_diagnostic_setting"</span> <span class="s2">"vpn_gateway"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_vpn_gateway</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span>

  <span class="nx">name</span>                       <span class="o">=</span> <span class="s2">"vpngw-diagnostics"</span>
  <span class="nx">target_resource_id</span>         <span class="o">=</span> <span class="nx">azurerm_virtual_network_gateway</span><span class="p">.</span><span class="nx">hub</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span>
  <span class="nx">log_analytics_workspace_id</span> <span class="o">=</span> <span class="nx">azurerm_log_analytics_workspace</span><span class="p">.</span><span class="nx">network</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">enabled_log</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"GatewayDiagnosticLog"</span>
  <span class="p">}</span>

  <span class="nx">enabled_log</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"TunnelDiagnosticLog"</span>
  <span class="p">}</span>

  <span class="nx">enabled_log</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"RouteDiagnosticLog"</span>
  <span class="p">}</span>

  <span class="nx">enabled_log</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"IKEDiagnosticLog"</span>
  <span class="p">}</span>

  <span class="nx">metric</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"AllMetrics"</span>
    <span class="nx">enabled</span>  <span class="o">=</span> <span class="kc">true</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Diagnostic settings for Bastion</span>
<span class="nx">resource</span> <span class="s2">"azurerm_monitor_diagnostic_setting"</span> <span class="s2">"bastion"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_bastion</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span>

  <span class="nx">name</span>                       <span class="o">=</span> <span class="s2">"bastion-diagnostics"</span>
  <span class="nx">target_resource_id</span>         <span class="o">=</span> <span class="nx">azurerm_bastion_host</span><span class="p">.</span><span class="nx">hub</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span>
  <span class="nx">log_analytics_workspace_id</span> <span class="o">=</span> <span class="nx">azurerm_log_analytics_workspace</span><span class="p">.</span><span class="nx">network</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">enabled_log</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"BastionAuditLogs"</span>
  <span class="p">}</span>

  <span class="nx">metric</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"AllMetrics"</span>
    <span class="nx">enabled</span>  <span class="o">=</span> <span class="kc">true</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Network Watcher</span>
<span class="nx">resource</span> <span class="s2">"azurerm_network_watcher"</span> <span class="s2">"main"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"nw-${var.environment}-${var.location}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="nx">local</span><span class="p">.</span><span class="nx">common_tags</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="outputs">Outputs</h3>

<p>Export important values for reference. Create <code class="language-plaintext highlighter-rouge">outputs.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">output</span> <span class="s2">"hub_vnet_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Resource ID of the hub VNet"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"hub_vnet_name"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Name of the hub VNet"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">name</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"firewall_private_ip"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Private IP address of Azure Firewall"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">azurerm_firewall</span><span class="p">.</span><span class="nx">hub</span><span class="p">.</span><span class="nx">ip_configuration</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">private_ip_address</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"spoke_vnet_ids"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Map of spoke VNet resource IDs"</span>
  <span class="nx">value</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">for</span> <span class="nx">k</span><span class="p">,</span> <span class="nx">v</span> <span class="nx">in</span> <span class="nx">azurerm_virtual_network</span><span class="p">.</span><span class="nx">spokes</span> <span class="o">:</span> <span class="nx">k</span> <span class="o">=&gt;</span> <span class="nx">v</span><span class="p">.</span><span class="nx">id</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"spoke_subnet_ids"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Map of spoke subnet resource IDs"</span>
  <span class="nx">value</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">for</span> <span class="nx">k</span><span class="p">,</span> <span class="nx">v</span> <span class="nx">in</span> <span class="nx">azurerm_subnet</span><span class="p">.</span><span class="nx">spoke_subnets</span> <span class="o">:</span> <span class="nx">k</span> <span class="o">=&gt;</span> <span class="nx">v</span><span class="p">.</span><span class="nx">id</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"bastion_fqdn"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"FQDN of Azure Bastion"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_bastion</span> <span class="o">?</span> <span class="nx">azurerm_bastion_host</span><span class="p">.</span><span class="nx">hub</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">dns_name</span> <span class="o">:</span> <span class="kc">null</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"vpn_gateway_public_ip"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Public IP address of VPN Gateway"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">enable_vpn_gateway</span> <span class="o">?</span> <span class="nx">azurerm_public_ip</span><span class="p">.</span><span class="nx">vpn_gateway</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">ip_address</span> <span class="o">:</span> <span class="kc">null</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="example-variables-file">Example Variables File</h3>

<p>Create <code class="language-plaintext highlighter-rouge">terraform.tfvars.example</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">environment</span>  <span class="o">=</span> <span class="s2">"prod"</span>
<span class="nx">location</span>     <span class="o">=</span> <span class="s2">"uksouth"</span>
<span class="nx">organisation</span> <span class="o">=</span> <span class="s2">"contoso"</span>

<span class="nx">hub_vnet_address_space</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"10.0.0.0/16"</span><span class="p">]</span>

<span class="nx">spoke_vnets</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">production</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">address_space</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"10.1.0.0/16"</span><span class="p">]</span>
    <span class="nx">subnets</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">web</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">address_prefix</span>    <span class="o">=</span> <span class="s2">"10.1.1.0/24"</span>
        <span class="nx">service_endpoints</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"Microsoft.Storage"</span><span class="p">,</span> <span class="s2">"Microsoft.KeyVault"</span><span class="p">]</span>
      <span class="p">}</span>
      <span class="nx">app</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">address_prefix</span>    <span class="o">=</span> <span class="s2">"10.1.2.0/24"</span>
        <span class="nx">service_endpoints</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"Microsoft.Storage"</span><span class="p">,</span> <span class="s2">"Microsoft.Sql"</span><span class="p">]</span>
      <span class="p">}</span>
      <span class="nx">data</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">address_prefix</span>    <span class="o">=</span> <span class="s2">"10.1.3.0/24"</span>
        <span class="nx">service_endpoints</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"Microsoft.Sql"</span><span class="p">,</span> <span class="s2">"Microsoft.Storage"</span><span class="p">]</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="nx">development</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">address_space</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"10.2.0.0/16"</span><span class="p">]</span>
    <span class="nx">subnets</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nx">workloads</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nx">address_prefix</span>    <span class="o">=</span> <span class="s2">"10.2.1.0/24"</span>
        <span class="nx">service_endpoints</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"Microsoft.Storage"</span><span class="p">]</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">enable_vpn_gateway</span> <span class="o">=</span> <span class="kc">true</span>
<span class="nx">enable_bastion</span>     <span class="o">=</span> <span class="kc">true</span>

<span class="nx">on_premises_address_spaces</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"192.168.0.0/16"</span><span class="p">]</span>

<span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
  <span class="nx">cost_centre</span> <span class="o">=</span> <span class="s2">"platform"</span>
  <span class="nx">project</span>     <span class="o">=</span> <span class="s2">"network-infrastructure"</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="security-best-practices">Security Best Practices</h2>

<p>Security in a hub and spoke topology requires defence in depth—multiple layers of protection that work together to prevent, detect, and respond to threats.</p>

<h3 id="network-segmentation">Network Segmentation</h3>

<p>The hub and spoke pattern inherently provides segmentation by isolating workloads into separate virtual networks. But segmentation within spokes is equally important. Use subnets to create security boundaries between tiers of your application.</p>

<p>A three-tier application should have three subnets minimum: web tier, application tier, and data tier. Network security groups on each subnet enforce which traffic is allowed. The web tier subnet allows inbound HTTPS from the internet and outbound connections to the application tier. The application tier allows inbound connections only from the web tier and outbound connections only to the data tier. The data tier allows inbound connections only from the application tier.</p>

<p>This creates a security posture where even if an attacker compromises the web tier, they cannot directly access databases in the data tier. They must traverse multiple network boundaries, each with its own controls, giving your security team multiple opportunities to detect and block the attack.</p>

<h3 id="azure-firewall-configuration">Azure Firewall Configuration</h3>

<p>Azure Firewall’s power lies in its policy-based approach to network security. Rather than configuring individual firewall rules on every virtual machine, you define policies centrally and Azure Firewall enforces them for all traffic flowing through the hub.</p>

<p>Start with a deny-all default posture and explicitly allow only the traffic you need. The application rules we configured allow access to Azure services and Ubuntu repositories, but block everything else by default. As your organisation grows, add more specific rules rather than opening up broad access.</p>

<p>Enable threat intelligence in Alert mode initially, which logs potential threats without blocking them. Once you understand your traffic patterns, switch to Alert and Deny mode to actively block connections to known malicious IP addresses and domains.</p>

<p>The DNS proxy feature is particularly valuable. When enabled, Azure Firewall acts as a DNS server for your spokes, allowing you to create firewall rules based on FQDNs rather than IP addresses. This is crucial for SaaS applications and Azure services where IP addresses change frequently.</p>

<h3 id="network-security-groups">Network Security Groups</h3>

<p>Network security groups provide granular control at the subnet or network interface level. They’re your last line of defence when Azure Firewall is the first line.</p>

<p>Design NSG rules with specificity. Instead of allowing all TCP traffic, specify the exact ports your application needs. Instead of allowing traffic from any source, limit it to the specific subnets or IP addresses that should have access.</p>

<p>Use application security groups (ASGs) to simplify NSG rule management. Instead of specifying IP addresses in NSG rules, you specify ASGs. Then assign virtual machine network interfaces to ASGs based on their role. When you need to change firewall rules for all web servers, you modify the NSG rule once rather than updating rules for each individual server.</p>

<p>Tag your NSG rules with descriptive names and comments explaining their purpose. Future you, investigating a security incident at 3am, will appreciate knowing why a particular rule exists and what business requirement it serves.</p>

<h3 id="private-endpoints">Private Endpoints</h3>

<p>For Azure PaaS services like Storage Accounts, SQL Databases, and Key Vaults, use private endpoints to access them over private IP addresses from your virtual network. This keeps data plane traffic entirely within Azure’s network backbone, never traversing the public internet.</p>

<p>Private endpoints create a network interface in your VNet with a private IP address. DNS resolution for the PaaS service hostname returns this private IP address instead of the public IP address. Your applications access the service over the private connection without any code changes.</p>

<p>Organise private endpoints in dedicated subnets within your spoke VNets. This makes it easier to manage network security group rules and DNS configuration. A subnet specifically for private endpoints can have restrictive NSG rules since it only contains network interfaces, not compute resources.</p>

<h3 id="just-in-time-access">Just-in-Time Access</h3>

<p>Azure Bastion eliminates the need for virtual machines with public IP addresses, but some scenarios still require SSH or RDP access. Azure Defender for Cloud’s Just-in-Time (JIT) VM Access feature provides a middle ground.</p>

<p>JIT keeps management ports closed by default and opens them only when needed, for a limited time, and only for authorised users. When you need to access a virtual machine, you request access through the Azure portal or CLI. Azure opens the NSG rules for your specific IP address, allows you to connect, and automatically closes the rules after the time period expires.</p>

<p>This dramatically reduces the attack surface. Rather than having RDP or SSH ports exposed 24/7, they’re only open for the brief periods when legitimate administrators need access. Even then, access is restricted to specific source IP addresses.</p>

<h3 id="audit-and-compliance">Audit and Compliance</h3>

<p>Enable diagnostic logging for all network resources. Azure Firewall logs every allowed and denied connection. VPN Gateway logs all tunnel events and configuration changes. Bastion logs every remote access session. This comprehensive audit trail is invaluable for security investigations and compliance requirements.</p>

<p>Send all logs to Azure Monitor Log Analytics where you can query them using KQL, create alerts for suspicious activity, and build dashboards for security operations teams. Configure alerts for events like firewall rule changes, VPN tunnel failures, or unusual traffic patterns.</p>

<p>For compliance requirements like PCI DSS or HIPAA, the hub and spoke topology provides clear network boundaries that align with compliance scopes. Your PCI DSS environment can live in a dedicated spoke with strict firewall rules controlling all access. Audit logs prove that traffic between scopes flows through inspected and logged network paths.</p>

<h2 id="operational-considerations">Operational Considerations</h2>

<p>Building the infrastructure is the first step. Operating it successfully over time requires attention to several ongoing concerns.</p>

<h3 id="capacity-planning">Capacity Planning</h3>

<p>Monitor Azure Firewall throughput to ensure it doesn’t become a bottleneck. The Standard SKU supports up to 30 Gbps of throughput, but actual performance depends on your specific traffic patterns and rule complexity. If you approach capacity limits, consider upgrading to the Premium SKU or deploying multiple firewall instances.</p>

<p>VPN Gateway capacity varies by SKU. The VpnGw1AZ SKU supports up to 650 Mbps aggregate throughput and 30 tunnels. The VpnGw5AZ SKU supports up to 10 Gbps and 100 tunnels. Plan for growth when selecting SKU sizes—upgrading later requires brief downtime.</p>

<p>Spoke VNet capacity rarely becomes an issue since each spoke can have up to 65,536 IP addresses with a /16 allocation. But subnet sizes within spokes require planning. A /24 subnet provides 251 usable addresses (Azure reserves five addresses per subnet). For auto-scaling workloads, ensure subnets have enough addresses to accommodate peak scale.</p>

<h3 id="cost-optimisation">Cost Optimisation</h3>

<p>Hub and spoke topologies have predictable cost components. Azure Firewall costs around £1,000 per month for the firewall itself plus data processing charges. VPN Gateway costs between £100 and £1,500 per month depending on SKU. Azure Bastion costs around £100 per month.</p>

<p>VNet peering incurs charges for data transfer between VNets. Peering between VNets in the same region costs £0.01 per GB in each direction. This seems small but compounds with high traffic volumes. If two services need to exchange large volumes of data frequently, consider deploying them in the same spoke VNet rather than separate spokes.</p>

<p>Enable Azure Advisor cost recommendations and review them regularly. Advisor identifies unused resources like VPN Gateway connections that haven’t carried traffic in weeks or public IP addresses that aren’t associated with any resource.</p>

<h3 id="high-availability">High Availability</h3>

<p>The hub becomes a single point of failure if not designed for high availability. Azure Firewall supports zone redundancy, spreading instances across availability zones. If one zone fails, the firewall continues operating from the other zones. Enable zone redundancy by specifying zones during firewall creation.</p>

<p>VPN Gateway also supports zone redundancy with the VpnGwNAZ SKUs. These deploy gateway instances across availability zones, ensuring VPN connectivity remains available even during zone failures. For mission-critical connectivity, deploy both VPN Gateway and ExpressRoute, configuring VPN as a backup path if ExpressRoute fails.</p>

<p>Azure Bastion’s Standard SKU includes high availability with multiple instances deployed automatically. Host scaling adjusts the number of instances based on concurrent sessions, ensuring performance during peak usage.</p>

<p>Consider deploying hub and spoke topologies in multiple regions for disaster recovery. Create a hub in your primary region and another in your secondary region. Spoke VNets in each region peer with their regional hub. Use Azure Traffic Manager or Azure Front Door to distribute traffic between regions.</p>

<h3 id="automation-and-gitops">Automation and GitOps</h3>

<p>Store all Terraform configuration in version control and use CI/CD pipelines to deploy changes. This provides audit trails showing who changed what and when, makes it easy to rollback problematic changes, and ensures consistency across environments.</p>

<p>Use Terraform workspaces or separate state files for different environments. Development, staging, and production environments should be identical in structure but isolated in deployment. This allows you to test network changes in development before applying them to production.</p>

<p>Implement automated testing for your network infrastructure. Tools like Terratest can validate that your Terraform code creates the expected resources with the correct configuration. Network tests can verify that spoke VNets can communicate through the firewall, that internet access works, and that on-premises connectivity functions correctly.</p>

<h3 id="troubleshooting-common-issues">Troubleshooting Common Issues</h3>

<p>Spoke-to-spoke connectivity issues usually stem from routing or firewall rule problems. Use Azure Network Watcher’s Connection Troubleshoot feature to test connectivity between virtual machines in different spokes. It shows you the exact network path traffic takes and identifies where it’s being blocked.</p>

<p>VPN connectivity problems often relate to on-premises firewall configuration or IP addressing conflicts. Ensure on-premises firewalls allow UDP ports 500 and 4500 for IKE traffic. Verify that on-premises address spaces don’t overlap with Azure VNet address spaces. Use VPN Gateway diagnostic logs to identify where tunnel negotiation is failing.</p>

<p>DNS resolution issues can break private endpoint connectivity. When using Azure Firewall’s DNS proxy, ensure spoke VNets configure the firewall’s private IP address as their DNS server. Use the <code class="language-plaintext highlighter-rouge">nslookup</code> command from spoke virtual machines to verify DNS resolution returns private IP addresses for PaaS services with private endpoints.</p>

<h2 id="advanced-patterns-and-extensions">Advanced Patterns and Extensions</h2>

<p>Once you’ve mastered the basic hub and spoke topology, several advanced patterns extend its capabilities.</p>

<h3 id="multi-region-hub-and-spoke">Multi-Region Hub and Spoke</h3>

<p>Global organisations often deploy hub and spoke topologies in multiple Azure regions. Each region has its own hub VNet with Azure Firewall, VPN Gateway, and Bastion. Regional spoke VNets peer with their regional hub.</p>

<p>Connect regional hubs using Global VNet Peering, which allows VNets in different regions to communicate over Microsoft’s global backbone network. This enables workloads in spoke VNets in different regions to communicate whilst keeping regional traffic within the region.</p>

<p>Use Azure Virtual WAN as an alternative to manually peering regional hubs. Virtual WAN provides a managed hub and spoke topology with built-in routing, VPN connectivity, and optimised global connectivity. It simplifies management but costs more than self-managed hubs.</p>

<h3 id="shared-services-spoke">Shared Services Spoke</h3>

<p>Some organisations create a dedicated shared services spoke within the hub and spoke topology. This spoke contains services used by multiple application spokes: Active Directory domain controllers, DNS servers, monitoring infrastructure, or DevOps tools.</p>

<p>The shared services spoke peers with the hub like any other spoke but receives special firewall rules allowing it to accept connections from all other spokes. Application spokes can initiate connections to shared services, but shared services cannot initiate connections to application spokes, maintaining security boundaries.</p>

<h3 id="network-virtual-appliances">Network Virtual Appliances</h3>

<p>Whilst Azure Firewall meets most requirements, some scenarios need third-party network virtual appliances (NVAs) for features like SD-WAN, advanced threat detection, or integration with existing on-premises security infrastructure.</p>

<p>Deploy NVAs in the hub VNet in a dedicated subnet. Configure user-defined routes to send traffic through the NVA instead of Azure Firewall. Many NVA vendors provide Azure Marketplace images and ARM templates that simplify deployment.</p>

<p>NVAs require careful capacity planning and high availability design. Deploy multiple instances in an availability set or availability zones with a load balancer distributing traffic between them. Monitor NVA performance and scale instances as traffic grows.</p>

<h3 id="azure-firewall-manager">Azure Firewall Manager</h3>

<p>For organisations with multiple hub and spoke topologies across regions or subscriptions, Azure Firewall Manager centralises policy management. Instead of configuring firewall rules separately in each hub, you define policies once in Firewall Manager and apply them to multiple firewalls.</p>

<p>Firewall Manager supports policy inheritance, where child policies inherit rules from parent policies and add environment-specific rules. This allows you to define organisation-wide security policies centrally whilst giving regional teams flexibility to add rules for their specific needs.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The hub and spoke network topology represents a mature, battle-tested approach to Azure network architecture. It solves the fundamental challenges of cloud networking—providing isolation between workloads whilst enabling controlled connectivity, centralising security enforcement without creating bottlenecks, and scaling elegantly from small deployments to global enterprise platforms.</p>

<p>Building a production-ready hub and spoke topology requires careful planning around IP addressing, routing, security, and high availability. The Terraform code we’ve built demonstrates these principles in practice, creating infrastructure that’s secure by default, scales with your organisation, and provides the observability needed for effective operations.</p>

<p>The initial investment in building a proper hub and spoke topology pays dividends over time. Each new spoke you add follows the established pattern, inheriting security controls and connectivity automatically. Your platform engineering team maintains central control over security policy and hybrid connectivity whilst development teams get the isolation and autonomy they need to move quickly.</p>

<p>As your Azure footprint grows, the hub and spoke pattern grows with you. Additional spokes, regional hubs, and advanced features like Azure Firewall Manager extend the topology without requiring fundamental redesign. You’ve built a foundation that will serve your organisation reliably for years, providing the network infrastructure on which thousands of workloads can run securely and efficiently.</p>]]></content><author><name>Glen Thomas</name></author><category term="Platform Engineering" /><category term="Azure" /><category term="Virtual Network" /><category term="VNet" /><category term="Hub and Spoke" /><category term="Terraform" /><category term="Network Security" /><summary type="html"><![CDATA[Network architecture is one of those foundational decisions that shapes everything you build on top of it. Get it right at the start, and you’ll have a scalable, secure platform that grows with your organisation. Get it wrong, and you’ll spend years fighting technical debt, security vulnerabilities, and operational complexity that compounds with every new workload you deploy.]]></summary></entry><entry><title type="html">Building a Centralised Azure Container Registry: A Platform Engineering Guide</title><link href="https://blog.glen-thomas.com/platform%20engineering/2025/10/14/building-a-centralised-azure-container-registry.html" rel="alternate" type="text/html" title="Building a Centralised Azure Container Registry: A Platform Engineering Guide" /><published>2025-10-14T22:19:00+01:00</published><updated>2025-10-14T22:19:00+01:00</updated><id>https://blog.glen-thomas.com/platform%20engineering/2025/10/14/building-a-centralised-azure-container-registry</id><content type="html" xml:base="https://blog.glen-thomas.com/platform%20engineering/2025/10/14/building-a-centralised-azure-container-registry.html"><![CDATA[<p>When building container-based applications on Azure, you’ll inevitably need to decide how to manage your container images. Azure Container Registry (ACR) is the natural choice for Azure workloads, but the real question is whether to create multiple registries across teams and environments, or build a single, centralised registry that serves your entire organisation.</p>

<p>In this comprehensive guide, I’ll walk you through building a centralised Azure Container Registry that serves as the single source of truth for container images across your organisation. We’ll explore the architectural considerations, implement the infrastructure using Terraform with security best practices baked in, and create GitHub Actions workflows that integrate seamlessly with your registry.</p>

<h2 id="why-centralise-your-container-registry">Why Centralise Your Container Registry?</h2>

<p>Before we dive into the implementation, it’s worth understanding why a centralised approach often makes more sense than having multiple registries scattered across your Azure subscriptions.</p>

<p>The most compelling reason is cost efficiency. Azure Container Registry pricing is based on storage and data egress, and consolidating images into a single registry reduces both duplication and management overhead. When multiple teams build similar base images or share common dependencies, a centralised registry with proper namespace organisation prevents storing the same layers multiple times.</p>

<p>Security and governance become significantly simpler with centralisation. Rather than managing access policies, network rules, and compliance requirements across multiple registries, you have a single point of control. This doesn’t mean sacrificing isolation. Azure Container Registry supports repository-scoped permissions that allow you to maintain strict boundaries between teams whilst sharing the infrastructure.</p>

<p>From an operational perspective, centralisation makes your platform easier to reason about. Your development teams know exactly where to push and pull images. Your security team has one place to scan for vulnerabilities. Your cost reporting becomes clearer. The cognitive load decreases as the architecture becomes more predictable.</p>

<h2 id="architectural-considerations">Architectural Considerations</h2>

<p>When designing a centralised container registry, several architectural decisions will shape your implementation. These aren’t merely technical choices, they reflect how your organisation operates and how your platform serves its users.</p>

<h3 id="network-topology-and-private-endpoints">Network Topology and Private Endpoints</h3>

<p>The first decision revolves around network accessibility. Whilst Azure Container Registry can operate with public endpoints, production environments almost universally require private network access. Using Azure Private Link, your ACR becomes accessible only through private endpoints within your virtual networks, ensuring container images never traverse the public internet.</p>

<p>This creates an interesting challenge: how do GitHub Actions runners, which operate outside your Azure network, push images to a private registry? The answer involves a hybrid approach where you maintain controlled public access for specific operations whilst keeping the registry predominantly private. We’ll implement this using network rules that allow GitHub’s IP ranges for push operations whilst restricting pulls to your private network.</p>

<h3 id="multi-tenancy-and-repository-organisation">Multi-Tenancy and Repository Organisation</h3>

<p>A centralised registry serves multiple teams, and establishing a clear organisational structure from the start prevents chaos as adoption grows. The pattern I recommend involves using repository namespaces that mirror your organisational structure. For example, images might follow a structure like <code class="language-plaintext highlighter-rouge">teamname/application/service:tag</code> rather than a flat namespace.</p>

<p>This provides natural isolation and makes it immediately clear who owns which images. Combined with Azure RBAC at the repository level, you can ensure teams can only push to their namespaces whilst potentially allowing broader pull access across the organisation.</p>

<h3 id="sku-selection-and-geo-replication">SKU Selection and Geo-Replication</h3>

<p>Azure Container Registry offers three SKUs: Basic, Standard, and Premium. For a production centralised registry, Premium is almost always the right choice. Beyond the increased storage and throughput, Premium unlocks critical features like geo-replication, customer-managed keys, and dedicated data endpoints.</p>

<p>Geo-replication deserves particular attention. If your AKS clusters or Container Apps exist in multiple Azure regions, replicating your registry to those regions dramatically improves image pull performance and resilience. The registry automatically routes pull requests to the nearest replica, reducing latency and eliminating cross-region data egress charges for pulls.</p>

<h2 id="infrastructure-as-code-with-terraform">Infrastructure as Code with Terraform</h2>

<p>Let’s translate these architectural principles into actual infrastructure. We’ll build this incrementally, starting with the core registry and progressively adding security layers.</p>

<h3 id="core-registry-infrastructure">Core Registry Infrastructure</h3>

<p>Our foundation begins with a Premium SKU registry configured for high availability. Create a new file called <code class="language-plaintext highlighter-rouge">acr.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Resource group for the centralised container registry</span>
<span class="nx">resource</span> <span class="s2">"azurerm_resource_group"</span> <span class="s2">"acr"</span> <span class="p">{</span>
  <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"rg-acr-${var.environment}-${var.location}"</span>
  <span class="nx">location</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">location</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
    <span class="nx">purpose</span>     <span class="o">=</span> <span class="s2">"centralised-container-registry"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Premium ACR with enhanced features enabled</span>
<span class="nx">resource</span> <span class="s2">"azurerm_container_registry"</span> <span class="s2">"main"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"acr${var.organisation}${var.environment}"</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">sku</span>                 <span class="o">=</span> <span class="s2">"Premium"</span>
  <span class="nx">admin_enabled</span>       <span class="o">=</span> <span class="kc">false</span>

  <span class="c1"># Enable anonymous pull for public base images (optional)</span>
  <span class="nx">anonymous_pull_enabled</span> <span class="o">=</span> <span class="kc">false</span>

  <span class="c1"># Network access default action</span>
  <span class="nx">public_network_access_enabled</span> <span class="o">=</span> <span class="kc">true</span>
  <span class="nx">network_rule_bypass_option</span>    <span class="o">=</span> <span class="s2">"AzureServices"</span>

  <span class="c1"># Enable zone redundancy for high availability</span>
  <span class="nx">zone_redundancy_enabled</span> <span class="o">=</span> <span class="kc">true</span>

  <span class="c1"># Encryption with customer-managed keys</span>
  <span class="nx">encryption</span> <span class="p">{</span>
    <span class="nx">enabled</span>            <span class="o">=</span> <span class="kc">true</span>
    <span class="nx">key_vault_key_id</span>   <span class="o">=</span> <span class="nx">azurerm_key_vault_key</span><span class="p">.</span><span class="nx">acr_encryption</span><span class="p">.</span><span class="nx">id</span>
    <span class="nx">identity_client_id</span> <span class="o">=</span> <span class="nx">azurerm_user_assigned_identity</span><span class="p">.</span><span class="nx">acr_encryption</span><span class="p">.</span><span class="nx">client_id</span>
  <span class="p">}</span>

  <span class="c1"># Enable the retention policy for untagged manifests</span>
  <span class="nx">retention_policy</span> <span class="p">{</span>
    <span class="nx">days</span>    <span class="o">=</span> <span class="mi">7</span>
    <span class="nx">enabled</span> <span class="o">=</span> <span class="kc">true</span>
  <span class="p">}</span>

  <span class="c1"># Trust policy for content trust</span>
  <span class="nx">trust_policy</span> <span class="p">{</span>
    <span class="nx">enabled</span> <span class="o">=</span> <span class="kc">true</span>
  <span class="p">}</span>

  <span class="c1"># Quarantine policy to scan images before making them available</span>
  <span class="nx">quarantine_policy</span> <span class="p">{</span>
    <span class="nx">enabled</span> <span class="o">=</span> <span class="kc">true</span>
  <span class="p">}</span>

  <span class="nx">identity</span> <span class="p">{</span>
    <span class="nx">type</span> <span class="o">=</span> <span class="s2">"UserAssigned"</span>
    <span class="nx">identity_ids</span> <span class="o">=</span> <span class="p">[</span>
      <span class="nx">azurerm_user_assigned_identity</span><span class="p">.</span><span class="nx">acr_encryption</span><span class="p">.</span><span class="nx">id</span>
    <span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
  <span class="p">}</span>

  <span class="nx">depends_on</span> <span class="o">=</span> <span class="p">[</span>
    <span class="nx">azurerm_key_vault_access_policy</span><span class="p">.</span><span class="nx">acr_encryption</span>
  <span class="p">]</span>
<span class="err">}</span>

<span class="c1"># Geo-replication to additional regions</span>
<span class="nx">resource</span> <span class="s2">"azurerm_container_registry_replication"</span> <span class="s2">"replicas"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">toset</span><span class="p">(</span><span class="nx">var</span><span class="p">.</span><span class="nx">replication_regions</span><span class="p">)</span>

  <span class="nx">name</span>                      <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span>
  <span class="nx">container_registry_name</span>   <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">resource_group_name</span>       <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">location</span>                  <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span>
  <span class="nx">zone_redundancy_enabled</span>   <span class="o">=</span> <span class="kc">true</span>
  <span class="nx">regional_endpoint_enabled</span> <span class="o">=</span> <span class="kc">true</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This configuration establishes several important security baselines. Admin credentials are explicitly disabled, forcing all authentication to use Azure AD identities. Zone redundancy ensures the registry remains available even if an availability zone fails. The retention policy automatically cleans up untagged manifests after seven days, preventing storage bloat from ephemeral CI builds.</p>

<h3 id="encryption-and-key-management">Encryption and Key Management</h3>

<p>Security-conscious organisations require customer-managed encryption keys rather than Microsoft-managed keys. This gives you complete control over key rotation and access. Create <code class="language-plaintext highlighter-rouge">encryption.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># User-assigned managed identity for ACR encryption</span>
<span class="nx">resource</span> <span class="s2">"azurerm_user_assigned_identity"</span> <span class="s2">"acr_encryption"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"id-acr-encryption-${var.environment}"</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">location</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Key Vault for storing encryption keys</span>
<span class="nx">resource</span> <span class="s2">"azurerm_key_vault"</span> <span class="s2">"acr"</span> <span class="p">{</span>
  <span class="nx">name</span>                       <span class="o">=</span> <span class="s2">"kv-acr-${var.environment}-${random_string.key_vault_suffix.result}"</span>
  <span class="nx">location</span>                   <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span>        <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">tenant_id</span>                  <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">azurerm_client_config</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">tenant_id</span>
  <span class="nx">sku_name</span>                   <span class="o">=</span> <span class="s2">"premium"</span>
  <span class="nx">soft_delete_retention_days</span> <span class="o">=</span> <span class="mi">90</span>
  <span class="nx">purge_protection_enabled</span>   <span class="o">=</span> <span class="kc">true</span>

  <span class="c1"># Network rules to restrict access</span>
  <span class="nx">network_acls</span> <span class="p">{</span>
    <span class="nx">bypass</span>         <span class="o">=</span> <span class="s2">"AzureServices"</span>
    <span class="nx">default_action</span> <span class="o">=</span> <span class="s2">"Deny"</span>

    <span class="nx">ip_rules</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">admin_ip_allowlist</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Random suffix to ensure Key Vault name uniqueness</span>
<span class="nx">resource</span> <span class="s2">"random_string"</span> <span class="s2">"key_vault_suffix"</span> <span class="p">{</span>
  <span class="nx">length</span>  <span class="o">=</span> <span class="mi">4</span>
  <span class="nx">special</span> <span class="o">=</span> <span class="kc">false</span>
  <span class="nx">upper</span>   <span class="o">=</span> <span class="kc">false</span>
<span class="p">}</span>

<span class="c1"># Access policy for the ACR managed identity</span>
<span class="nx">resource</span> <span class="s2">"azurerm_key_vault_access_policy"</span> <span class="s2">"acr_encryption"</span> <span class="p">{</span>
  <span class="nx">key_vault_id</span> <span class="o">=</span> <span class="nx">azurerm_key_vault</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">tenant_id</span>    <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">azurerm_client_config</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">tenant_id</span>
  <span class="nx">object_id</span>    <span class="o">=</span> <span class="nx">azurerm_user_assigned_identity</span><span class="p">.</span><span class="nx">acr_encryption</span><span class="p">.</span><span class="nx">principal_id</span>

  <span class="nx">key_permissions</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"Get"</span><span class="p">,</span>
    <span class="s2">"UnwrapKey"</span><span class="p">,</span>
    <span class="s2">"WrapKey"</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="c1"># Access policy for Terraform service principal</span>
<span class="nx">resource</span> <span class="s2">"azurerm_key_vault_access_policy"</span> <span class="s2">"terraform"</span> <span class="p">{</span>
  <span class="nx">key_vault_id</span> <span class="o">=</span> <span class="nx">azurerm_key_vault</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">tenant_id</span>    <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">azurerm_client_config</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">tenant_id</span>
  <span class="nx">object_id</span>    <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">azurerm_client_config</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">object_id</span>

  <span class="nx">key_permissions</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"Get"</span><span class="p">,</span>
    <span class="s2">"Create"</span><span class="p">,</span>
    <span class="s2">"Delete"</span><span class="p">,</span>
    <span class="s2">"List"</span><span class="p">,</span>
    <span class="s2">"Purge"</span><span class="p">,</span>
    <span class="s2">"Recover"</span><span class="p">,</span>
    <span class="s2">"GetRotationPolicy"</span><span class="p">,</span>
    <span class="s2">"SetRotationPolicy"</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="c1"># Customer-managed encryption key</span>
<span class="nx">resource</span> <span class="s2">"azurerm_key_vault_key"</span> <span class="s2">"acr_encryption"</span> <span class="p">{</span>
  <span class="nx">name</span>         <span class="o">=</span> <span class="s2">"acr-encryption-key"</span>
  <span class="nx">key_vault_id</span> <span class="o">=</span> <span class="nx">azurerm_key_vault</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">key_type</span>     <span class="o">=</span> <span class="s2">"RSA"</span>
  <span class="nx">key_size</span>     <span class="o">=</span> <span class="mi">2048</span>

  <span class="nx">key_opts</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s2">"decrypt"</span><span class="p">,</span>
    <span class="s2">"encrypt"</span><span class="p">,</span>
    <span class="s2">"sign"</span><span class="p">,</span>
    <span class="s2">"unwrapKey"</span><span class="p">,</span>
    <span class="s2">"verify"</span><span class="p">,</span>
    <span class="s2">"wrapKey"</span>
  <span class="p">]</span>

  <span class="nx">rotation_policy</span> <span class="p">{</span>
    <span class="nx">automatic</span> <span class="p">{</span>
      <span class="nx">time_before_expiry</span> <span class="o">=</span> <span class="s2">"P30D"</span>
    <span class="p">}</span>

    <span class="nx">expire_after</span>         <span class="o">=</span> <span class="s2">"P90D"</span>
    <span class="nx">notify_before_expiry</span> <span class="o">=</span> <span class="s2">"P29D"</span>
  <span class="p">}</span>

  <span class="nx">depends_on</span> <span class="o">=</span> <span class="p">[</span>
    <span class="nx">azurerm_key_vault_access_policy</span><span class="p">.</span><span class="nx">terraform</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="nx">data</span> <span class="s2">"azurerm_client_config"</span> <span class="s2">"current"</span> <span class="p">{}</span>
</code></pre></div></div>

<p>This setup demonstrates defence in depth. The Key Vault itself sits behind network restrictions, accepting connections only from Azure services and specified admin IPs. Soft delete with purge protection prevents accidental key deletion from destroying your encrypted data. The automatic rotation policy ensures keys are refreshed every 90 days without manual intervention.</p>

<h3 id="network-security-and-private-endpoints">Network Security and Private Endpoints</h3>

<p>Now we implement the network security layer that restricts registry access to your private networks whilst allowing controlled access from GitHub Actions. Create <code class="language-plaintext highlighter-rouge">networking.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Private DNS zone for ACR</span>
<span class="nx">resource</span> <span class="s2">"azurerm_private_dns_zone"</span> <span class="s2">"acr"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"privatelink.azurecr.io"</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Link DNS zone to hub VNet</span>
<span class="nx">resource</span> <span class="s2">"azurerm_private_dns_zone_virtual_network_link"</span> <span class="s2">"acr_hub"</span> <span class="p">{</span>
  <span class="nx">name</span>                  <span class="o">=</span> <span class="s2">"acr-hub-link"</span>
  <span class="nx">resource_group_name</span>   <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">private_dns_zone_name</span> <span class="o">=</span> <span class="nx">azurerm_private_dns_zone</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">virtual_network_id</span>    <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">hub_vnet_id</span>
  <span class="nx">registration_enabled</span>  <span class="o">=</span> <span class="kc">false</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Link DNS zone to AKS spoke VNets</span>
<span class="nx">resource</span> <span class="s2">"azurerm_private_dns_zone_virtual_network_link"</span> <span class="s2">"acr_aks_spokes"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">aks_spoke_vnet_ids</span>

  <span class="nx">name</span>                  <span class="o">=</span> <span class="s2">"acr-aks-${each.key}-link"</span>
  <span class="nx">resource_group_name</span>   <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">private_dns_zone_name</span> <span class="o">=</span> <span class="nx">azurerm_private_dns_zone</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">virtual_network_id</span>    <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span>
  <span class="nx">registration_enabled</span>  <span class="o">=</span> <span class="kc">false</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Subnet for ACR private endpoint</span>
<span class="nx">resource</span> <span class="s2">"azurerm_subnet"</span> <span class="s2">"acr_endpoints"</span> <span class="p">{</span>
  <span class="nx">name</span>                 <span class="o">=</span> <span class="s2">"snet-acr-endpoints"</span>
  <span class="nx">resource_group_name</span>  <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">hub_vnet_resource_group</span>
  <span class="nx">virtual_network_name</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">hub_vnet_name</span>
  <span class="nx">address_prefixes</span>     <span class="o">=</span> <span class="p">[</span><span class="nx">var</span><span class="p">.</span><span class="nx">acr_endpoint_subnet_cidr</span><span class="p">]</span>

  <span class="nx">private_endpoint_network_policies_enabled</span> <span class="o">=</span> <span class="kc">false</span>
<span class="p">}</span>

<span class="c1"># Private endpoint for ACR</span>
<span class="nx">resource</span> <span class="s2">"azurerm_private_endpoint"</span> <span class="s2">"acr"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"pe-acr-${var.environment}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">subnet_id</span>           <span class="o">=</span> <span class="nx">azurerm_subnet</span><span class="p">.</span><span class="nx">acr_endpoints</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">private_service_connection</span> <span class="p">{</span>
    <span class="nx">name</span>                           <span class="o">=</span> <span class="s2">"acr-private-connection"</span>
    <span class="nx">private_connection_resource_id</span> <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>
    <span class="nx">is_manual_connection</span>           <span class="o">=</span> <span class="kc">false</span>
    <span class="nx">subresource_names</span>              <span class="o">=</span> <span class="p">[</span><span class="s2">"registry"</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">private_dns_zone_group</span> <span class="p">{</span>
    <span class="nx">name</span>                 <span class="o">=</span> <span class="s2">"acr-dns-zone-group"</span>
    <span class="nx">private_dns_zone_ids</span> <span class="o">=</span> <span class="p">[</span><span class="nx">azurerm_private_dns_zone</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Network rules for ACR - allow GitHub Actions</span>
<span class="nx">resource</span> <span class="s2">"azurerm_container_registry_network_rule_set"</span> <span class="s2">"main"</span> <span class="p">{</span>
  <span class="nx">container_registry_id</span> <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">default_action</span> <span class="o">=</span> <span class="s2">"Deny"</span>

  <span class="c1"># Allow GitHub Actions IP ranges for image push</span>
  <span class="nx">ip_rule</span> <span class="o">=</span> <span class="p">[</span>
    <span class="nx">for</span> <span class="nx">cidr</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">github_actions_ip_ranges</span> <span class="o">:</span> <span class="p">{</span>
      <span class="nx">action</span>   <span class="o">=</span> <span class="s2">"Allow"</span>
      <span class="nx">ip_range</span> <span class="o">=</span> <span class="nx">cidr</span>
    <span class="p">}</span>
  <span class="p">]</span>

  <span class="c1"># Allow Azure services</span>
  <span class="nx">virtual_network</span> <span class="o">=</span> <span class="p">[]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This network configuration creates a crucial security boundary. The private endpoint places your registry inside your virtual network, making it accessible to AKS clusters and Container Apps through private IP addresses. The private DNS zone ensures that when your applications resolve the registry’s hostname, they receive the private IP rather than the public one.</p>

<p>The network rule set demonstrates the hybrid approach I mentioned earlier. By allowing GitHub’s IP ranges whilst denying everything else by default, we enable CI/CD workflows to push images whilst preventing unauthorised access. In production, you might further restrict this by using self-hosted GitHub Actions runners within your network, eliminating the need for public access entirely.</p>

<h3 id="identity-and-access-management">Identity and Access Management</h3>

<p>Proper access control ensures teams can push to their namespaces whilst the registry remains secure. Create <code class="language-plaintext highlighter-rouge">iam.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Service principal for GitHub Actions with OIDC federated credentials</span>
<span class="nx">resource</span> <span class="s2">"azuread_application"</span> <span class="s2">"github_actions"</span> <span class="p">{</span>
  <span class="nx">display_name</span> <span class="o">=</span> <span class="s2">"sp-acr-github-actions-${var.environment}"</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azuread_service_principal"</span> <span class="s2">"github_actions"</span> <span class="p">{</span>
  <span class="nx">client_id</span> <span class="o">=</span> <span class="nx">azuread_application</span><span class="p">.</span><span class="nx">github_actions</span><span class="p">.</span><span class="nx">client_id</span>
<span class="p">}</span>

<span class="c1"># Federated identity credential for GitHub OIDC (main branch)</span>
<span class="nx">resource</span> <span class="s2">"azuread_application_federated_identity_credential"</span> <span class="s2">"github_actions_main"</span> <span class="p">{</span>
  <span class="nx">application_id</span> <span class="o">=</span> <span class="nx">azuread_application</span><span class="p">.</span><span class="nx">github_actions</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">display_name</span>   <span class="o">=</span> <span class="s2">"github-actions-main-branch"</span>
  <span class="nx">description</span>    <span class="o">=</span> <span class="s2">"Federated credential for GitHub Actions main branch"</span>
  <span class="nx">audiences</span>      <span class="o">=</span> <span class="p">[</span><span class="s2">"api://AzureADTokenExchange"</span><span class="p">]</span>
  <span class="nx">issuer</span>         <span class="o">=</span> <span class="s2">"https://token.actions.githubusercontent.com"</span>
  <span class="nx">subject</span>        <span class="o">=</span> <span class="s2">"repo:${var.github_org}/${var.github_repo}:ref:refs/heads/main"</span>
<span class="p">}</span>

<span class="c1"># Federated identity credential for GitHub OIDC (pull requests)</span>
<span class="nx">resource</span> <span class="s2">"azuread_application_federated_identity_credential"</span> <span class="s2">"github_actions_pr"</span> <span class="p">{</span>
  <span class="nx">application_id</span> <span class="o">=</span> <span class="nx">azuread_application</span><span class="p">.</span><span class="nx">github_actions</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">display_name</span>   <span class="o">=</span> <span class="s2">"github-actions-pull-requests"</span>
  <span class="nx">description</span>    <span class="o">=</span> <span class="s2">"Federated credential for GitHub Actions pull requests"</span>
  <span class="nx">audiences</span>      <span class="o">=</span> <span class="p">[</span><span class="s2">"api://AzureADTokenExchange"</span><span class="p">]</span>
  <span class="nx">issuer</span>         <span class="o">=</span> <span class="s2">"https://token.actions.githubusercontent.com"</span>
  <span class="nx">subject</span>        <span class="o">=</span> <span class="s2">"repo:${var.github_org}/${var.github_repo}:pull_request"</span>
<span class="p">}</span>

<span class="c1"># Federated identity credential for GitHub OIDC (develop branch)</span>
<span class="nx">resource</span> <span class="s2">"azuread_application_federated_identity_credential"</span> <span class="s2">"github_actions_develop"</span> <span class="p">{</span>
  <span class="nx">application_id</span> <span class="o">=</span> <span class="nx">azuread_application</span><span class="p">.</span><span class="nx">github_actions</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">display_name</span>   <span class="o">=</span> <span class="s2">"github-actions-develop-branch"</span>
  <span class="nx">description</span>    <span class="o">=</span> <span class="s2">"Federated credential for GitHub Actions develop branch"</span>
  <span class="nx">audiences</span>      <span class="o">=</span> <span class="p">[</span><span class="s2">"api://AzureADTokenExchange"</span><span class="p">]</span>
  <span class="nx">issuer</span>         <span class="o">=</span> <span class="s2">"https://token.actions.githubusercontent.com"</span>
  <span class="nx">subject</span>        <span class="o">=</span> <span class="s2">"repo:${var.github_org}/${var.github_repo}:ref:refs/heads/develop"</span>
<span class="p">}</span>

<span class="c1"># Role assignment for GitHub Actions - push access</span>
<span class="nx">resource</span> <span class="s2">"azurerm_role_assignment"</span> <span class="s2">"github_actions_push"</span> <span class="p">{</span>
  <span class="nx">scope</span>                <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">role_definition_name</span> <span class="o">=</span> <span class="s2">"AcrPush"</span>
  <span class="nx">principal_id</span>         <span class="o">=</span> <span class="nx">azuread_service_principal</span><span class="p">.</span><span class="nx">github_actions</span><span class="p">.</span><span class="nx">object_id</span>
<span class="p">}</span>

<span class="c1"># Managed identities for AKS clusters</span>
<span class="nx">resource</span> <span class="s2">"azurerm_user_assigned_identity"</span> <span class="s2">"aks_clusters"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">aks_clusters</span>

  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"id-aks-${each.key}-${var.environment}"</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">location</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
    <span class="nx">cluster</span>     <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">key</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Role assignment for AKS - pull access</span>
<span class="nx">resource</span> <span class="s2">"azurerm_role_assignment"</span> <span class="s2">"aks_pull"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">azurerm_user_assigned_identity</span><span class="p">.</span><span class="nx">aks_clusters</span>

  <span class="nx">scope</span>                <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">role_definition_name</span> <span class="o">=</span> <span class="s2">"AcrPull"</span>
  <span class="nx">principal_id</span>         <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">principal_id</span>
<span class="p">}</span>

<span class="c1"># Managed identities for Container Apps</span>
<span class="nx">resource</span> <span class="s2">"azurerm_user_assigned_identity"</span> <span class="s2">"container_apps"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">container_app_environments</span>

  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"id-containerapp-${each.key}-${var.environment}"</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">location</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
    <span class="nx">app_env</span>     <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">key</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Role assignment for Container Apps - pull access</span>
<span class="nx">resource</span> <span class="s2">"azurerm_role_assignment"</span> <span class="s2">"container_apps_pull"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="o">=</span> <span class="nx">azurerm_user_assigned_identity</span><span class="p">.</span><span class="nx">container_apps</span>

  <span class="nx">scope</span>                <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">role_definition_name</span> <span class="o">=</span> <span class="s2">"AcrPull"</span>
  <span class="nx">principal_id</span>         <span class="o">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">principal_id</span>
<span class="p">}</span>

<span class="c1"># Custom role for team-specific repository access (example)</span>
<span class="nx">resource</span> <span class="s2">"azurerm_role_definition"</span> <span class="s2">"team_repository_contributor"</span> <span class="p">{</span>
  <span class="nx">name</span>  <span class="o">=</span> <span class="s2">"ACR Team Repository Contributor"</span>
  <span class="nx">scope</span> <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">permissions</span> <span class="p">{</span>
    <span class="nx">actions</span> <span class="o">=</span> <span class="p">[</span>
      <span class="s2">"Microsoft.ContainerRegistry/registries/pull/read"</span><span class="p">,</span>
      <span class="s2">"Microsoft.ContainerRegistry/registries/push/write"</span><span class="p">,</span>
      <span class="s2">"Microsoft.ContainerRegistry/registries/artifacts/delete"</span>
    <span class="p">]</span>

    <span class="nx">data_actions</span> <span class="o">=</span> <span class="p">[</span>
      <span class="s2">"Microsoft.ContainerRegistry/registries/*/read"</span><span class="p">,</span>
      <span class="s2">"Microsoft.ContainerRegistry/registries/*/write"</span><span class="p">,</span>
      <span class="s2">"Microsoft.ContainerRegistry/registries/*/delete"</span>
    <span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">assignable_scopes</span> <span class="o">=</span> <span class="p">[</span>
    <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>
  <span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This IAM configuration follows the principle of least privilege. GitHub Actions receives only push permissions, not the ability to delete or modify the registry itself. AKS clusters and Container Apps receive pull-only access since they never need to push images. The custom role definition provides a template for team-specific access that can be scoped to particular repositories.</p>

<h3 id="monitoring-and-diagnostics">Monitoring and Diagnostics</h3>

<p>Observability is crucial for a centralised service that multiple teams depend upon. Create <code class="language-plaintext highlighter-rouge">monitoring.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Log Analytics workspace for ACR diagnostics</span>
<span class="nx">resource</span> <span class="s2">"azurerm_log_analytics_workspace"</span> <span class="s2">"acr"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"log-acr-${var.environment}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">sku</span>                 <span class="o">=</span> <span class="s2">"PerGB2018"</span>
  <span class="nx">retention_in_days</span>   <span class="o">=</span> <span class="mi">30</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Diagnostic settings for ACR</span>
<span class="nx">resource</span> <span class="s2">"azurerm_monitor_diagnostic_setting"</span> <span class="s2">"acr"</span> <span class="p">{</span>
  <span class="nx">name</span>                       <span class="o">=</span> <span class="s2">"acr-diagnostics"</span>
  <span class="nx">target_resource_id</span>         <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">log_analytics_workspace_id</span> <span class="o">=</span> <span class="nx">azurerm_log_analytics_workspace</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">enabled_log</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"ContainerRegistryRepositoryEvents"</span>
  <span class="p">}</span>

  <span class="nx">enabled_log</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"ContainerRegistryLoginEvents"</span>
  <span class="p">}</span>

  <span class="nx">metric</span> <span class="p">{</span>
    <span class="nx">category</span> <span class="o">=</span> <span class="s2">"AllMetrics"</span>
    <span class="nx">enabled</span>  <span class="o">=</span> <span class="kc">true</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Alert for failed authentication attempts</span>
<span class="nx">resource</span> <span class="s2">"azurerm_monitor_metric_alert"</span> <span class="s2">"auth_failures"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"acr-auth-failures-${var.environment}"</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">scopes</span>              <span class="o">=</span> <span class="p">[</span><span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span>
  <span class="nx">description</span>         <span class="o">=</span> <span class="s2">"Alert when ACR authentication failures exceed threshold"</span>
  <span class="nx">severity</span>            <span class="o">=</span> <span class="mi">2</span>
  <span class="nx">frequency</span>           <span class="o">=</span> <span class="s2">"PT5M"</span>
  <span class="nx">window_size</span>         <span class="o">=</span> <span class="s2">"PT15M"</span>

  <span class="nx">criteria</span> <span class="p">{</span>
    <span class="nx">metric_namespace</span> <span class="o">=</span> <span class="s2">"Microsoft.ContainerRegistry/registries"</span>
    <span class="nx">metric_name</span>      <span class="o">=</span> <span class="s2">"TotalPullCount"</span>
    <span class="nx">aggregation</span>      <span class="o">=</span> <span class="s2">"Total"</span>
    <span class="nx">operator</span>         <span class="o">=</span> <span class="s2">"GreaterThan"</span>
    <span class="nx">threshold</span>        <span class="o">=</span> <span class="mi">100</span>

    <span class="nx">dimension</span> <span class="p">{</span>
      <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"StatusCode"</span>
      <span class="nx">operator</span> <span class="o">=</span> <span class="s2">"Include"</span>
      <span class="nx">values</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">"401"</span><span class="p">,</span> <span class="s2">"403"</span><span class="p">]</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nx">action</span> <span class="p">{</span>
    <span class="nx">action_group_id</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">security_alert_action_group_id</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Alert for storage usage</span>
<span class="nx">resource</span> <span class="s2">"azurerm_monitor_metric_alert"</span> <span class="s2">"storage_usage"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"acr-storage-usage-${var.environment}"</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">scopes</span>              <span class="o">=</span> <span class="p">[</span><span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span>
  <span class="nx">description</span>         <span class="o">=</span> <span class="s2">"Alert when ACR storage usage exceeds 80%"</span>
  <span class="nx">severity</span>            <span class="o">=</span> <span class="mi">3</span>
  <span class="nx">frequency</span>           <span class="o">=</span> <span class="s2">"PT1H"</span>
  <span class="nx">window_size</span>         <span class="o">=</span> <span class="s2">"PT1H"</span>

  <span class="nx">criteria</span> <span class="p">{</span>
    <span class="nx">metric_namespace</span> <span class="o">=</span> <span class="s2">"Microsoft.ContainerRegistry/registries"</span>
    <span class="nx">metric_name</span>      <span class="o">=</span> <span class="s2">"StorageUsed"</span>
    <span class="nx">aggregation</span>      <span class="o">=</span> <span class="s2">"Average"</span>
    <span class="nx">operator</span>         <span class="o">=</span> <span class="s2">"GreaterThan"</span>
    <span class="nx">threshold</span>        <span class="o">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">storage_alert_threshold_bytes</span>
  <span class="p">}</span>

  <span class="nx">action</span> <span class="p">{</span>
    <span class="nx">action_group_id</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">platform_alert_action_group_id</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1"># Workbook for ACR usage analytics</span>
<span class="nx">resource</span> <span class="s2">"azurerm_application_insights_workbook"</span> <span class="s2">"acr_usage"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"workbook-acr-usage-${var.environment}"</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">display_name</span>        <span class="o">=</span> <span class="s2">"ACR Usage Analytics"</span>

  <span class="nx">data_json</span> <span class="o">=</span> <span class="nx">jsonencode</span><span class="p">({</span>
    <span class="nx">version</span> <span class="o">=</span> <span class="s2">"Notebook/1.0"</span>
    <span class="nx">items</span> <span class="o">=</span> <span class="p">[</span>
      <span class="p">{</span>
        <span class="nx">type</span> <span class="o">=</span> <span class="mi">1</span>
        <span class="nx">content</span> <span class="o">=</span> <span class="p">{</span>
          <span class="nx">json</span> <span class="o">=</span> <span class="s2">"## Container Registry Usage Analytics</span><span class="err">\</span><span class="s2">n</span><span class="err">\</span><span class="s2">nThis workbook provides insights into registry usage, image pulls, authentication events, and storage consumption."</span>
        <span class="p">}</span>
      <span class="p">},</span>
      <span class="p">{</span>
        <span class="nx">type</span> <span class="o">=</span> <span class="mi">3</span>
        <span class="nx">content</span> <span class="o">=</span> <span class="p">{</span>
          <span class="nx">version</span> <span class="o">=</span> <span class="s2">"KqlItem/1.0"</span>
          <span class="nx">query</span> <span class="o">=</span> <span class="s2">"ContainerRegistryRepositoryEvents</span><span class="err">\</span><span class="s2">n| where TimeGenerated &gt; ago(7d)</span><span class="err">\</span><span class="s2">n| summarize PullCount = countif(OperationName == 'Pull'), PushCount = countif(OperationName == 'Push') by bin(TimeGenerated, 1h), Repository</span><span class="err">\</span><span class="s2">n| render timechart"</span>
          <span class="nx">size</span> <span class="o">=</span> <span class="mi">0</span>
          <span class="nx">title</span> <span class="o">=</span> <span class="s2">"Pull and Push Operations by Repository (Last 7 Days)"</span>
          <span class="nx">queryType</span> <span class="o">=</span> <span class="mi">0</span>
          <span class="nx">resourceType</span> <span class="o">=</span> <span class="s2">"microsoft.operationalinsights/workspaces"</span>
        <span class="err">}</span>
      <span class="p">}</span>
    <span class="err">]</span>
  <span class="p">})</span>

  <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">environment</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
    <span class="nx">managed_by</span>  <span class="o">=</span> <span class="s2">"terraform"</span>
  <span class="p">}</span>
<span class="err">}</span>
</code></pre></div></div>

<p>These monitoring resources provide comprehensive visibility into registry operations. Authentication failures might indicate misconfigured applications or potential security incidents. Storage usage alerts prevent unexpected cost increases. The workbook gives platform teams an at-a-glance view of which repositories are most active and how the registry is being used.</p>

<h3 id="variables-and-outputs">Variables and Outputs</h3>

<p>To make this Terraform configuration reusable, create <code class="language-plaintext highlighter-rouge">variables.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"environment"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Environment name (e.g., prod, staging)"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"location"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Primary Azure region"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="s2">"uksouth"</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"organisation"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Organisation name for resource naming"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"replication_regions"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Additional regions for ACR geo-replication"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="p">[</span><span class="s2">"ukwest"</span><span class="p">,</span> <span class="s2">"northeurope"</span><span class="p">]</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"hub_vnet_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Resource ID of hub VNet"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"hub_vnet_name"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Name of hub VNet"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"hub_vnet_resource_group"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Resource group of hub VNet"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"acr_endpoint_subnet_cidr"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"CIDR block for ACR private endpoint subnet"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"aks_spoke_vnet_ids"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Map of AKS spoke VNet IDs for DNS zone linking"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">map</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"aks_clusters"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Map of AKS cluster names for managed identity creation"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">map</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"container_app_environments"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Map of Container App environment names"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">map</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="p">{}</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"github_actions_ip_ranges"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"IP ranges for GitHub Actions runners"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span> <span class="o">=</span> <span class="p">[</span>
    <span class="c1"># These are example ranges - use actual GitHub IP ranges</span>
    <span class="s2">"140.82.112.0/20"</span><span class="p">,</span>
    <span class="s2">"143.55.64.0/20"</span><span class="p">,</span>
    <span class="s2">"185.199.108.0/22"</span><span class="p">,</span>
    <span class="s2">"192.30.252.0/22"</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"admin_ip_allowlist"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"IP addresses allowed to access Key Vault"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"security_alert_action_group_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Action group ID for security alerts"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"platform_alert_action_group_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Action group ID for platform alerts"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"storage_alert_threshold_bytes"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Storage threshold for alerts in bytes"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">number</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="mi">1099511627776</span> <span class="c1"># 1TB</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"github_org"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"GitHub organisation name"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="nx">variable</span> <span class="s2">"github_repo"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"GitHub repository name"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">string</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And create <code class="language-plaintext highlighter-rouge">outputs.tf</code>:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">output</span> <span class="s2">"registry_name"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Name of the container registry"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">name</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"registry_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Resource ID of the container registry"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"registry_login_server"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Login server URL for the registry"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">login_server</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"github_actions_client_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Client ID for GitHub Actions service principal"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">azuread_application</span><span class="p">.</span><span class="nx">github_actions</span><span class="p">.</span><span class="nx">client_id</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"github_actions_tenant_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Azure AD tenant ID"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">azurerm_client_config</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">tenant_id</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"github_actions_subscription_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Azure subscription ID"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">azurerm_client_config</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">subscription_id</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"aks_identity_ids"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Map of AKS managed identity resource IDs"</span>
  <span class="nx">value</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">for</span> <span class="nx">k</span><span class="p">,</span> <span class="nx">v</span> <span class="nx">in</span> <span class="nx">azurerm_user_assigned_identity</span><span class="p">.</span><span class="nx">aks_clusters</span> <span class="o">:</span> <span class="nx">k</span> <span class="o">=&gt;</span> <span class="nx">v</span><span class="p">.</span><span class="nx">id</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"container_app_identity_ids"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Map of Container App managed identity resource IDs"</span>
  <span class="nx">value</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">for</span> <span class="nx">k</span><span class="p">,</span> <span class="nx">v</span> <span class="nx">in</span> <span class="nx">azurerm_user_assigned_identity</span><span class="p">.</span><span class="nx">container_apps</span> <span class="o">:</span> <span class="nx">k</span> <span class="o">=&gt;</span> <span class="nx">v</span><span class="p">.</span><span class="nx">id</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nx">output</span> <span class="s2">"private_endpoint_ip"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Private IP address of the ACR endpoint"</span>
  <span class="nx">value</span>       <span class="o">=</span> <span class="nx">azurerm_private_endpoint</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">private_service_connection</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">private_ip_address</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="github-actions-integration">GitHub Actions Integration</h2>

<p>With the infrastructure in place, let’s build GitHub Actions workflows that push images to our centralised registry. These workflows demonstrate production-ready practices including multi-stage builds, security scanning, and proper tagging strategies.</p>

<h3 id="reusable-docker-build-and-push-workflow">Reusable Docker Build and Push Workflow</h3>

<p>Create <code class="language-plaintext highlighter-rouge">.github/workflows/docker-build-push.yml</code> as a reusable workflow:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Build and Push Docker Image</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">workflow_call</span><span class="pi">:</span>
    <span class="na">inputs</span><span class="pi">:</span>
      <span class="na">image_name</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Name</span><span class="nv"> </span><span class="s">of</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">Docker</span><span class="nv"> </span><span class="s">image</span><span class="nv"> </span><span class="s">(without</span><span class="nv"> </span><span class="s">registry</span><span class="nv"> </span><span class="s">prefix)'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
      <span class="na">dockerfile_path</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Path</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">Dockerfile'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
        <span class="na">default</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./Dockerfile'</span>
      <span class="na">context_path</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Build</span><span class="nv"> </span><span class="s">context</span><span class="nv"> </span><span class="s">path'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
        <span class="na">default</span><span class="pi">:</span> <span class="s1">'</span><span class="s">.'</span>
      <span class="na">platforms</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Target</span><span class="nv"> </span><span class="s">platforms</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">multi-arch</span><span class="nv"> </span><span class="s">builds'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
        <span class="na">default</span><span class="pi">:</span> <span class="s1">'</span><span class="s">linux/amd64,linux/arm64'</span>
      <span class="na">push</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Whether</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">push</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">image'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">boolean</span>
        <span class="na">default</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">scan_image</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Whether</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">scan</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">vulnerabilities'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">false</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">boolean</span>
        <span class="na">default</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">secrets</span><span class="pi">:</span>
      <span class="na">AZURE_CLIENT_ID</span><span class="pi">:</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">AZURE_TENANT_ID</span><span class="pi">:</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">AZURE_SUBSCRIPTION_ID</span><span class="pi">:</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
      <span class="na">ACR_LOGIN_SERVER</span><span class="pi">:</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">outputs</span><span class="pi">:</span>
      <span class="na">image_tag</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">The</span><span class="nv"> </span><span class="s">full</span><span class="nv"> </span><span class="s">image</span><span class="nv"> </span><span class="s">tag</span><span class="nv"> </span><span class="s">that</span><span class="nv"> </span><span class="s">was</span><span class="nv"> </span><span class="s">built</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">pushed'</span>
        <span class="na">value</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">image_digest</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">The</span><span class="nv"> </span><span class="s">image</span><span class="nv"> </span><span class="s">digest</span><span class="nv"> </span><span class="s">SHA'</span>
        <span class="na">value</span><span class="pi">:</span> <span class="s">$</span>

<span class="na">permissions</span><span class="pi">:</span>
  <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
  <span class="na">security-events</span><span class="pi">:</span> <span class="s">write</span>
  <span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">outputs</span><span class="pi">:</span>
      <span class="na">image_tag</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">image_digest</span><span class="pi">:</span> <span class="s">$</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout code</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Azure Login via OIDC</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">azure/login@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">client-id</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">tenant-id</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">subscription-id</span><span class="pi">:</span> <span class="s">$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Docker Buildx</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">platforms</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">driver-opts</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">image=moby/buildkit:latest</span>
            <span class="s">network=host</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Azure Container Registry</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">az acr login --name $(echo $ | cut -d'.' -f1)</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Extract metadata for Docker</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">meta</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/metadata-action@v5</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">images</span><span class="pi">:</span> <span class="s">$/$</span>
          <span class="na">tags</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">type=ref,event=branch</span>
            <span class="s">type=ref,event=pr</span>
            <span class="s">type=semver,pattern=</span>
            <span class="s">type=semver,pattern=.</span>
            <span class="s">type=semver,pattern=</span>
            <span class="s">type=sha,prefix=-</span>
            <span class="s">type=raw,value=latest,enable=</span>
          <span class="na">labels</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">org.opencontainers.image.title=$</span>
            <span class="s">org.opencontainers.image.description=Built by GitHub Actions</span>
            <span class="s">org.opencontainers.image.vendor=$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build and push Docker image</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">build</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/build-push-action@v5</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">context</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">file</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">platforms</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">push</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">tags</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">labels</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">cache-from</span><span class="pi">:</span> <span class="s">type=registry,ref=$/$:buildcache</span>
          <span class="na">cache-to</span><span class="pi">:</span> <span class="s">type=registry,ref=$/$:buildcache,mode=max</span>
          <span class="na">provenance</span><span class="pi">:</span> <span class="kc">true</span>
          <span class="na">sbom</span><span class="pi">:</span> <span class="kc">true</span>
          <span class="na">build-args</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">BUILD_DATE=$</span>
            <span class="s">VERSION=$</span>
            <span class="s">REVISION=$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run Trivy vulnerability scanner</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">inputs.scan_image</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">aquasecurity/trivy-action@master</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">image-ref</span><span class="pi">:</span> <span class="s">$/$@$</span>
          <span class="na">format</span><span class="pi">:</span> <span class="s1">'</span><span class="s">sarif'</span>
          <span class="na">output</span><span class="pi">:</span> <span class="s1">'</span><span class="s">trivy-results.sarif'</span>
          <span class="na">severity</span><span class="pi">:</span> <span class="s1">'</span><span class="s">CRITICAL,HIGH'</span>
          <span class="na">ignore-unfixed</span><span class="pi">:</span> <span class="kc">true</span>
          <span class="na">scanners</span><span class="pi">:</span> <span class="s1">'</span><span class="s">vuln,secret,config'</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload Trivy results to GitHub Security</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">inputs.scan_image &amp;&amp; !cancelled()</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">github/codeql-action/upload-sarif@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">sarif_file</span><span class="pi">:</span> <span class="s1">'</span><span class="s">trivy-results.sarif'</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Generate build summary</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">always()</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "## Docker Build Summary" &gt;&gt; $GITHUB_STEP_SUMMARY</span>
          <span class="s">echo "" &gt;&gt; $GITHUB_STEP_SUMMARY</span>
          <span class="s">echo "**Image:** \`$/$\`" &gt;&gt; $GITHUB_STEP_SUMMARY</span>
          <span class="s">echo "**Tags:** $" &gt;&gt; $GITHUB_STEP_SUMMARY</span>
          <span class="s">echo "**Digest:** \`$\`" &gt;&gt; $GITHUB_STEP_SUMMARY</span>
          <span class="s">echo "**Platforms:** $" &gt;&gt; $GITHUB_STEP_SUMMARY</span>
          <span class="s">echo "" &gt;&gt; $GITHUB_STEP_SUMMARY</span>
          <span class="s">echo "**Build Context:** \`$\`" &gt;&gt; $GITHUB_STEP_SUMMARY</span>
          <span class="s">echo "**Dockerfile:** \`$\`" &gt;&gt; $GITHUB_STEP_SUMMARY</span>
</code></pre></div></div>

<p>This reusable workflow encapsulates all the best practices for building container images. The metadata action generates semantic tags automatically based on git references and semantic versioning. BuildKit’s cache management significantly speeds up subsequent builds by storing layer caches in the registry itself. The workflow generates both provenance attestations and SBOMs (Software Bill of Materials), which are increasingly important for supply chain security.</p>

<p>The Trivy scanner catches vulnerabilities before they reach production. By uploading results to GitHub Security, you centralise vulnerability management alongside your code. The build summary provides immediate feedback in the workflow run, making it easy to verify what was built.</p>

<h3 id="application-specific-workflow">Application-Specific Workflow</h3>

<p>Now create a workflow that uses this reusable workflow for a specific application, <code class="language-plaintext highlighter-rouge">.github/workflows/api-service.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">API Service - Build and Deploy</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>
      <span class="pi">-</span> <span class="s">develop</span>
    <span class="na">paths</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">services/api/**'</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">.github/workflows/api-service.yml'</span>
  <span class="na">pull_request</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>
      <span class="pi">-</span> <span class="s">develop</span>
    <span class="na">paths</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">services/api/**'</span>
  <span class="na">workflow_dispatch</span><span class="pi">:</span>
    <span class="na">inputs</span><span class="pi">:</span>
      <span class="na">environment</span><span class="pi">:</span>
        <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Deployment</span><span class="nv"> </span><span class="s">environment'</span>
        <span class="na">required</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">type</span><span class="pi">:</span> <span class="s">choice</span>
        <span class="na">options</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="s">development</span>
          <span class="pi">-</span> <span class="s">staging</span>
          <span class="pi">-</span> <span class="s">production</span>

<span class="na">permissions</span><span class="pi">:</span>
  <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
  <span class="na">security-events</span><span class="pi">:</span> <span class="s">write</span>
  <span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>
  <span class="na">pull-requests</span><span class="pi">:</span> <span class="s">write</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Build API Service Image</span>
    <span class="na">uses</span><span class="pi">:</span> <span class="s">./.github/workflows/docker-build-push.yml</span>
    <span class="na">with</span><span class="pi">:</span>
      <span class="na">image_name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">platform/api-service'</span>
      <span class="na">dockerfile_path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./services/api/Dockerfile'</span>
      <span class="na">context_path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./services/api'</span>
      <span class="na">platforms</span><span class="pi">:</span> <span class="s1">'</span><span class="s">linux/amd64'</span>
      <span class="na">push</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">scan_image</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">secrets</span><span class="pi">:</span>
      <span class="na">AZURE_CLIENT_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">AZURE_TENANT_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">AZURE_SUBSCRIPTION_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">ACR_LOGIN_SERVER</span><span class="pi">:</span> <span class="s">$</span>

  <span class="na">deploy-dev</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to Development</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">build</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">github.ref == 'refs/heads/develop' &amp;&amp; github.event_name == 'push'</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">environment</span><span class="pi">:</span> <span class="s">development</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Azure Login</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">azure/login@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">creds</span><span class="pi">:</span> <span class="s">$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to Container Apps</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">azure/container-apps-deploy-action@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">containerAppName</span><span class="pi">:</span> <span class="s">ca-api-service-dev</span>
          <span class="na">resourceGroup</span><span class="pi">:</span> <span class="s">rg-platform-dev</span>
          <span class="na">imageToDeploy</span><span class="pi">:</span> <span class="s">$</span>

  <span class="na">deploy-staging</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to Staging</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">build</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">github.ref == 'refs/heads/main' &amp;&amp; github.event_name == 'push'</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">environment</span><span class="pi">:</span> <span class="s">staging</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Azure Login</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">azure/login@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">creds</span><span class="pi">:</span> <span class="s">$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to AKS</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">azure/k8s-deploy@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">namespace</span><span class="pi">:</span> <span class="s">api-services</span>
          <span class="na">manifests</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">services/api/k8s/deployment.yaml</span>
            <span class="s">services/api/k8s/service.yaml</span>
          <span class="na">images</span><span class="pi">:</span> <span class="s">$</span>

  <span class="na">deploy-prod</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to Production</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">build</span><span class="pi">,</span> <span class="nv">deploy-staging</span><span class="pi">]</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">github.event.inputs.environment == 'production'</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">environment</span><span class="pi">:</span> <span class="s">production</span>

    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Azure Login</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">azure/login@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">creds</span><span class="pi">:</span> <span class="s">$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Deploy to AKS</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">azure/k8s-deploy@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">namespace</span><span class="pi">:</span> <span class="s">api-services</span>
          <span class="na">manifests</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">services/api/k8s/deployment.yaml</span>
            <span class="s">services/api/k8s/service.yaml</span>
          <span class="na">images</span><span class="pi">:</span> <span class="s">$</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Create deployment record</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">echo "Deployed $ to production" &gt;&gt; deployment-log.txt</span>
</code></pre></div></div>

<p>This workflow demonstrates a complete CI/CD pipeline. Pull requests trigger builds without pushing to verify the image builds successfully. Pushes to develop automatically deploy to the development environment. The main branch deploys to staging, whilst production deployments require manual approval through the workflow dispatch trigger.</p>

<h3 id="multi-service-monorepo-workflow">Multi-Service Monorepo Workflow</h3>

<p>For organisations managing multiple services in a monorepo, create <code class="language-plaintext highlighter-rouge">.github/workflows/monorepo-build.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Monorepo - Build All Services</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>
  <span class="na">pull_request</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>

<span class="na">permissions</span><span class="pi">:</span>
  <span class="na">contents</span><span class="pi">:</span> <span class="s">read</span>
  <span class="na">security-events</span><span class="pi">:</span> <span class="s">write</span>
  <span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">detect-changes</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Detect Service Changes</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">outputs</span><span class="pi">:</span>
      <span class="na">api</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">web</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">worker</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">dorny/paths-filter@v2</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">filter</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">filters</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">api:</span>
              <span class="s">- 'services/api/**'</span>
              <span class="s">- 'shared/**'</span>
            <span class="s">web:</span>
              <span class="s">- 'services/web/**'</span>
              <span class="s">- 'shared/**'</span>
            <span class="s">worker:</span>
              <span class="s">- 'services/worker/**'</span>
              <span class="s">- 'shared/**'</span>

  <span class="na">build-api</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Build API Service</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">detect-changes</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">needs.detect-changes.outputs.api == 'true'</span>
    <span class="na">uses</span><span class="pi">:</span> <span class="s">./.github/workflows/docker-build-push.yml</span>
    <span class="na">with</span><span class="pi">:</span>
      <span class="na">image_name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">platform/api-service'</span>
      <span class="na">dockerfile_path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./services/api/Dockerfile'</span>
      <span class="na">context_path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">.'</span>
      <span class="na">platforms</span><span class="pi">:</span> <span class="s1">'</span><span class="s">linux/amd64'</span>
      <span class="na">push</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">secrets</span><span class="pi">:</span>
      <span class="na">AZURE_CLIENT_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">AZURE_TENANT_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">AZURE_SUBSCRIPTION_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">ACR_LOGIN_SERVER</span><span class="pi">:</span> <span class="s">$</span>

  <span class="na">build-web</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Build Web Service</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">detect-changes</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">needs.detect-changes.outputs.web == 'true'</span>
    <span class="na">uses</span><span class="pi">:</span> <span class="s">./.github/workflows/docker-build-push.yml</span>
    <span class="na">with</span><span class="pi">:</span>
      <span class="na">image_name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">platform/web-service'</span>
      <span class="na">dockerfile_path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./services/web/Dockerfile'</span>
      <span class="na">context_path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">.'</span>
      <span class="na">platforms</span><span class="pi">:</span> <span class="s1">'</span><span class="s">linux/amd64'</span>
      <span class="na">push</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">secrets</span><span class="pi">:</span>
      <span class="na">AZURE_CLIENT_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">AZURE_TENANT_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">AZURE_SUBSCRIPTION_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">ACR_LOGIN_SERVER</span><span class="pi">:</span> <span class="s">$</span>

  <span class="na">build-worker</span><span class="pi">:</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s">Build Worker Service</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">detect-changes</span>
    <span class="na">if</span><span class="pi">:</span> <span class="s">needs.detect-changes.outputs.worker == 'true'</span>
    <span class="na">uses</span><span class="pi">:</span> <span class="s">./.github/workflows/docker-build-push.yml</span>
    <span class="na">with</span><span class="pi">:</span>
      <span class="na">image_name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">platform/worker-service'</span>
      <span class="na">dockerfile_path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./services/worker/Dockerfile'</span>
      <span class="na">context_path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">.'</span>
      <span class="na">platforms</span><span class="pi">:</span> <span class="s1">'</span><span class="s">linux/amd64'</span>
      <span class="na">push</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">secrets</span><span class="pi">:</span>
      <span class="na">AZURE_CLIENT_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">AZURE_TENANT_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">AZURE_SUBSCRIPTION_ID</span><span class="pi">:</span> <span class="s">$</span>
      <span class="na">ACR_LOGIN_SERVER</span><span class="pi">:</span> <span class="s">$</span>
</code></pre></div></div>

<p>This workflow uses path filtering to detect which services have changed, building only what’s necessary. This dramatically reduces CI/CD time for large monorepos where a change in one service doesn’t affect others.</p>

<h2 id="integrating-with-aks-clusters">Integrating with AKS Clusters</h2>

<p>Your AKS clusters need proper configuration to pull from the centralised registry. The key is using managed identities rather than image pull secrets, which eliminates credential management headaches.</p>

<h3 id="aks-cluster-configuration">AKS Cluster Configuration</h3>

<p>When creating your AKS cluster, attach the managed identity we created earlier:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"azurerm_kubernetes_cluster"</span> <span class="s2">"main"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"aks-${var.cluster_name}-${var.environment}"</span>
  <span class="nx">location</span>            <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">location</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">resource_group_name</span>
  <span class="nx">dns_prefix</span>          <span class="o">=</span> <span class="s2">"aks-${var.cluster_name}"</span>
  
  <span class="nx">default_node_pool</span> <span class="p">{</span>
    <span class="nx">name</span>       <span class="o">=</span> <span class="s2">"system"</span>
    <span class="nx">node_count</span> <span class="o">=</span> <span class="mi">3</span>
    <span class="nx">vm_size</span>    <span class="o">=</span> <span class="s2">"Standard_D4s_v5"</span>
    
    <span class="c1"># Critical for private registry access</span>
    <span class="nx">vnet_subnet_id</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">aks_subnet_id</span>
  <span class="p">}</span>

  <span class="c1"># Use the managed identity that has AcrPull permissions</span>
  <span class="nx">identity</span> <span class="p">{</span>
    <span class="nx">type</span> <span class="o">=</span> <span class="s2">"UserAssigned"</span>
    <span class="nx">identity_ids</span> <span class="o">=</span> <span class="p">[</span><span class="nx">var</span><span class="p">.</span><span class="nx">acr_pull_identity_id</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="c1"># Attach to the ACR</span>
  <span class="nx">kubelet_identity</span> <span class="p">{</span>
    <span class="nx">client_id</span>                 <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">acr_pull_identity_client_id</span>
    <span class="nx">object_id</span>                 <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">acr_pull_identity_object_id</span>
    <span class="nx">user_assigned_identity_id</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">acr_pull_identity_id</span>
  <span class="p">}</span>

  <span class="nx">network_profile</span> <span class="p">{</span>
    <span class="nx">network_plugin</span>    <span class="o">=</span> <span class="s2">"azure"</span>
    <span class="nx">network_policy</span>    <span class="o">=</span> <span class="s2">"calico"</span>
    <span class="nx">dns_service_ip</span>    <span class="o">=</span> <span class="s2">"10.2.0.10"</span>
    <span class="nx">service_cidr</span>      <span class="o">=</span> <span class="s2">"10.2.0.0/16"</span>
    <span class="nx">load_balancer_sku</span> <span class="o">=</span> <span class="s2">"standard"</span>
  <span class="p">}</span>

  <span class="c1"># Enable private cluster for enhanced security</span>
  <span class="nx">private_cluster_enabled</span> <span class="o">=</span> <span class="kc">true</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With this configuration, your pods can reference images directly without any image pull secrets:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">api-service</span>
  <span class="na">namespace</span><span class="pi">:</span> <span class="s">api-services</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">replicas</span><span class="pi">:</span> <span class="m">3</span>
  <span class="na">selector</span><span class="pi">:</span>
    <span class="na">matchLabels</span><span class="pi">:</span>
      <span class="na">app</span><span class="pi">:</span> <span class="s">api-service</span>
  <span class="na">template</span><span class="pi">:</span>
    <span class="na">metadata</span><span class="pi">:</span>
      <span class="na">labels</span><span class="pi">:</span>
        <span class="na">app</span><span class="pi">:</span> <span class="s">api-service</span>
    <span class="na">spec</span><span class="pi">:</span>
      <span class="na">containers</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">api</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">acrorganisationprod.azurecr.io/platform/api-service:v1.2.3</span>
        <span class="na">ports</span><span class="pi">:</span>
        <span class="pi">-</span> <span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
        <span class="na">resources</span><span class="pi">:</span>
          <span class="na">requests</span><span class="pi">:</span>
            <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">256Mi"</span>
            <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">250m"</span>
          <span class="na">limits</span><span class="pi">:</span>
            <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">512Mi"</span>
            <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">500m"</span>
</code></pre></div></div>

<p>The kubelet automatically authenticates to ACR using the managed identity, pulling images seamlessly over the private endpoint.</p>

<h2 id="integrating-with-azure-container-apps">Integrating with Azure Container Apps</h2>

<p>Container Apps integration is similarly straightforward when using managed identities:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"azurerm_container_app"</span> <span class="s2">"api"</span> <span class="p">{</span>
  <span class="nx">name</span>                         <span class="o">=</span> <span class="s2">"ca-api-service-${var.environment}"</span>
  <span class="nx">container_app_environment_id</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">container_app_environment_id</span>
  <span class="nx">resource_group_name</span>          <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">resource_group_name</span>
  <span class="nx">revision_mode</span>                <span class="o">=</span> <span class="s2">"Single"</span>

  <span class="nx">identity</span> <span class="p">{</span>
    <span class="nx">type</span> <span class="o">=</span> <span class="s2">"UserAssigned"</span>
    <span class="nx">identity_ids</span> <span class="o">=</span> <span class="p">[</span><span class="nx">var</span><span class="p">.</span><span class="nx">acr_pull_identity_id</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">registry</span> <span class="p">{</span>
    <span class="nx">server</span>   <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">acr_login_server</span>
    <span class="nx">identity</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">acr_pull_identity_id</span>
  <span class="p">}</span>

  <span class="nx">template</span> <span class="p">{</span>
    <span class="nx">container</span> <span class="p">{</span>
      <span class="nx">name</span>   <span class="o">=</span> <span class="s2">"api-service"</span>
      <span class="nx">image</span>  <span class="o">=</span> <span class="s2">"${var.acr_login_server}/platform/api-service:latest"</span>
      <span class="nx">cpu</span>    <span class="o">=</span> <span class="mf">0.5</span>
      <span class="nx">memory</span> <span class="o">=</span> <span class="s2">"1Gi"</span>

      <span class="nx">env</span> <span class="p">{</span>
        <span class="nx">name</span>  <span class="o">=</span> <span class="s2">"ASPNETCORE_ENVIRONMENT"</span>
        <span class="nx">value</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">environment</span>
      <span class="p">}</span>
    <span class="p">}</span>

    <span class="nx">min_replicas</span> <span class="o">=</span> <span class="mi">1</span>
    <span class="nx">max_replicas</span> <span class="o">=</span> <span class="mi">10</span>
  <span class="p">}</span>

  <span class="nx">ingress</span> <span class="p">{</span>
    <span class="nx">external_enabled</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="nx">target_port</span>      <span class="o">=</span> <span class="mi">8080</span>
    <span class="nx">traffic_weight</span> <span class="p">{</span>
      <span class="nx">latest_revision</span> <span class="o">=</span> <span class="kc">true</span>
      <span class="nx">percentage</span>      <span class="o">=</span> <span class="mi">100</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The Container App uses the managed identity to authenticate with ACR automatically. Because the Container App environment is connected to your VNet, it pulls images through the private endpoint.</p>

<h2 id="security-best-practices-in-practice">Security Best Practices in Practice</h2>

<p>Throughout this implementation, we’ve embedded security best practices at every layer. Let me highlight the critical security decisions and why they matter.</p>

<h3 id="defence-in-depth">Defence in Depth</h3>

<p>The architecture implements multiple security layers. Even if an attacker breaches one layer, others remain intact. The registry sits behind network restrictions and private endpoints. Access requires Azure AD authentication with role-based access control. Encryption protects data at rest using customer-managed keys. Each layer independently contributes to security.</p>

<h3 id="least-privilege-access">Least Privilege Access</h3>

<p>No identity receives more permissions than absolutely necessary. GitHub Actions can push but not delete. AKS clusters can pull but not push. Teams can access only their namespaces. The principle of least privilege minimises the blast radius if credentials are compromised.</p>

<h3 id="immutable-infrastructure">Immutable Infrastructure</h3>

<p>Whilst the Terraform code doesn’t explicitly show this, consider implementing repository locks and retention policies that prevent tag overwrites. Once you tag an image as <code class="language-plaintext highlighter-rouge">v1.2.3</code>, that tag should never change. This immutability ensures reproducible deployments and prevents malicious tag replacement.</p>

<h3 id="audit-logging">Audit Logging</h3>

<p>Every operation against the registry is logged to Log Analytics. Authentication attempts, image pulls, image pushes, configuration changes—all are recorded with timestamps and identity information. These logs are crucial for security investigations and compliance requirements.</p>

<h3 id="vulnerability-scanning">Vulnerability Scanning</h3>

<p>The GitHub Actions workflow scans every image before it enters the registry. Additionally, consider enabling Azure Defender for Container Registries, which continuously scans images even after they’re pushed and alerts you to newly discovered vulnerabilities.</p>

<h3 id="network-isolation">Network Isolation</h3>

<p>The private endpoints ensure registry traffic never touches the public internet when accessed from your Azure workloads. This prevents eavesdropping and man-in-the-middle attacks. The hybrid model for GitHub Actions is a pragmatic compromise, but self-hosted runners would eliminate it entirely.</p>

<h2 id="operational-considerations">Operational Considerations</h2>

<p>Building the infrastructure is one thing; operating it successfully requires ongoing attention to several areas.</p>

<h3 id="image-lifecycle-management">Image Lifecycle Management</h3>

<p>Container registries grow quickly as CI/CD pipelines push images continuously. Without lifecycle management, you’ll accumulate thousands of untagged manifests and old images that nobody uses. The retention policy we configured helps, but consider implementing more sophisticated cleanup:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Image retention task to clean up old images</span>
<span class="nx">resource</span> <span class="s2">"azurerm_container_registry_task"</span> <span class="s2">"cleanup"</span> <span class="p">{</span>
  <span class="nx">name</span>                  <span class="o">=</span> <span class="s2">"cleanup-old-images"</span>
  <span class="nx">container_registry_id</span> <span class="o">=</span> <span class="nx">azurerm_container_registry</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>
  
  <span class="nx">platform</span> <span class="p">{</span>
    <span class="nx">os</span> <span class="o">=</span> <span class="s2">"Linux"</span>
  <span class="p">}</span>

  <span class="nx">encoded_step</span> <span class="p">{</span>
    <span class="nx">task_content</span> <span class="o">=</span> <span class="nx">base64encode</span><span class="p">(</span><span class="o">&lt;&lt;-</span><span class="nx">EOT</span>
      <span class="nx">version</span><span class="o">:</span> <span class="nx">v1</span><span class="p">.</span><span class="mf">1.0</span>
      <span class="nx">steps</span><span class="o">:</span>
        <span class="o">-</span> <span class="nx">cmd</span><span class="o">:</span> <span class="nx">acr</span> <span class="nx">purge</span> <span class="o">--</span><span class="nx">filter</span> <span class="s1">'platform/.*:.*'</span> <span class="o">--</span><span class="nx">ago</span> <span class="mi">90</span><span class="nx">d</span> <span class="o">--</span><span class="nx">untagged</span>
          <span class="nx">disableWorkingDirectoryOverride</span><span class="o">:</span> <span class="kc">true</span>
          <span class="nx">timeout</span><span class="o">:</span> <span class="mi">3600</span>
    <span class="nx">EOT</span>
    <span class="err">)</span>
  <span class="p">}</span>

  <span class="nx">timer_trigger</span> <span class="p">{</span>
    <span class="nx">name</span>     <span class="o">=</span> <span class="s2">"weekly"</span>
    <span class="nx">schedule</span> <span class="o">=</span> <span class="s2">"0 0 * * 0"</span>
    <span class="nx">enabled</span>  <span class="o">=</span> <span class="kc">true</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This ACR Task runs weekly, removing untagged manifests older than 90 days and images in the platform namespace that haven’t been pulled in 90 days. Adjust the filters and retention periods based on your organisation’s needs.</p>

<h3 id="cost-management">Cost Management</h3>

<p>Container registries can become expensive if not monitored. The primary costs come from storage and geo-replication data egress. Monitor your storage usage trends and investigate unexpected growth. Consider whether you need geo-replication to all regions or if strategic placement in key regions suffices.</p>

<p>Enable Azure Cost Management alerts to notify you when registry costs exceed expected thresholds. Tag images with team or project information so you can attribute costs accurately.</p>

<h3 id="disaster-recovery">Disaster Recovery</h3>

<p>Your container registry becomes a critical dependency. If it’s unavailable, deployments fail and existing pods that need to pull images can’t start. Geo-replication provides regional redundancy, but also implement backup strategies:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"azurerm_backup_policy_file_share"</span> <span class="s2">"acr"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="o">=</span> <span class="s2">"acr-backup-policy"</span>
  <span class="nx">resource_group_name</span> <span class="o">=</span> <span class="nx">azurerm_resource_group</span><span class="p">.</span><span class="nx">acr</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">recovery_vault_name</span> <span class="o">=</span> <span class="nx">var</span><span class="p">.</span><span class="nx">recovery_vault_name</span>

  <span class="nx">backup</span> <span class="p">{</span>
    <span class="nx">frequency</span> <span class="o">=</span> <span class="s2">"Daily"</span>
    <span class="nx">time</span>      <span class="o">=</span> <span class="s2">"23:00"</span>
  <span class="p">}</span>

  <span class="nx">retention_daily</span> <span class="p">{</span>
    <span class="nx">count</span> <span class="o">=</span> <span class="mi">30</span>
  <span class="p">}</span>

  <span class="nx">retention_weekly</span> <span class="p">{</span>
    <span class="nx">count</span>    <span class="o">=</span> <span class="mi">12</span>
    <span class="nx">weekdays</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"Sunday"</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">retention_monthly</span> <span class="p">{</span>
    <span class="nx">count</span>    <span class="o">=</span> <span class="mi">12</span>
    <span class="nx">weekdays</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"Sunday"</span><span class="p">]</span>
    <span class="nx">weeks</span>    <span class="o">=</span> <span class="p">[</span><span class="s2">"First"</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Test your disaster recovery procedures regularly. Can you restore the registry if it’s accidentally deleted? How quickly can you recover? Document the procedures so any team member can execute them under pressure.</p>

<h3 id="performance-optimisation">Performance Optimisation</h3>

<p>Image pull performance directly affects deployment speed and pod startup time. Several factors influence performance:</p>

<p>Geo-replication places registry replicas close to your workloads, reducing latency. The regional endpoint feature we enabled routes requests to the nearest replica automatically.</p>

<p>Layer caching in your Dockerfiles minimises rebuild time. Structure your Dockerfiles so frequently changing layers appear near the end, allowing Docker to reuse cached layers for earlier steps.</p>

<p>Multi-stage builds reduce final image size by excluding build tools and intermediate artefacts from the runtime image. Smaller images pull faster and consume less storage.</p>

<p>Consider implementing a registry cache or pull-through cache for external base images. This prevents pulling the same base image from Docker Hub repeatedly, reducing external bandwidth usage and improving reliability.</p>

<h2 id="team-adoption-and-documentation">Team Adoption and Documentation</h2>

<p>A centralised registry serves multiple teams, and successful adoption requires clear documentation and communication.</p>

<h3 id="developer-documentation">Developer Documentation</h3>

<p>Create comprehensive documentation that answers common questions developers will have. Document the registry URL, authentication methods, naming conventions, and how to troubleshoot common issues. Include examples for different use cases.</p>

<p>The key constraint to communicate clearly is that our registry is configured for maximum security with admin credentials disabled and network access restricted to private endpoints. This means local development workflows differ from traditional registry patterns.</p>

<p><strong>For local development and testing:</strong></p>

<p>With our security-first configuration (private endpoints, network deny-by-default), developers working from their local machines <strong>cannot directly access the registry</strong>. The network rules only permit GitHub Actions IP ranges and private endpoint access from within Azure.</p>

<p>This is intentional and represents a security best practice, but it requires developers to adapt their workflows:</p>

<p><em>Option 1: Build and test locally without registry access (recommended)</em></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Build locally with a temporary tag</span>
docker build <span class="nt">-t</span> myapp:local-test <span class="nb">.</span>

<span class="c"># Test locally</span>
docker run <span class="nt">-p</span> 8080:8080 myapp:local-test

<span class="c"># When ready, commit and push code</span>
git add <span class="nb">.</span>
git commit <span class="nt">-m</span> <span class="s2">"feat: add new feature"</span>
git push origin feature/my-branch

<span class="c"># Let GitHub Actions build and push to ACR</span>
</code></pre></div></div>

<p>This is the standard workflow for secure production registries. Developers never interact with the registry directly—they build and test locally, then CI/CD handles all registry operations.</p>

<p><em>Option 2: VPN access for pulling images</em></p>

<p>For developers who need to pull production images for debugging or testing, provide VPN access to your Azure network:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Connect to corporate VPN that routes to Azure VNet</span>
<span class="c"># 2. Authenticate using Azure AD</span>
az login
az acr login <span class="nt">--name</span> acrorganisationprod

<span class="c"># 3. Pull images (pulling works, pushing still requires appropriate RBAC)</span>
docker pull acrorganisationprod.azurecr.io/platform/api-service:latest

<span class="c"># 4. Run locally for debugging</span>
docker run <span class="nt">-p</span> 8080:8080 acrorganisationprod.azurecr.io/platform/api-service:latest
</code></pre></div></div>

<p>Once connected via VPN, developers can access the registry through the private endpoint. However, even with VPN access, pushing requires both network access <strong>and</strong> appropriate RBAC permissions (AcrPush role), which should remain restricted to CI/CD service principals.</p>

<p><em>Option 3: Temporary IP allowlist (not recommended)</em></p>

<p>For exceptional circumstances, platform teams could temporarily add a developer’s IP to the network rules:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Temporary addition to variables.tf</span>
<span class="nx">variable</span> <span class="s2">"developer_ip_allowlist"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="o">=</span> <span class="s2">"Temporary developer IPs (remove after use)"</span>
  <span class="nx">type</span>        <span class="o">=</span> <span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span>     <span class="o">=</span> <span class="p">[]</span>
<span class="p">}</span>

<span class="c1"># In networking.tf, add to ip_rule</span>
<span class="nx">ip_rule</span> <span class="o">=</span> <span class="nx">concat</span><span class="err">(</span>
  <span class="p">[</span><span class="nx">for</span> <span class="nx">cidr</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">github_actions_ip_ranges</span> <span class="o">:</span> <span class="p">{</span>
    <span class="nx">action</span>   <span class="o">=</span> <span class="s2">"Allow"</span>
    <span class="nx">ip_range</span> <span class="o">=</span> <span class="nx">cidr</span>
  <span class="p">}]</span><span class="err">,</span>
  <span class="p">[</span><span class="nx">for</span> <span class="nx">ip</span> <span class="nx">in</span> <span class="nx">var</span><span class="p">.</span><span class="nx">developer_ip_allowlist</span> <span class="o">:</span> <span class="p">{</span>
    <span class="nx">action</span>   <span class="o">=</span> <span class="s2">"Allow"</span>
    <span class="nx">ip_range</span> <span class="o">=</span> <span class="nx">ip</span>
  <span class="p">}]</span>
<span class="err">)</span>
</code></pre></div></div>

<p>This defeats the purpose of network security controls and should only be used as a last resort for troubleshooting.</p>

<p>The key message for developers: <strong>build and test locally, let CI/CD handle registry pushes</strong>. This pattern aligns with security best practices whilst maintaining developer productivity.</p>

<p><strong>For CI/CD pipelines:</strong></p>

<p>GitHub Actions workflows authenticate using <strong>workload identity federation with OIDC</strong>, which is far more secure than traditional client secrets. This approach uses short-lived tokens issued by GitHub that are trusted by Azure AD through federated credentials.</p>

<p>The authentication happens automatically through the <code class="language-plaintext highlighter-rouge">azure/login</code> action:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Azure Login via OIDC</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">azure/login@v1</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">client-id</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">tenant-id</span><span class="pi">:</span> <span class="s">$</span>
    <span class="na">subscription-id</span><span class="pi">:</span> <span class="s">$</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Log in to Azure Container Registry</span>
  <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">az acr login --name acrorganisationprod</span>
</code></pre></div></div>

<p><strong>Setting up GitHub repository secrets:</strong></p>

<p>After deploying the Terraform configuration, add these secrets to your GitHub repository (Settings → Secrets and variables → Actions):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># These values come from Terraform outputs</span>
AZURE_CLIENT_ID: &lt;output from terraform&gt;
AZURE_TENANT_ID: &lt;output from terraform&gt;
AZURE_SUBSCRIPTION_ID: &lt;output from terraform&gt;
ACR_LOGIN_SERVER: acrorganisationprod.azurecr.io
</code></pre></div></div>

<p>The Terraform configuration creates federated identity credentials for your main branch, develop branch, and pull requests. If you need additional branches or environments, add more <code class="language-plaintext highlighter-rouge">azuread_application_federated_identity_credential</code> resources.</p>

<h3 id="naming-conventions">Naming Conventions</h3>

<p>Establish and document clear naming conventions. A suggested structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{registry}.azurecr.io/{team|org-unit}/{application}/{component}:{tag}
</code></pre></div></div>

<p>Examples:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">acrorganisationprod.azurecr.io/platform/api-service:v1.2.3</code></li>
  <li><code class="language-plaintext highlighter-rouge">acrorganisationprod.azurecr.io/data-team/ingestion-worker:2024-01-15-abc123</code></li>
  <li><code class="language-plaintext highlighter-rouge">acrorganisationprod.azurecr.io/mobile/ios-backend:release-candidate</code></li>
</ul>

<p>Consistent naming makes it easy to understand image ownership, apply automation, and manage access policies.</p>

<h3 id="onboarding-process">Onboarding Process</h3>

<p>Create a streamlined onboarding process for new teams or projects. This might involve:</p>

<ol>
  <li>Requesting a namespace in the registry (e.g., <code class="language-plaintext highlighter-rouge">teamname/</code>)</li>
  <li>Creating a managed identity with pull access</li>
  <li>Granting the team push access to their namespace</li>
  <li>Providing GitHub Actions secrets or service principal credentials</li>
  <li>Adding the team to monitoring and alerting for their images</li>
</ol>

<p>Automate this process where possible. A simple internal portal or infrastructure-as-code template that provisions these resources reduces friction and ensures consistency.</p>

<h2 id="troubleshooting-common-issues">Troubleshooting Common Issues</h2>

<p>Even with careful implementation, teams will encounter issues. Here are solutions to common problems.</p>

<h3 id="authentication-failures">Authentication Failures</h3>

<p><strong>Symptom:</strong> <code class="language-plaintext highlighter-rouge">unauthorized: authentication required</code> or <code class="language-plaintext highlighter-rouge">unauthorized: access denied</code></p>

<p><strong>Causes:</strong></p>
<ul>
  <li>Expired service principal credentials</li>
  <li>Incorrect RBAC assignments</li>
  <li>Network rules blocking the request</li>
  <li>Managed identity not properly attached to AKS or Container Apps</li>
</ul>

<p><strong>Resolution:</strong>
Check the diagnostic logs in Log Analytics to see the specific authentication failure. Verify that the identity being used has the appropriate role assignment. For AKS, ensure the kubelet identity is correctly configured. For Container Apps, verify the registry identity matches the pull identity.</p>

<h3 id="image-pull-failures-from-aks">Image Pull Failures from AKS</h3>

<p><strong>Symptom:</strong> Pods stuck in <code class="language-plaintext highlighter-rouge">ImagePullBackOff</code> state</p>

<p><strong>Causes:</strong></p>
<ul>
  <li>Private endpoint DNS resolution failing</li>
  <li>Network connectivity issues</li>
  <li>Image doesn’t exist or tag is wrong</li>
  <li>Kubelet identity lacks permissions</li>
</ul>

<p><strong>Resolution:</strong>
First, verify DNS resolution from a pod:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl run <span class="nt">-it</span> <span class="nt">--rm</span> debug <span class="nt">--image</span><span class="o">=</span>busybox <span class="nt">--restart</span><span class="o">=</span>Never <span class="nt">--</span> nslookup acrorganisationprod.azurecr.io
</code></pre></div></div>

<p>The hostname should resolve to a private IP address from the 10.x.x.x range, not a public IP. If it resolves to a public IP, the private DNS zone linking is misconfigured.</p>

<p>Check pod events for specific error messages:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe pod &lt;pod-name&gt;
</code></pre></div></div>

<p>Test pulling the image manually from a node:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># SSH to a node (or use kubectl debug)</span>
<span class="nb">sudo </span>crictl pull acrorganisationprod.azurecr.io/platform/api-service:latest
</code></pre></div></div>

<h3 id="slow-image-pulls">Slow Image Pulls</h3>

<p><strong>Symptom:</strong> Deployments take minutes to pull images</p>

<p><strong>Causes:</strong></p>
<ul>
  <li>Large image sizes</li>
  <li>Pulling across regions</li>
  <li>Network bandwidth constraints</li>
  <li>Missing layer cache</li>
</ul>

<p><strong>Resolution:</strong>
Investigate image size and optimise:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker images <span class="nt">--format</span> <span class="s2">"table </span><span class="se">\t\t</span><span class="s2">"</span>
</code></pre></div></div>

<p>Images over 1GB warrant investigation. Use multi-stage builds, alpine base images, and .dockerignore files to reduce size.</p>

<p>Verify geo-replication is working:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az acr replication list <span class="nt">--registry</span> acrorganisationprod <span class="nt">--output</span> table
</code></pre></div></div>

<p>Ensure replicas exist in the same regions as your workloads.</p>

<h3 id="github-actions-push-failures">GitHub Actions Push Failures</h3>

<p><strong>Symptom:</strong> <code class="language-plaintext highlighter-rouge">denied: client with IP not allowed</code> or similar network errors</p>

<p><strong>Causes:</strong></p>
<ul>
  <li>GitHub Actions IP ranges changed</li>
  <li>Network rule set too restrictive</li>
  <li>Service principal credentials expired</li>
</ul>

<p><strong>Resolution:</strong>
GitHub occasionally adds new IP ranges. Check the current ranges:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl https://api.github.com/meta | jq .actions
</code></pre></div></div>

<p>Update your network rules if necessary. Consider using self-hosted runners in your Azure network to avoid this issue entirely.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Building a centralised Azure Container Registry is an investment in your platform’s future. Done correctly, it becomes invisible infrastructure that just works. Developers push images, deployments pull them, and the registry quietly manages terabytes of container layers across regions.</p>

<p>The architecture we’ve built embeds security at every layer, from customer-managed encryption keys to network isolation to comprehensive audit logging. The Terraform code is production-ready, implementing Azure best practices that satisfy security teams whilst remaining operationally practical.</p>

<p>The GitHub Actions workflows demonstrate modern CI/CD patterns: semantic versioning, multi-architecture builds, vulnerability scanning, provenance attestations, and efficient layer caching. These aren’t just nice-to-haves, they’re essential practices for maintaining secure, reliable container deployments.</p>

<p>Perhaps most importantly, this centralised approach scales with your organisation. Whether you’re supporting ten services or a thousand, the architectural patterns remain consistent. Teams gain autonomy through namespaced repositories whilst platform engineering maintains governance through centralized policies.</p>

<p>Container registries are foundational infrastructure. Build them well, secure them thoroughly, and they’ll serve your organisation reliably for years. The initial investment in proper architecture, security controls, and operational procedures pays dividends every single day as your teams deploy with confidence, knowing their images are stored securely and delivered efficiently wherever they’re needed.</p>]]></content><author><name>Glen Thomas</name></author><category term="Platform Engineering" /><category term="DevOps" /><category term="Azure" /><category term="Container Registry" /><category term="AKS" /><category term="Container Apps" /><category term="Terraform" /><category term="Security" /><summary type="html"><![CDATA[When building container-based applications on Azure, you’ll inevitably need to decide how to manage your container images. Azure Container Registry (ACR) is the natural choice for Azure workloads, but the real question is whether to create multiple registries across teams and environments, or build a single, centralised registry that serves your entire organisation.]]></summary></entry><entry><title type="html">Mastering pnpm Workspaces: A Complete Guide to Monorepo Management</title><link href="https://blog.glen-thomas.com/software%20engineering/2025/10/02/mastering-pnpm-workspaces-complete-guide-to-monorepo-management.html" rel="alternate" type="text/html" title="Mastering pnpm Workspaces: A Complete Guide to Monorepo Management" /><published>2025-10-02T11:00:00+01:00</published><updated>2025-10-02T11:00:00+01:00</updated><id>https://blog.glen-thomas.com/software%20engineering/2025/10/02/mastering-pnpm-workspaces-complete-guide-to-monorepo-management</id><content type="html" xml:base="https://blog.glen-thomas.com/software%20engineering/2025/10/02/mastering-pnpm-workspaces-complete-guide-to-monorepo-management.html"><![CDATA[<p>Managing multiple related packages and applications can quickly become a nightmare with traditional package managers. Enter <strong>pnpm workspaces</strong> – a powerful feature that transforms how we handle monorepos, offering superior performance, disk efficiency, and dependency management compared to npm or Yarn workspaces.</p>

<p>In this comprehensive tutorial, we’ll explore what pnpm workspaces are, why they’re game-changing for modern JavaScript development, and build a complete example monorepo from scratch.</p>

<h2 id="what-are-pnpm-workspaces">What are pnpm Workspaces?</h2>

<p>pnpm workspaces are a monorepo solution that allows you to manage multiple packages within a single repository while sharing dependencies efficiently. Unlike npm or Yarn, pnpm uses a unique <strong>content-addressable store</strong> that creates hard links to shared dependencies, dramatically reducing disk usage and installation time.</p>

<h3 id="key-benefits-of-pnpm-workspaces">Key Benefits of pnpm Workspaces</h3>

<ol>
  <li><strong>Disk Efficiency</strong>: Shared dependencies are stored once and linked across projects</li>
  <li><strong>Fast Installations</strong>: Hard linking eliminates duplicate downloads and extractions</li>
  <li><strong>Strict Dependency Management</strong>: Prevents phantom dependencies and version conflicts</li>
  <li><strong>Seamless Hoisting</strong>: Intelligent dependency hoisting without the pitfalls</li>
  <li><strong>Built-in Monorepo Support</strong>: No additional tools needed for workspace management</li>
</ol>

<h2 id="why-choose-pnpm-over-npmyarn-workspaces">Why Choose pnpm Over npm/Yarn Workspaces?</h2>

<table>
  <thead>
    <tr>
      <th>Feature</th>
      <th>pnpm</th>
      <th>npm</th>
      <th>Yarn</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Disk Usage</td>
      <td><strong>Minimal</strong> (hard links)</td>
      <td>High (duplicates)</td>
      <td>High (duplicates)</td>
    </tr>
    <tr>
      <td>Installation Speed</td>
      <td><strong>Fastest</strong></td>
      <td>Slow</td>
      <td>Moderate</td>
    </tr>
    <tr>
      <td>Phantom Dependencies</td>
      <td><strong>Prevented</strong></td>
      <td>Possible</td>
      <td>Possible</td>
    </tr>
    <tr>
      <td>Node Modules Structure</td>
      <td><strong>Strict</strong></td>
      <td>Flat (confusing)</td>
      <td>Flat (confusing)</td>
    </tr>
    <tr>
      <td>Workspace Features</td>
      <td><strong>Native</strong></td>
      <td>Basic</td>
      <td>Good</td>
    </tr>
  </tbody>
</table>

<h2 id="setting-up-our-example-monorepo">Setting Up Our Example Monorepo</h2>

<p>Let’s build a realistic monorepo with the following structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>my-workspace/
├── apps/
│   ├── web-app/          # React frontend
│   ├── api-server/       # Node.js backend
│   └── mobile-app/       # React Native app
├── packages/
│   ├── ui-components/    # Shared React components
│   ├── utils/           # Shared utilities
│   └── api-client/      # API client library
├── tools/
│   └── eslint-config/   # Shared ESLint configuration
├── package.json
└── pnpm-workspace.yaml
</code></pre></div></div>

<p>See my GitHub repo for full source code: <a href="https://github.com/glenthomas/Mastering-pnpm-Workspaces-Monorepo-Management">Mastering-pnpm-Workspaces-Monorepo-Management</a>.</p>

<h3 id="step-1-initialise-the-workspace">Step 1: Initialise the Workspace</h3>

<p>First, ensure you have pnpm installed globally:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install</span> <span class="nt">-g</span> pnpm
<span class="c"># or</span>
curl <span class="nt">-fsSL</span> https://get.pnpm.io/install.sh | sh
</code></pre></div></div>

<p>Create the root directory and initialise:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>my-workspace
<span class="nb">cd </span>my-workspace
pnpm init
</code></pre></div></div>

<h3 id="step-2-configure-workspace-structure">Step 2: Configure Workspace Structure</h3>

<p>Create the workspace configuration file:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># pnpm-workspace.yaml</span>
<span class="na">packages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s1">'</span><span class="s">apps/*'</span>
  <span class="pi">-</span> <span class="s1">'</span><span class="s">packages/*'</span>
  <span class="pi">-</span> <span class="s1">'</span><span class="s">tools/*'</span>
</code></pre></div></div>

<p>Update the root <code class="language-plaintext highlighter-rouge">package.json</code>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"my-workspace"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"private"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pnpm -r build"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pnpm -r test"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pnpm -r --parallel dev"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"lint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pnpm -r lint"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"clean"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pnpm -r clean"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"type-check"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pnpm -r type-check"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@types/node"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^20.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"typescript"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^5.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tsup"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^7.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vitest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^0.34.0"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="step-3-create-shared-packages">Step 3: Create Shared Packages</h3>

<h4 id="shared-utilities-package">Shared Utilities Package</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> packages/utils
<span class="nb">cd </span>packages/utils
pnpm init
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@workspace/utils"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"main"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.js"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"module"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.mjs"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"types"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.d.ts"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"exports"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"."</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"import"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.mjs"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"require"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.js"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"types"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.d.ts"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsup src/index.ts --format cjs,esm --dts"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsup src/index.ts --format cjs,esm --dts --watch"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vitest"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"clean"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rm -rf dist"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"typescript"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tsup"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vitest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Create the utility functions:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// packages/utils/src/index.ts</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nf">formatCurrency</span><span class="p">(</span><span class="nx">amount</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span> <span class="nx">currency</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">USD</span><span class="dl">'</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nx">Intl</span><span class="p">.</span><span class="nc">NumberFormat</span><span class="p">(</span><span class="dl">'</span><span class="s1">en-US</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">style</span><span class="p">:</span> <span class="dl">'</span><span class="s1">currency</span><span class="dl">'</span><span class="p">,</span>
    <span class="nx">currency</span><span class="p">,</span>
  <span class="p">}).</span><span class="nf">format</span><span class="p">(</span><span class="nx">amount</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nf">debounce</span><span class="o">&lt;</span><span class="nx">T</span> <span class="nf">extends </span><span class="p">(...</span><span class="nx">args</span><span class="p">:</span> <span class="kr">any</span><span class="p">[])</span> <span class="o">=&gt;</span> <span class="kr">any</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="nx">func</span><span class="p">:</span> <span class="nx">T</span><span class="p">,</span>
  <span class="nx">wait</span><span class="p">:</span> <span class="kr">number</span>
<span class="p">):</span> <span class="p">(...</span><span class="nx">args</span><span class="p">:</span> <span class="nb">Parameters</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="na">timeout</span><span class="p">:</span> <span class="nx">NodeJS</span><span class="p">.</span><span class="nx">Timeout</span><span class="p">;</span>
  <span class="k">return </span><span class="p">(...</span><span class="na">args</span><span class="p">:</span> <span class="nb">Parameters</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nf">clearTimeout</span><span class="p">(</span><span class="nx">timeout</span><span class="p">);</span>
    <span class="nx">timeout</span> <span class="o">=</span> <span class="nf">setTimeout</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">func</span><span class="p">.</span><span class="nf">apply</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="nx">args</span><span class="p">),</span> <span class="nx">wait</span><span class="p">);</span>
  <span class="p">};</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nf">slugify</span><span class="p">(</span><span class="nx">text</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">text</span>
    <span class="p">.</span><span class="nf">toLowerCase</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">[^\w\s</span><span class="sr">-</span><span class="se">]</span><span class="sr">/g</span><span class="p">,</span> <span class="dl">''</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">[\s</span><span class="sr">_-</span><span class="se">]</span><span class="sr">+/g</span><span class="p">,</span> <span class="dl">'</span><span class="s1">-</span><span class="dl">'</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="sr">/^-+|-+$/g</span><span class="p">,</span> <span class="dl">''</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">ApiResponse</span><span class="o">&lt;</span><span class="nx">T</span> <span class="o">=</span> <span class="kr">any</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="na">data</span><span class="p">:</span> <span class="nx">T</span><span class="p">;</span>
  <span class="nl">success</span><span class="p">:</span> <span class="nx">boolean</span><span class="p">;</span>
  <span class="nl">message</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nc">Logger</span> <span class="p">{</span>
  <span class="k">private</span> <span class="nx">context</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>

  <span class="nf">constructor</span><span class="p">(</span><span class="nx">context</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">context</span> <span class="o">=</span> <span class="nx">context</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="nf">info</span><span class="p">(</span><span class="nx">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="p">...</span><span class="nx">args</span><span class="p">:</span> <span class="kr">any</span><span class="p">[]):</span> <span class="k">void</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="s2">`[</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">context</span><span class="p">}</span><span class="s2">] </span><span class="p">${</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="p">...</span><span class="nx">args</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nf">error</span><span class="p">(</span><span class="nx">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">error</span><span class="p">?:</span> <span class="nb">Error</span><span class="p">):</span> <span class="k">void</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="s2">`[</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">context</span><span class="p">}</span><span class="s2">] </span><span class="p">${</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nf">warn</span><span class="p">(</span><span class="nx">message</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="p">...</span><span class="nx">args</span><span class="p">:</span> <span class="kr">any</span><span class="p">[]):</span> <span class="k">void</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">warn</span><span class="p">(</span><span class="s2">`[</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">context</span><span class="p">}</span><span class="s2">] </span><span class="p">${</span><span class="nx">message</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="p">...</span><span class="nx">args</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Add TypeScript configuration:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">packages/utils/tsconfig.json</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"extends"</span><span class="p">:</span><span class="w"> </span><span class="s2">"../../tsconfig.json"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"compilerOptions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"outDir"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"rootDir"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./src"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"include"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"src/**/*"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"exclude"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"dist"</span><span class="p">,</span><span class="w"> </span><span class="s2">"node_modules"</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="ui-components-package">UI Components Package</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> packages/ui-components
<span class="nb">cd </span>packages/ui-components
pnpm init
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@workspace/ui-components"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"main"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.js"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"module"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.mjs"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"types"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.d.ts"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"exports"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"."</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"import"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.mjs"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"require"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.js"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"types"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.d.ts"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"./styles"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/styles.css"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsup src/index.ts --format cjs,esm --dts --external react"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsup src/index.ts --format cjs,esm --dts --external react --watch"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vitest"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"clean"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rm -rf dist"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"peerDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"react"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"react-dom"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.0.0"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@workspace/utils"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@types/react"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@types/react-dom"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"typescript"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tsup"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vitest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Create reusable components:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// packages/ui-components/src/Button.tsx</span>
<span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">ButtonProps</span> <span class="p">{</span>
  <span class="nl">variant</span><span class="p">?:</span> <span class="dl">'</span><span class="s1">primary</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">secondary</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">danger</span><span class="dl">'</span><span class="p">;</span>
  <span class="nl">size</span><span class="p">?:</span> <span class="dl">'</span><span class="s1">sm</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">md</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">lg</span><span class="dl">'</span><span class="p">;</span>
  <span class="nl">disabled</span><span class="p">?:</span> <span class="nx">boolean</span><span class="p">;</span>
  <span class="nl">loading</span><span class="p">?:</span> <span class="nx">boolean</span><span class="p">;</span>
  <span class="nl">children</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">ReactNode</span><span class="p">;</span>
  <span class="nl">onClick</span><span class="p">?:</span> <span class="p">(</span><span class="nx">event</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">MouseEvent</span><span class="o">&lt;</span><span class="nx">HTMLButtonElement</span><span class="o">&gt;</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="k">void</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nf">Button</span><span class="p">({</span>
  <span class="nx">variant</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">primary</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">size</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">md</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">disabled</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
  <span class="nx">loading</span> <span class="o">=</span> <span class="kc">false</span><span class="p">,</span>
  <span class="nx">children</span><span class="p">,</span>
  <span class="nx">onClick</span><span class="p">,</span>
<span class="p">}:</span> <span class="nx">ButtonProps</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">baseClasses</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">inline-flex items-center justify-center font-medium rounded-md transition-colors</span><span class="dl">'</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">variantClasses</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">primary</span><span class="p">:</span> <span class="dl">'</span><span class="s1">bg-blue-600 text-white hover:bg-blue-700 disabled:bg-blue-300</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">secondary</span><span class="p">:</span> <span class="dl">'</span><span class="s1">bg-gray-200 text-gray-900 hover:bg-gray-300 disabled:bg-gray-100</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">danger</span><span class="p">:</span> <span class="dl">'</span><span class="s1">bg-red-600 text-white hover:bg-red-700 disabled:bg-red-300</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">};</span>
  <span class="kd">const</span> <span class="nx">sizeClasses</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">sm</span><span class="p">:</span> <span class="dl">'</span><span class="s1">px-3 py-1.5 text-sm</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">md</span><span class="p">:</span> <span class="dl">'</span><span class="s1">px-4 py-2 text-base</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">lg</span><span class="p">:</span> <span class="dl">'</span><span class="s1">px-6 py-3 text-lg</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">};</span>

  <span class="k">return </span><span class="p">(</span>
    <span class="o">&lt;</span><span class="nx">button</span>
      <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`</span><span class="p">${</span><span class="nx">baseClasses</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">variantClasses</span><span class="p">[</span><span class="nx">variant</span><span class="p">]}</span><span class="s2"> </span><span class="p">${</span><span class="nx">sizeClasses</span><span class="p">[</span><span class="nx">size</span><span class="p">]}</span><span class="s2">`</span><span class="p">}</span>
      <span class="nx">disabled</span><span class="o">=</span><span class="p">{</span><span class="nx">disabled</span> <span class="o">||</span> <span class="nx">loading</span><span class="p">}</span>
      <span class="nx">onClick</span><span class="o">=</span><span class="p">{</span><span class="nx">onClick</span><span class="p">}</span>
    <span class="o">&gt;</span>
      <span class="p">{</span><span class="nx">loading</span> <span class="o">&amp;&amp;</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">animate-spin mr-2 h-4 w-4 border-2 border-white border-t-transparent rounded-full</span><span class="dl">"</span> <span class="o">/&gt;</span><span class="p">}</span>
      <span class="p">{</span><span class="nx">children</span><span class="p">}</span>
    <span class="o">&lt;</span><span class="sr">/button</span><span class="err">&gt;
</span>  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// packages/ui-components/src/Card.tsx</span>
<span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">CardProps</span> <span class="p">{</span>
  <span class="nl">title</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">children</span><span class="p">:</span> <span class="nx">React</span><span class="p">.</span><span class="nx">ReactNode</span><span class="p">;</span>
  <span class="nl">className</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nf">Card</span><span class="p">({</span> <span class="nx">title</span><span class="p">,</span> <span class="nx">children</span><span class="p">,</span> <span class="nx">className</span> <span class="o">=</span> <span class="dl">''</span> <span class="p">}:</span> <span class="nx">CardProps</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return </span><span class="p">(</span>
    <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="p">{</span><span class="s2">`bg-white rounded-lg shadow-md border border-gray-200 </span><span class="p">${</span><span class="nx">className</span><span class="p">}</span><span class="s2">`</span><span class="p">}</span><span class="o">&gt;</span>
      <span class="p">{</span><span class="nx">title</span> <span class="o">&amp;&amp;</span> <span class="p">(</span>
        <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">px-6 py-4 border-b border-gray-200</span><span class="dl">"</span><span class="o">&gt;</span>
          <span class="o">&lt;</span><span class="nx">h3</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-lg font-semibold text-gray-900</span><span class="dl">"</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">title</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/h3</span><span class="err">&gt;
</span>        <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>      <span class="p">)}</span>
      <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">p-6</span><span class="dl">"</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">children</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>    <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// packages/ui-components/src/index.ts</span>
<span class="k">export</span> <span class="p">{</span> <span class="nx">Button</span><span class="p">,</span> <span class="kd">type</span> <span class="nx">ButtonProps</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Button</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="p">{</span> <span class="nx">Card</span><span class="p">,</span> <span class="kd">type</span> <span class="nx">CardProps</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./Card</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>

<h4 id="api-client-package">API Client Package</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> packages/api-client
<span class="nb">cd </span>packages/api-client
pnpm init
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@workspace/api-client"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"main"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.js"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"module"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.mjs"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"types"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./dist/index.d.ts"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsup src/index.ts --format cjs,esm --dts"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsup src/index.ts --format cjs,esm --dts --watch"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vitest"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"clean"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rm -rf dist"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@workspace/utils"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"typescript"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tsup"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vitest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// packages/api-client/src/index.ts</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ApiResponse</span><span class="p">,</span> <span class="nx">Logger</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@workspace/utils</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nc">ApiClient</span> <span class="p">{</span>
  <span class="k">private</span> <span class="nx">baseUrl</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="k">private</span> <span class="nx">logger</span><span class="p">:</span> <span class="nx">Logger</span><span class="p">;</span>

  <span class="nf">constructor</span><span class="p">(</span><span class="nx">baseUrl</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">baseUrl</span> <span class="o">=</span> <span class="nx">baseUrl</span><span class="p">;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">logger</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Logger</span><span class="p">(</span><span class="dl">'</span><span class="s1">ApiClient</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">private</span> <span class="k">async</span> <span class="nx">request</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span>
    <span class="nx">endpoint</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
    <span class="nx">options</span><span class="p">:</span> <span class="nx">RequestInit</span> <span class="o">=</span> <span class="p">{}</span>
  <span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">ApiResponse</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="k">this</span><span class="p">.</span><span class="nx">baseUrl</span><span class="p">}${</span><span class="nx">endpoint</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
    
    <span class="k">try</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
          <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
          <span class="p">...</span><span class="nx">options</span><span class="p">.</span><span class="nx">headers</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">...</span><span class="nx">options</span><span class="p">,</span>
      <span class="p">});</span>

      <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">response</span><span class="p">.</span><span class="nx">ok</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nc">Error</span><span class="p">(</span><span class="s2">`HTTP error! status: </span><span class="p">${</span><span class="nx">response</span><span class="p">.</span><span class="nx">status</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
      <span class="p">}</span>

      <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">options</span><span class="p">.</span><span class="nx">method</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">GET</span><span class="dl">'</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">endpoint</span><span class="p">}</span><span class="s2"> - Success`</span><span class="p">);</span>
      
      <span class="k">return</span> <span class="p">{</span>
        <span class="nx">data</span><span class="p">,</span>
        <span class="na">success</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="p">};</span>
    <span class="p">}</span> <span class="k">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">logger</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">options</span><span class="p">.</span><span class="nx">method</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">GET</span><span class="dl">'</span><span class="p">}</span><span class="s2"> </span><span class="p">${</span><span class="nx">endpoint</span><span class="p">}</span><span class="s2"> - Error`</span><span class="p">,</span> <span class="nx">error</span> <span class="kd">as </span><span class="nb">Error</span><span class="p">);</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="na">data</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
        <span class="na">success</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
        <span class="na">message</span><span class="p">:</span> <span class="nx">error</span> <span class="k">instanceof</span> <span class="nb">Error</span> <span class="p">?</span> <span class="nx">error</span><span class="p">.</span><span class="nx">message</span> <span class="p">:</span> <span class="dl">'</span><span class="s1">Unknown error</span><span class="dl">'</span><span class="p">,</span>
      <span class="p">};</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="kd">get</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">endpoint</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">ApiResponse</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">request</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">endpoint</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">post</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">endpoint</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">data</span><span class="p">:</span> <span class="kr">any</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">ApiResponse</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">request</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">endpoint</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">),</span>
    <span class="p">});</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">put</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">endpoint</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">data</span><span class="p">:</span> <span class="kr">any</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">ApiResponse</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">request</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">endpoint</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PUT</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">data</span><span class="p">),</span>
    <span class="p">});</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="k">delete</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">endpoint</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">ApiResponse</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">request</span><span class="o">&lt;</span><span class="nx">T</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">endpoint</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">DELETE</span><span class="dl">'</span><span class="p">,</span>
    <span class="p">});</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">User</span> <span class="p">{</span>
  <span class="nl">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">email</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">createdAt</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kr">interface</span> <span class="nx">Product</span> <span class="p">{</span>
  <span class="nl">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">name</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">price</span><span class="p">:</span> <span class="kr">number</span><span class="p">;</span>
  <span class="nl">description</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nc">UserService</span> <span class="p">{</span>
  <span class="nf">constructor</span><span class="p">(</span><span class="k">private</span> <span class="nx">client</span><span class="p">:</span> <span class="nx">ApiClient</span><span class="p">)</span> <span class="p">{}</span>

  <span class="k">async</span> <span class="nf">getUsers</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">ApiResponse</span><span class="o">&lt;</span><span class="nx">User</span><span class="p">[]</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">client</span><span class="p">.</span><span class="kd">get</span><span class="o">&lt;</span><span class="nx">User</span><span class="p">[]</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">'</span><span class="s1">/users</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nf">getUser</span><span class="p">(</span><span class="nx">id</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">ApiResponse</span><span class="o">&lt;</span><span class="nx">User</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">client</span><span class="p">.</span><span class="kd">get</span><span class="o">&lt;</span><span class="nx">User</span><span class="o">&gt;</span><span class="p">(</span><span class="s2">`/users/</span><span class="p">${</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nf">createUser</span><span class="p">(</span><span class="nx">userData</span><span class="p">:</span> <span class="nb">Omit</span><span class="o">&lt;</span><span class="nx">User</span><span class="p">,</span> <span class="dl">'</span><span class="s1">id</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">createdAt</span><span class="dl">'</span><span class="o">&gt;</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">ApiResponse</span><span class="o">&lt;</span><span class="nx">User</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">client</span><span class="p">.</span><span class="nx">post</span><span class="o">&lt;</span><span class="nx">User</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">'</span><span class="s1">/users</span><span class="dl">'</span><span class="p">,</span> <span class="nx">userData</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="step-4-create-application-packages">Step 4: Create Application Packages</h3>

<h4 id="react-web-app">React Web App</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> apps/web-app
<span class="nb">cd </span>apps/web-app
pnpm init
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@workspace/web-app"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"private"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vite"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsc &amp;&amp; vite build"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"preview"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vite preview"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vitest"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"clean"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rm -rf dist"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"react"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.2.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"react-dom"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.2.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@workspace/ui-components"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@workspace/api-client"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@workspace/utils"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@types/react"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.2.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@types/react-dom"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.2.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@vitejs/plugin-react"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"typescript"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vite"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.4.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vitest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"autoprefixer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^10.4.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"postcss"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^8.4.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tailwindcss"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^3.3.0"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Create a simple React app:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// apps/web-app/src/App.tsx</span>
<span class="k">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useEffect</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Button</span><span class="p">,</span> <span class="nx">Card</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@workspace/ui-components</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">ApiClient</span><span class="p">,</span> <span class="nx">UserService</span><span class="p">,</span> <span class="nx">User</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@workspace/api-client</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">formatCurrency</span><span class="p">,</span> <span class="nx">debounce</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@workspace/utils</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">apiClient</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ApiClient</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://jsonplaceholder.typicode.com</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">userService</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">UserService</span><span class="p">(</span><span class="nx">apiClient</span><span class="p">);</span>

<span class="kd">function</span> <span class="nf">App</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">users</span><span class="p">,</span> <span class="nx">setUsers</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="o">&lt;</span><span class="nx">User</span><span class="p">[]</span><span class="o">&gt;</span><span class="p">([]);</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">loading</span><span class="p">,</span> <span class="nx">setLoading</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">searchTerm</span><span class="p">,</span> <span class="nx">setSearchTerm</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="dl">''</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">debouncedSearch</span> <span class="o">=</span> <span class="nf">debounce</span><span class="p">((</span><span class="nx">term</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Searching for:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">term</span><span class="p">);</span>
    <span class="c1">// Implement search logic here</span>
  <span class="p">},</span> <span class="mi">300</span><span class="p">);</span>

  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">loadUsers</span> <span class="o">=</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="nf">setLoading</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
      <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">userService</span><span class="p">.</span><span class="nf">getUsers</span><span class="p">();</span>
      <span class="k">if </span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">success</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">setUsers</span><span class="p">(</span><span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">6</span><span class="p">));</span> <span class="c1">// Limit to 6 users for demo</span>
      <span class="p">}</span>
      <span class="nf">setLoading</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
    <span class="p">};</span>

    <span class="nf">loadUsers</span><span class="p">();</span>
  <span class="p">},</span> <span class="p">[]);</span>

  <span class="nf">useEffect</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">searchTerm</span><span class="p">)</span> <span class="p">{</span>
      <span class="nf">debouncedSearch</span><span class="p">(</span><span class="nx">searchTerm</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">},</span> <span class="p">[</span><span class="nx">searchTerm</span><span class="p">,</span> <span class="nx">debouncedSearch</span><span class="p">]);</span>

  <span class="k">return </span><span class="p">(</span>
    <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">min-h-screen bg-gray-50 py-8</span><span class="dl">"</span><span class="o">&gt;</span>
      <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">max-w-6xl mx-auto px-4</span><span class="dl">"</span><span class="o">&gt;</span>
        <span class="o">&lt;</span><span class="nx">h1</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-3xl font-bold text-gray-900 mb-8</span><span class="dl">"</span><span class="o">&gt;</span>
          <span class="nx">pnpm</span> <span class="nx">Workspace</span> <span class="nx">Demo</span> <span class="nx">App</span>
        <span class="o">&lt;</span><span class="sr">/h1</span><span class="err">&gt;
</span>        
        <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mb-6</span><span class="dl">"</span><span class="o">&gt;</span>
          <span class="o">&lt;</span><span class="nx">input</span>
            <span class="kd">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">text</span><span class="dl">"</span>
            <span class="nx">placeholder</span><span class="o">=</span><span class="dl">"</span><span class="s2">Search users...</span><span class="dl">"</span>
            <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">w-full max-w-md px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500</span><span class="dl">"</span>
            <span class="nx">value</span><span class="o">=</span><span class="p">{</span><span class="nx">searchTerm</span><span class="p">}</span>
            <span class="nx">onChange</span><span class="o">=</span><span class="p">{(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nf">setSearchTerm</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">)}</span>
          <span class="sr">/</span><span class="err">&gt;
</span>        <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>
        <span class="p">{</span><span class="nx">loading</span> <span class="p">?</span> <span class="p">(</span>
          <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-center py-8</span><span class="dl">"</span><span class="o">&gt;</span>
            <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full mx-auto</span><span class="dl">"</span><span class="o">&gt;&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>            <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">mt-2 text-gray-600</span><span class="dl">"</span><span class="o">&gt;</span><span class="nx">Loading</span> <span class="nx">users</span><span class="p">...</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>          <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>        <span class="p">)</span> <span class="p">:</span> <span class="p">(</span>
          <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6</span><span class="dl">"</span><span class="o">&gt;</span>
            <span class="p">{</span><span class="nx">users</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">user</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">(</span>
              <span class="o">&lt;</span><span class="nx">Card</span> <span class="nx">key</span><span class="o">=</span><span class="p">{</span><span class="nx">user</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span> <span class="nx">title</span><span class="o">=</span><span class="p">{</span><span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="o">&gt;</span>
                <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">space-y-3</span><span class="dl">"</span><span class="o">&gt;</span>
                  <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-gray-600</span><span class="dl">"</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">user</span><span class="p">.</span><span class="nx">email</span><span class="p">}</span><span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>                  <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-sm text-gray-500</span><span class="dl">"</span><span class="o">&gt;</span>
                    <span class="na">Joined</span><span class="p">:</span> <span class="p">{</span><span class="k">new</span> <span class="nc">Date</span><span class="p">(</span><span class="nx">user</span><span class="p">.</span><span class="nx">createdAt</span> <span class="o">||</span> <span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">()).</span><span class="nf">toLocaleDateString</span><span class="p">()}</span>
                  <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>                  <span class="o">&lt;</span><span class="nx">p</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">text-lg font-semibold text-green-600</span><span class="dl">"</span><span class="o">&gt;</span>
                    <span class="na">Value</span><span class="p">:</span> <span class="p">{</span><span class="nf">formatCurrency</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nf">random</span><span class="p">()</span> <span class="o">*</span> <span class="mi">1000</span> <span class="o">+</span> <span class="mi">100</span><span class="p">)}</span>
                  <span class="o">&lt;</span><span class="sr">/p</span><span class="err">&gt;
</span>                  <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="dl">"</span><span class="s2">flex space-x-2</span><span class="dl">"</span><span class="o">&gt;</span>
                    <span class="o">&lt;</span><span class="nx">Button</span> <span class="nx">size</span><span class="o">=</span><span class="dl">"</span><span class="s2">sm</span><span class="dl">"</span> <span class="nx">variant</span><span class="o">=</span><span class="dl">"</span><span class="s2">primary</span><span class="dl">"</span><span class="o">&gt;</span>
                      <span class="nx">View</span> <span class="nx">Profile</span>
                    <span class="o">&lt;</span><span class="sr">/Button</span><span class="err">&gt;
</span>                    <span class="o">&lt;</span><span class="nx">Button</span> <span class="nx">size</span><span class="o">=</span><span class="dl">"</span><span class="s2">sm</span><span class="dl">"</span> <span class="nx">variant</span><span class="o">=</span><span class="dl">"</span><span class="s2">secondary</span><span class="dl">"</span><span class="o">&gt;</span>
                      <span class="nx">Send</span> <span class="nx">Message</span>
                    <span class="o">&lt;</span><span class="sr">/Button</span><span class="err">&gt;
</span>                  <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>                <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>              <span class="o">&lt;</span><span class="sr">/Card</span><span class="err">&gt;
</span>            <span class="p">))}</span>
          <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>        <span class="p">)}</span>
      <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>    <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>  <span class="p">);</span>
<span class="p">}</span>

<span class="k">export</span> <span class="k">default</span> <span class="nx">App</span><span class="p">;</span>
</code></pre></div></div>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// apps/web-app/src/main.tsx</span>
<span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">ReactDOM</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-dom/client</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">App</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./App</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">./index.css</span><span class="dl">'</span><span class="p">;</span>

<span class="nx">ReactDOM</span><span class="p">.</span><span class="nf">createRoot</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">root</span><span class="dl">'</span><span class="p">)</span><span class="o">!</span><span class="p">).</span><span class="nf">render</span><span class="p">(</span>
  <span class="o">&lt;</span><span class="nx">React</span><span class="p">.</span><span class="nx">StrictMode</span><span class="o">&gt;</span>
    <span class="o">&lt;</span><span class="nx">App</span> <span class="o">/&gt;</span>
  <span class="o">&lt;</span><span class="sr">/React.StrictMode</span><span class="err">&gt;
</span><span class="p">);</span>
</code></pre></div></div>

<h4 id="nodejs-api-server">Node.js API Server</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> apps/api-server
<span class="nb">cd </span>apps/api-server
pnpm init
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@workspace/api-server"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"private"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"dev"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsx watch src/index.ts"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"build"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsup src/index.ts --format cjs"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"start"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node dist/index.js"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vitest"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"clean"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rm -rf dist"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"express"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.18.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"cors"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^2.8.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@workspace/utils"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@workspace/api-client"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@types/express"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.17.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@types/cors"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^2.8.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"typescript"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tsup"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"tsx"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^3.12.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vitest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// apps/api-server/src/index.ts</span>
<span class="k">import</span> <span class="nx">express</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">express</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">cors</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">cors</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Logger</span><span class="p">,</span> <span class="nx">slugify</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@workspace/utils</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">User</span><span class="p">,</span> <span class="nx">Product</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@workspace/api-client</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nf">express</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">logger</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Logger</span><span class="p">(</span><span class="dl">'</span><span class="s1">ApiServer</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">PORT</span> <span class="o">=</span> <span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">PORT</span> <span class="o">||</span> <span class="mi">3001</span><span class="p">;</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">use</span><span class="p">(</span><span class="nf">cors</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nf">use</span><span class="p">(</span><span class="nx">express</span><span class="p">.</span><span class="nf">json</span><span class="p">());</span>

<span class="c1">// Mock data</span>
<span class="kd">const</span> <span class="nx">users</span><span class="p">:</span> <span class="nx">User</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
  <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">John Doe</span><span class="dl">'</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="dl">'</span><span class="s1">john@example.com</span><span class="dl">'</span><span class="p">,</span> <span class="na">createdAt</span><span class="p">:</span> <span class="dl">'</span><span class="s1">2024-01-15</span><span class="dl">'</span> <span class="p">},</span>
  <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">2</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Jane Smith</span><span class="dl">'</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="dl">'</span><span class="s1">jane@example.com</span><span class="dl">'</span><span class="p">,</span> <span class="na">createdAt</span><span class="p">:</span> <span class="dl">'</span><span class="s1">2024-01-20</span><span class="dl">'</span> <span class="p">},</span>
  <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">3</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Bob Johnson</span><span class="dl">'</span><span class="p">,</span> <span class="na">email</span><span class="p">:</span> <span class="dl">'</span><span class="s1">bob@example.com</span><span class="dl">'</span><span class="p">,</span> <span class="na">createdAt</span><span class="p">:</span> <span class="dl">'</span><span class="s1">2024-02-01</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">];</span>

<span class="kd">const</span> <span class="nx">products</span><span class="p">:</span> <span class="nx">Product</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
  <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">1</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Laptop</span><span class="dl">'</span><span class="p">,</span> <span class="na">price</span><span class="p">:</span> <span class="mf">999.99</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">'</span><span class="s1">High-performance laptop</span><span class="dl">'</span> <span class="p">},</span>
  <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">2</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Phone</span><span class="dl">'</span><span class="p">,</span> <span class="na">price</span><span class="p">:</span> <span class="mf">699.99</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Latest smartphone</span><span class="dl">'</span> <span class="p">},</span>
  <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="dl">'</span><span class="s1">3</span><span class="dl">'</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Headphones</span><span class="dl">'</span><span class="p">,</span> <span class="na">price</span><span class="p">:</span> <span class="mf">199.99</span><span class="p">,</span> <span class="na">description</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Wireless headphones</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">];</span>

<span class="c1">// Routes</span>
<span class="nx">app</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/health</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">({</span> <span class="na">status</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ok</span><span class="dl">'</span><span class="p">,</span> <span class="na">timestamp</span><span class="p">:</span> <span class="k">new</span> <span class="nc">Date</span><span class="p">().</span><span class="nf">toISOString</span><span class="p">()</span> <span class="p">});</span>
<span class="p">});</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/users</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="dl">'</span><span class="s1">Fetching all users</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">(</span><span class="nx">users</span><span class="p">);</span>
<span class="p">});</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/users/:id</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">users</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="nx">u</span> <span class="o">=&gt;</span> <span class="nx">u</span><span class="p">.</span><span class="nx">id</span> <span class="o">===</span> <span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
  <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">user</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nf">status</span><span class="p">(</span><span class="mi">404</span><span class="p">).</span><span class="nf">json</span><span class="p">({</span> <span class="na">error</span><span class="p">:</span> <span class="dl">'</span><span class="s1">User not found</span><span class="dl">'</span> <span class="p">});</span>
  <span class="p">}</span>
  <span class="nx">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="s2">`Fetching user </span><span class="p">${</span><span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="p">});</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="dl">'</span><span class="s1">/users</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">email</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">body</span><span class="p">;</span>
  <span class="kd">const</span> <span class="na">newUser</span><span class="p">:</span> <span class="nx">User</span> <span class="o">=</span> <span class="p">{</span>
    <span class="na">id</span><span class="p">:</span> <span class="p">(</span><span class="nx">users</span><span class="p">.</span><span class="nx">length</span> <span class="o">+</span> <span class="mi">1</span><span class="p">).</span><span class="nf">toString</span><span class="p">(),</span>
    <span class="nx">name</span><span class="p">,</span>
    <span class="nx">email</span><span class="p">,</span>
    <span class="na">createdAt</span><span class="p">:</span> <span class="k">new</span> <span class="nc">Date</span><span class="p">().</span><span class="nf">toISOString</span><span class="p">(),</span>
  <span class="p">};</span>
  <span class="nx">users</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">newUser</span><span class="p">);</span>
  <span class="nx">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="s2">`Created user </span><span class="p">${</span><span class="nx">newUser</span><span class="p">.</span><span class="nx">id</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">status</span><span class="p">(</span><span class="mi">201</span><span class="p">).</span><span class="nf">json</span><span class="p">(</span><span class="nx">newUser</span><span class="p">);</span>
<span class="p">});</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/products</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="dl">'</span><span class="s1">Fetching all products</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">(</span><span class="nx">products</span><span class="p">);</span>
<span class="p">});</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/products/search</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">query</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">query</span><span class="p">.</span><span class="nx">q</span> <span class="kd">as </span><span class="kr">string</span><span class="p">;</span>
  <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">query</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">(</span><span class="nx">products</span><span class="p">);</span>
  <span class="p">}</span>
  
  <span class="kd">const</span> <span class="nx">searchSlug</span> <span class="o">=</span> <span class="nf">slugify</span><span class="p">(</span><span class="nx">query</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">filtered</span> <span class="o">=</span> <span class="nx">products</span><span class="p">.</span><span class="nf">filter</span><span class="p">(</span><span class="nx">p</span> <span class="o">=&gt;</span> 
    <span class="nf">slugify</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">name</span><span class="p">).</span><span class="nf">includes</span><span class="p">(</span><span class="nx">searchSlug</span><span class="p">)</span> <span class="o">||</span> 
    <span class="nf">slugify</span><span class="p">(</span><span class="nx">p</span><span class="p">.</span><span class="nx">description</span><span class="p">).</span><span class="nf">includes</span><span class="p">(</span><span class="nx">searchSlug</span><span class="p">)</span>
  <span class="p">);</span>
  
  <span class="nx">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="s2">`Searching products for: </span><span class="p">${</span><span class="nx">query</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">(</span><span class="nx">filtered</span><span class="p">);</span>
<span class="p">});</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">listen</span><span class="p">(</span><span class="nx">PORT</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">logger</span><span class="p">.</span><span class="nf">info</span><span class="p">(</span><span class="s2">`Server running on port </span><span class="p">${</span><span class="nx">PORT</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="step-5-shared-tools-and-configuration">Step 5: Shared Tools and Configuration</h3>

<h4 id="eslint-configuration-package">ESLint Configuration Package</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> tools/eslint-config
<span class="nb">cd </span>tools/eslint-config
pnpm init
</code></pre></div></div>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@workspace/eslint-config"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"main"</span><span class="p">:</span><span class="w"> </span><span class="s2">"index.js"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@typescript-eslint/eslint-plugin"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^6.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"@typescript-eslint/parser"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^6.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"eslint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^8.0.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"eslint-plugin-react"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^7.33.0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"eslint-plugin-react-hooks"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.6.0"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// tools/eslint-config/index.js</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">parser</span><span class="p">:</span> <span class="dl">'</span><span class="s1">@typescript-eslint/parser</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">extends</span><span class="p">:</span> <span class="p">[</span>
    <span class="dl">'</span><span class="s1">eslint:recommended</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">@typescript-eslint/recommended</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">plugin:react/recommended</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">plugin:react-hooks/recommended</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">],</span>
  <span class="na">plugins</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">@typescript-eslint</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">react-hooks</span><span class="dl">'</span><span class="p">],</span>
  <span class="na">rules</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">@typescript-eslint/no-unused-vars</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">error</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">@typescript-eslint/no-explicit-any</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">warn</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">react/react-in-jsx-scope</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">off</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">react/prop-types</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">off</span><span class="dl">'</span><span class="p">,</span>
  <span class="p">},</span>
  <span class="na">settings</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">react</span><span class="p">:</span> <span class="p">{</span>
      <span class="na">version</span><span class="p">:</span> <span class="dl">'</span><span class="s1">detect</span><span class="dl">'</span><span class="p">,</span>
    <span class="p">},</span>
  <span class="p">},</span>
<span class="p">};</span>
</code></pre></div></div>

<h3 id="step-6-root-configuration-files">Step 6: Root Configuration Files</h3>

<p>Create TypeScript configuration for the workspace:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">tsconfig.json</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"compilerOptions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"target"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ES2020"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"lib"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"DOM"</span><span class="p">,</span><span class="w"> </span><span class="s2">"DOM.Iterable"</span><span class="p">,</span><span class="w"> </span><span class="s2">"ES6"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"allowJs"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"skipLibCheck"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"esModuleInterop"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"allowSyntheticDefaultImports"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"strict"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"forceConsistentCasingInFileNames"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"moduleResolution"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bundler"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"resolveJsonModule"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"isolatedModules"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"noEmit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"jsx"</span><span class="p">:</span><span class="w"> </span><span class="s2">"react-jsx"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"declaration"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"declarationMap"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"sourceMap"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"baseUrl"</span><span class="p">:</span><span class="w"> </span><span class="s2">"."</span><span class="p">,</span><span class="w">
    </span><span class="nl">"paths"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"@workspace/*"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"./packages/*/src"</span><span class="p">,</span><span class="w"> </span><span class="s2">"./apps/*/src"</span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"include"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"apps/**/*"</span><span class="p">,</span><span class="w"> </span><span class="s2">"packages/**/*"</span><span class="p">,</span><span class="w"> </span><span class="s2">"tools/**/*"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"exclude"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"node_modules"</span><span class="p">,</span><span class="w"> </span><span class="s2">"**/dist"</span><span class="p">,</span><span class="w"> </span><span class="s2">"**/build"</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="advanced-pnpm-workspace-commands">Advanced pnpm Workspace Commands</h2>

<h3 id="installation-and-dependency-management">Installation and Dependency Management</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install dependencies for all packages</span>
pnpm <span class="nb">install</span>

<span class="c"># Add a dependency to a specific package</span>
pnpm add react <span class="nt">--filter</span> @workspace/web-app

<span class="c"># Add a dev dependency to all packages</span>
pnpm add <span class="nt">-D</span> prettier <span class="nt">--filter</span> <span class="s2">"*"</span>

<span class="c"># Add a workspace dependency</span>
pnpm add @workspace/utils <span class="nt">--filter</span> @workspace/api-server
</code></pre></div></div>

<h3 id="running-scripts">Running Scripts</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run build script in all packages</span>
pnpm <span class="nt">-r</span> build

<span class="c"># Run dev script in parallel for all packages</span>
pnpm <span class="nt">-r</span> <span class="nt">--parallel</span> dev

<span class="c"># Run script in specific package</span>
pnpm <span class="nt">--filter</span> @workspace/web-app dev

<span class="c"># Run script in packages matching pattern</span>
pnpm <span class="nt">--filter</span> <span class="s2">"./apps/*"</span> build
</code></pre></div></div>

<h3 id="advanced-filtering">Advanced Filtering</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Run commands on packages that depend on @workspace/utils</span>
pnpm <span class="nt">--filter</span> <span class="s2">"...@workspace/utils"</span> build

<span class="c"># Run commands on @workspace/utils and its dependents</span>
pnpm <span class="nt">--filter</span> <span class="s2">"@workspace/utils..."</span> build

<span class="c"># Run commands on changed packages (requires git)</span>
pnpm <span class="nt">--filter</span> <span class="s2">"[HEAD^1]"</span> build
</code></pre></div></div>

<h2 id="performance-benefits-in-action">Performance Benefits in Action</h2>

<p>Let’s see the real impact of pnpm workspaces:</p>

<h3 id="disk-usage-comparison">Disk Usage Comparison</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Traditional approach with separate repositories</span>
npm <span class="nb">install</span>  <span class="c"># ~200MB per project × 6 projects = ~1.2GB</span>

<span class="c"># With pnpm workspace</span>
pnpm <span class="nb">install</span>  <span class="c"># ~250MB total (shared dependencies)</span>
</code></pre></div></div>

<h3 id="installation-speed">Installation Speed</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Cold install</span>
<span class="nb">time </span>pnpm <span class="nb">install</span>  <span class="c"># ~15 seconds</span>

<span class="c"># Subsequent installs (with cache)</span>
<span class="nb">time </span>pnpm <span class="nb">install</span>  <span class="c"># ~2 seconds</span>
</code></pre></div></div>

<h3 id="dependency-analysis">Dependency Analysis</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check which packages use specific dependencies</span>
pnpm list react <span class="nt">--depth</span><span class="o">=</span>Infinity

<span class="c"># Audit dependencies across workspace</span>
pnpm audit

<span class="c"># Check outdated packages</span>
pnpm outdated
</code></pre></div></div>

<h2 id="best-practices-and-tips">Best Practices and Tips</h2>

<h3 id="1-use-workspace-protocol">1. <strong>Use Workspace Protocol</strong></h3>
<p>Always use <code class="language-plaintext highlighter-rouge">workspace:*</code> for internal dependencies to ensure version consistency:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"dependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"@workspace/utils"</span><span class="p">:</span><span class="w"> </span><span class="s2">"workspace:*"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="2-organise-by-purpose">2. <strong>Organise by Purpose</strong></h3>
<p>Structure your workspace logically:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">apps/</code> - Applications and services</li>
  <li><code class="language-plaintext highlighter-rouge">packages/</code> - Reusable libraries</li>
  <li><code class="language-plaintext highlighter-rouge">tools/</code> - Development tools and configs</li>
</ul>

<h3 id="3-centralise-configuration">3. <strong>Centralise Configuration</strong></h3>
<p>Share configuration files across packages:</p>
<ul>
  <li>Root <code class="language-plaintext highlighter-rouge">tsconfig.json</code> with package-specific extensions</li>
  <li>Shared ESLint, Prettier, and test configurations</li>
  <li>Unified build scripts in root package.json</li>
</ul>

<h3 id="4-version-management">4. <strong>Version Management</strong></h3>
<p>Use semantic versioning and consider tools like Changeset for version management:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pnpm add <span class="nt">-Dw</span> @changesets/cli
pnpm changeset init
</code></pre></div></div>

<h3 id="5-cicd-optimisation">5. <strong>CI/CD Optimisation</strong></h3>
<p>Leverage pnpm’s filtering for efficient CI/CD:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/ci.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build affected packages</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">pnpm --filter "[HEAD^1]" build</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Test changed packages</span>  
  <span class="na">run</span><span class="pi">:</span> <span class="s">pnpm --filter "[HEAD^1]" test</span>
</code></pre></div></div>

<h2 id="troubleshooting-common-issues">Troubleshooting Common Issues</h2>

<h3 id="phantom-dependencies">Phantom Dependencies</h3>
<p>pnpm prevents phantom dependencies by default, but you might encounter issues:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check for phantom dependencies</span>
pnpm audit <span class="nt">--audit-level</span> moderate

<span class="c"># Fix by adding missing dependencies</span>
pnpm add missing-package <span class="nt">--filter</span> affected-package
</code></pre></div></div>

<h3 id="hoisting-issues">Hoisting Issues</h3>
<p>If you need to hoist specific packages:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .pnpmrc</span>
<span class="s">public-hoist-pattern[]=*eslint*</span>
<span class="s">public-hoist-pattern[]=*prettier*</span>
</code></pre></div></div>

<h3 id="version-conflicts">Version Conflicts</h3>
<p>Handle version conflicts across workspace:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check version conflicts</span>
pnpm list <span class="nt">--depth</span><span class="o">=</span>Infinity | <span class="nb">grep </span>conflict

<span class="c"># Resolve by updating to compatible versions</span>
pnpm update <span class="nt">--filter</span> <span class="s2">"*"</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>pnpm workspaces represent a significant evolution in JavaScript package management, offering superior performance, strict dependency management, and excellent monorepo support. By implementing the patterns shown in this tutorial, you’ll achieve:</p>

<ul>
  <li><strong>60-80% reduction in disk usage</strong> compared to traditional approaches</li>
  <li><strong>3-5x faster installation times</strong> through intelligent caching and linking</li>
  <li><strong>Elimination of phantom dependencies</strong> and version conflicts</li>
  <li><strong>Streamlined development workflow</strong> across multiple related packages</li>
</ul>

<p>The example workspace we’ve built demonstrates real-world patterns including shared utilities, UI components, API clients, and multiple applications all working together seamlessly. This foundation can scale to support enterprise-level monorepos with dozens of packages and applications.</p>

<p>Start small with a few related packages, then gradually expand your workspace as your project grows. The investment in setting up pnpm workspaces pays dividends in development velocity, dependency management, and build performance.</p>

<p>Happy coding with pnpm workspaces! 🚀</p>]]></content><author><name>Glen Thomas</name></author><category term="Software Engineering" /><category term="Software Engineering" /><category term="JavaScript" /><category term="TypeScript" /><category term="Monorepo" /><category term="Package Management" /><summary type="html"><![CDATA[Managing multiple related packages and applications can quickly become a nightmare with traditional package managers. Enter pnpm workspaces – a powerful feature that transforms how we handle monorepos, offering superior performance, disk efficiency, and dependency management compared to npm or Yarn workspaces.]]></summary></entry><entry><title type="html">Modeling Relational Data in DynamoDB: The Adjacency List Pattern</title><link href="https://blog.glen-thomas.com/software%20engineering/2025/09/10/modeling-relational-data-in-dynamodb-the-adjacency-list-pattern.html" rel="alternate" type="text/html" title="Modeling Relational Data in DynamoDB: The Adjacency List Pattern" /><published>2025-09-10T23:42:00+01:00</published><updated>2025-09-10T23:42:00+01:00</updated><id>https://blog.glen-thomas.com/software%20engineering/2025/09/10/modeling-relational-data-in-dynamodb-the-adjacency-list-pattern</id><content type="html" xml:base="https://blog.glen-thomas.com/software%20engineering/2025/09/10/modeling-relational-data-in-dynamodb-the-adjacency-list-pattern.html"><![CDATA[<p>One of the biggest challenges when transitioning from relational databases to DynamoDB is figuring out how to model relationships between entities. General design principles in Amazon DynamoDB recommend that you keep the number of tables you use to a minimum. In the majority of cases, it is recommended that you use a single table. The Adjacency List pattern is a powerful technique that allows you to store multiple entity types and their relationships in a single DynamoDB table while supporting efficient queries for various access patterns.</p>

<h2 id="what-is-the-adjacency-list-pattern">What is the Adjacency List Pattern?</h2>

<p>The Adjacency List pattern is inspired by graph theory, where each node in a graph maintains a list of its adjacent nodes. In DynamoDB terms, this means storing both entities and their relationships as items in the same table, using a consistent key structure that enables efficient traversal of related data.
The key insight is that relationships themselves become first-class entities in your data model, stored alongside your primary entities in the same table.</p>

<h2 id="core-design-principles">Core Design Principles</h2>

<p>The Adjacency List pattern follows these principles:</p>

<ol>
  <li><strong>Single Table Design:</strong> All entities and relationships live in one table</li>
  <li><strong>Hierarchical Keys:</strong> Use compound keys that represent the relationship hierarchy</li>
  <li><strong>Bidirectional Relationships:</strong> Store relationships in both directions for flexible querying</li>
  <li><strong>Type Indicators:</strong> Use prefixes or attributes to distinguish entity types</li>
</ol>

<h2 id="example-e-commerce-platform">Example: E-commerce Platform</h2>

<p>Let’s build a comprehensive example using an e-commerce platform with these entities:</p>

<ul>
  <li>Users (customers and sellers)</li>
  <li>Products</li>
  <li>Orders</li>
  <li>Categories</li>
  <li>Reviews</li>
</ul>

<p>To start designing a DynamoDB table that will scale efficiently, you must take several steps first to identify the access patterns that are required by the systems that it needs to support:</p>

<ul>
  <li>For new applications, review user stories about activities and objectives. Document the various use cases you identify, and analyse the access patterns that they require.</li>
  <li>For existing applications, analyse query logs to find out how people are currently using the system and what the key access patterns are.</li>
</ul>

<p>After completing this process, you should end up with a list that might look something like the following.</p>

<ol>
  <li>Get user details</li>
  <li>Get user’s orders (with order details)</li>
  <li>Get product with reviews</li>
  <li>Get products in category</li>
  <li>Get order with all items</li>
</ol>

<h3 id="table-schema">Table Schema</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Table: EcommerceData
Partition Key: PK (String)
Sort Key: SK (String)
Attributes: Type, Individual entity attributes, GSI1PK, GSI1SK, CreatedAt, UpdatedAt

Global Secondary Index: GSI1
Partition Key: GSI1PK
Sort Key: GSI1SK
</code></pre></div></div>

<p>The partition key is the entity attribute that uniquely identifies the item and is referred to generically on all items as PK.</p>

<p>The sort key attribute contains an attribute value that you can use for an inverted index or global secondary index. It is generically referred to as SK.</p>

<p>Instead of using a nested Data attribute, we use individual attributes for each entity property. This approach provides better query performance, enables attribute-level filtering and projection, and allows individual attributes to be used as GSI keys. The GSI1PK and GSI1SK attributes enable reverse lookups and time-based queries across multiple access patterns using a single global secondary index.</p>

<p>The GSI1 GSI enables reverse lookups and time-based queries.</p>

<h3 id="entity-and-relationship-design">Entity and Relationship Design</h3>

<h4 id="1-user-entities">1. User Entities</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Customer</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USER#12345"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USER#12345"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"User"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"userId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"12345"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"john@example.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"userType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"customer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USER"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-15T10:30:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="err">//</span><span class="w"> </span><span class="err">Seller</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USER#67890"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USER#67890"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"User"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"userId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"67890"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TechStore Inc"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"email"</span><span class="p">:</span><span class="w"> </span><span class="s2">"contact@techstore.com"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"userType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"seller"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USER"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-10T08:15:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="2-product-entities">2. Product Entities</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Product"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"productId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Wireless Headphones"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"price"</span><span class="p">:</span><span class="w"> </span><span class="mf">199.99</span><span class="p">,</span><span class="w">
  </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Premium wireless headphones"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"sellerId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"67890"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-16T14:20:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="3-category-entities-and-relationships">3. Category Entities and Relationships</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Category</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CATEGORY#electronics"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CATEGORY#electronics"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Category"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"categoryId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"electronics"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Electronics"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Electronic devices and accessories"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="err">//</span><span class="w"> </span><span class="err">Product-Category</span><span class="w"> </span><span class="err">Relationship</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CATEGORY#electronics"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ProductCategory"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"productId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"categoryId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"electronics"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CATEGORY#electronics"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="4-order-entities-and-relationships">4. Order Entities and Relationships</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Order</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ORDER#ORD001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ORDER#ORD001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Order"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"orderId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ORD001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"customerId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"12345"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"total"</span><span class="p">:</span><span class="w"> </span><span class="mf">199.99</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"shipped"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"orderDate"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-20T12:00:00Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USER#12345"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ORDER#2024-01-20T12:00:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="err">//</span><span class="w"> </span><span class="err">Order-Product</span><span class="w"> </span><span class="err">Relationship</span><span class="w"> </span><span class="err">(Order</span><span class="w"> </span><span class="err">Item)</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ORDER#ORD001"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"OrderItem"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"orderId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ORD001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"productId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ABC123"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"quantity"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"unitPrice"</span><span class="p">:</span><span class="w"> </span><span class="mf">199.99</span><span class="p">,</span><span class="w">
  </span><span class="nl">"totalPrice"</span><span class="p">:</span><span class="w"> </span><span class="mf">199.99</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ORDER#ORD001"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="5-review-entities-and-relationships">5. Review Entities and Relationships</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Review</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REVIEW#REV001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REVIEW#REV001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Review"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reviewId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REV001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"customerId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"12345"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"productId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
  </span><span class="nl">"comment"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Excellent sound quality!"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reviewDate"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-25T09:30:00Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"CreatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-25T09:30:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="err">//</span><span class="w"> </span><span class="err">User-Review</span><span class="w"> </span><span class="err">Relationship</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USER#12345"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REVIEW#REV001"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"UserReview"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"customerId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"12345"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reviewId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REV001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REVIEW#REV001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USER#12345"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"CreatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-25T09:30:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="err">//</span><span class="w"> </span><span class="err">Product-Review</span><span class="w"> </span><span class="err">Relationship</span><span class="w">  
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REVIEW#REV001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ProductReview"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"productId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"reviewId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REV001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"rating"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REVIEW#REV001"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"GSI1SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"CreatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-25T09:30:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="access-patterns-and-queries">Access Patterns and Queries</h3>

<h4 id="1-get-user-details">1. Get User Details</h4>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">TableName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">EcommerceData</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">Key</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">PK</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USER#12345</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">SK</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USER#12345</span><span class="dl">'</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<h4 id="2-get-users-orders-with-order-details">2. Get User’s Orders (with order details)</h4>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">TableName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">EcommerceData</span><span class="dl">'</span><span class="p">,</span> 
  <span class="na">IndexName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GSI1</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">KeyConditionExpression</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GSI1PK = :pk AND begins_with(GSI1SK, :sk)</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">ExpressionAttributeValues</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">:pk</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">USER#12345</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">:sk</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ORDER#</span><span class="dl">'</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<h4 id="3-get-product-with-reviews">3. Get Product with Reviews</h4>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// First get the product</span>
<span class="kd">const</span> <span class="nx">productParams</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">TableName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">EcommerceData</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">KeyConditionExpression</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PK = :pk</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">ExpressionAttributeValues</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">:pk</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PRODUCT#ABC123</span><span class="dl">'</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>This returns the product and all its relationships (reviews, categories, etc.)</p>

<h4 id="4-get-recent-reviews-for-a-product">4. Get Recent Reviews for a Product</h4>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">TableName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">EcommerceData</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">KeyConditionExpression</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PK = :pk AND begins_with(SK, :sk)</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">ExpressionAttributeValues</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">:pk</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PRODUCT#ABC123</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">:sk</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">REVIEW#</span><span class="dl">'</span>
  <span class="p">},</span>
  <span class="na">ScanIndexForward</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="c1">// Get most recent first</span>
  <span class="na">Limit</span><span class="p">:</span> <span class="mi">10</span>
<span class="p">};</span>
</code></pre></div></div>

<h4 id="5-get-products-in-category">5. Get Products in Category</h4>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">TableName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">EcommerceData</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">IndexName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GSI1</span><span class="dl">'</span><span class="p">,</span> 
  <span class="na">KeyConditionExpression</span><span class="p">:</span> <span class="dl">'</span><span class="s1">GSI1PK = :pk AND begins_with(GSI1SK, :sk)</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">ExpressionAttributeValues</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">:pk</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">CATEGORY#electronics</span><span class="dl">'</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">:sk</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PRODUCT#</span><span class="dl">'</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<h4 id="6-get-order-with-all-items">6. Get Order with All Items</h4>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">TableName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">EcommerceData</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">KeyConditionExpression</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PK = :pk</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">ExpressionAttributeValues</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">:pk</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ORDER#ORD001</span><span class="dl">'</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Returns order details and all order items in a single query</p>

<h4 id="7-get-customers-reviews">7. Get Customer’s Reviews</h4>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">TableName</span><span class="p">:</span> <span class="nx">TABLE_NAME</span><span class="p">,</span>
  <span class="na">KeyConditionExpression</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PK = :pk AND begins_with(SK, :sk)</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">ExpressionAttributeValues</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">:pk</span><span class="dl">'</span><span class="p">:</span> <span class="s2">`USER#</span><span class="p">${</span><span class="nx">customerId</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">:sk</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">REVIEW#</span><span class="dl">'</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<h4 id="8-get-customers-orders-alternative-pattern">8. Get Customer’s Orders (Alternative Pattern)</h4>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">TableName</span><span class="p">:</span> <span class="nx">TABLE_NAME</span><span class="p">,</span>
  <span class="na">KeyConditionExpression</span><span class="p">:</span> <span class="dl">'</span><span class="s1">PK = :pk AND begins_with(SK, :sk)</span><span class="dl">'</span><span class="p">,</span>
  <span class="na">ExpressionAttributeValues</span><span class="p">:</span> <span class="p">{</span>
    <span class="dl">'</span><span class="s1">:pk</span><span class="dl">'</span><span class="p">:</span> <span class="s2">`USER#</span><span class="p">${</span><span class="nx">userId</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span>
    <span class="dl">'</span><span class="s1">:sk</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">ORDER#</span><span class="dl">'</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<h2 id="complete-relationship-structure">Complete Relationship Structure</h2>

<p>The implementation includes these relationship types:</p>

<ol>
  <li><strong>OrderItem</strong>: Links orders to products (PK: <code class="language-plaintext highlighter-rouge">ORDER#xxx</code>, SK: <code class="language-plaintext highlighter-rouge">PRODUCT#xxx</code>)</li>
  <li><strong>ProductCategory</strong>: Links products to categories (PK: <code class="language-plaintext highlighter-rouge">PRODUCT#xxx</code>, SK: <code class="language-plaintext highlighter-rouge">CATEGORY#xxx</code>)</li>
  <li><strong>UserReview</strong>: Links users to their reviews (PK: <code class="language-plaintext highlighter-rouge">USER#xxx</code>, SK: <code class="language-plaintext highlighter-rouge">REVIEW#xxx</code>)</li>
  <li><strong>ProductReview</strong>: Links products to their reviews (PK: <code class="language-plaintext highlighter-rouge">PRODUCT#xxx</code>, SK: <code class="language-plaintext highlighter-rouge">REVIEW#xxx</code>)</li>
  <li><strong>CustomerOrder</strong>: Links customers to their orders (PK: <code class="language-plaintext highlighter-rouge">USER#xxx</code>, SK: <code class="language-plaintext highlighter-rouge">ORDER#xxx</code>)</li>
</ol>

<p>Each relationship is stored bidirectionally with appropriate GSI keys for reverse lookups, enabling efficient queries in both directions.</p>

<h2 id="advanced-patterns">Advanced Patterns</h2>

<h3 id="many-to-many-relationships">Many-to-Many Relationships</h3>

<p>For complex many-to-many relationships, you can create junction entities:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Product-Tag</span><span class="w"> </span><span class="err">Relationship</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TAG#wireless"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ProductTag"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"productId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ABC123"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"tagName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"wireless"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TAG#wireless"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"CreatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-16T14:20:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="hierarchical-categories">Hierarchical Categories</h3>

<p>For nested categories, you can model parent-child relationships:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Subcategory</span><span class="w"> </span><span class="err">relationship</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CATEGORY#electronics"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CHILD#headphones"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CategoryChild"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"parentCategoryId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"electronics"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"childCategoryId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"headphones"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"childName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Headphones"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CATEGORY#headphones"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"GSI1SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PARENT#electronics"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"CreatedAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-01T00:00:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="time-series-data">Time-Series Data</h3>

<p>Use the sort key to store time-based data:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"PK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRODUCT#ABC123"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"SK"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PRICE#2024-01-20T10:00:00Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PriceHistory"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"productId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ABC123"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"price"</span><span class="p">:</span><span class="w"> </span><span class="mf">199.99</span><span class="p">,</span><span class="w">
  </span><span class="nl">"timestamp"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-20T10:00:00Z"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="best-practices">Best Practices</h2>

<ol>
  <li>
    <p>Design for Your Access Patterns
Always start by identifying your access patterns before designing the table structure. The Adjacency List pattern shines when you need to traverse relationships efficiently.</p>
  </li>
  <li>
    <p>Use Meaningful Prefixes
Consistent prefixes make your data self-documenting and enable powerful query patterns:</p>
  </li>
</ol>

<ul>
  <li><code class="language-plaintext highlighter-rouge">USER#</code> for users</li>
  <li><code class="language-plaintext highlighter-rouge">PRODUCT#</code> for products</li>
  <li><code class="language-plaintext highlighter-rouge">ORDER#</code> for orders</li>
  <li><code class="language-plaintext highlighter-rouge">REVIEW#</code> for reviews</li>
</ul>

<ol>
  <li>
    <p>Leverage Sort Key Flexibility
The sort key can represent different types of relationships or attributes, making single queries very powerful.</p>
  </li>
  <li>
    <p>Consider Hot Partitions
Distribute your data to avoid hot partitions. If you have very popular products or users, consider adding random suffixes or using time-based partitioning.</p>
  </li>
  <li>
    <p>Plan for Growth
Design your key structure to accommodate future entity types and relationships without requiring major refactoring.</p>
  </li>
</ol>

<h2 id="performance-considerations">Performance Considerations</h2>

<p>The Adjacency List pattern offers several performance benefits:</p>

<ul>
  <li><strong>Single Query Efficiency:</strong> Get an entity and all its relationships in one query</li>
  <li><strong>Reduced Network Round Trips:</strong> Fewer API calls compared to normalised approaches</li>
  <li><strong>Flexible Querying:</strong> Support multiple access patterns with the same data structure</li>
  <li><strong>Cost Effective:</strong> Fewer read operations mean lower costs</li>
</ul>

<p>However, be aware of these considerations:</p>

<p>However, be aware of item size limits (400KB) and ensure your entities don’t grow too large when storing many relationships.</p>

<h3 id="item-size-limits">Item Size Limits</h3>

<p>DynamoDB has a 400KB item size limit. When using adjacency lists, monitor your item sizes and ensure your entities don’t grow too large when storing entities with many relationships.</p>

<h3 id="hot-partition-management">Hot Partition Management</h3>

<p>Managing hot partitions is crucial for maintaining DynamoDB performance and scalability, especially as your application grows and certain entities become more popular. Hot partitions occur when a disproportionate amount of read or write traffic is directed to a small subset of partition keys, leading to throttling and increased latency. By proactively monitoring, designing for distribution, and implementing techniques such as write sharding, you can mitigate the risks associated with hot partitions and ensure your DynamoDB tables remain responsive under heavy load.</p>

<h4 id="monitor-cloudwatch-metrics-for-throttling-and-hot-partition-indicators">Monitor CloudWatch metrics for throttling and hot partition indicators</h4>

<p>Regularly monitor Amazon CloudWatch metrics such as <code class="language-plaintext highlighter-rouge">ConsumedReadCapacityUnits</code>, <code class="language-plaintext highlighter-rouge">ConsumedWriteCapacityUnits</code>, <code class="language-plaintext highlighter-rouge">ThrottledRequests</code>, and <code class="language-plaintext highlighter-rouge">ReturnedItemCount</code> for your DynamoDB table. Pay special attention to metrics at the partition level, as spikes in throttling or uneven capacity consumption can indicate hot partitions. Set up CloudWatch alarms to alert you when thresholds are exceeded, enabling proactive scaling or refactoring of your key design. Use the DynamoDB Console’s “Explore Table” and “Capacity” dashboards to visualise partition activity and identify problematic access patterns.</p>

<h4 id="implement-write-sharding-for-high-traffic-entities">Implement write sharding for high-traffic entities</h4>

<p>Write sharding is a technique used to distribute write operations across multiple partitions or items to avoid hot spots in databases like DynamoDB. When a particular entity (such as a popular user or product) receives a large number of writes, storing all updates in a single item can lead to throttling and degraded performance. By sharding writes, you spread the load across several items, reducing contention and improving scalability.</p>

<p>To implement write sharding, you can append a random suffix or a time-based value to the partition key for popular items. This creates multiple versions of the same entity, each with a unique key. When writing data, you select a shard (randomly or based on the current time), and when reading, you aggregate data from all shards.</p>

<p>Here’s a simple example of how to implement write sharding for a high-traffic entity in DynamoDB using TypeScript and the AWS SDK v3:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">PutCommand</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@aws-sdk/lib-dynamodb</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">docClient</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./config</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">function</span> <span class="nf">getShardKey</span><span class="p">(</span><span class="nx">baseKey</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">numShards</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">string</span> <span class="p">{</span>
    <span class="c1">// Choose a random shard suffix</span>
    <span class="kd">const</span> <span class="nx">shardId</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nf">floor</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nf">random</span><span class="p">()</span> <span class="o">*</span> <span class="nx">numShards</span><span class="p">);</span>
    <span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="nx">baseKey</span><span class="p">}</span><span class="s2">#SHARD</span><span class="p">${</span><span class="nx">shardId</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nf">writeShardedEntity</span><span class="p">(</span><span class="nx">entityId</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span> <span class="nx">activityData</span><span class="p">:</span> <span class="kr">any</span><span class="p">,</span> <span class="nx">numShards</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">pk</span> <span class="o">=</span> <span class="nf">getShardKey</span><span class="p">(</span><span class="s2">`USER#</span><span class="p">${</span><span class="nx">entityId</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="nx">numShards</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">sk</span> <span class="o">=</span> <span class="s2">`ACTIVITY#</span><span class="p">${</span><span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">()}</span><span class="s2">`</span><span class="p">;</span>

    <span class="kd">const</span> <span class="nx">item</span> <span class="o">=</span> <span class="p">{</span>
        <span class="na">PK</span><span class="p">:</span> <span class="nx">pk</span><span class="p">,</span>
        <span class="na">SK</span><span class="p">:</span> <span class="nx">sk</span><span class="p">,</span>
        <span class="na">Type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">UserActivity</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">userId</span><span class="p">:</span> <span class="nx">entityId</span><span class="p">,</span>
        <span class="na">action</span><span class="p">:</span> <span class="nx">activityData</span><span class="p">.</span><span class="nx">action</span><span class="p">,</span>
        <span class="na">productId</span><span class="p">:</span> <span class="nx">activityData</span><span class="p">.</span><span class="nx">productId</span><span class="p">,</span>
        <span class="na">timestamp</span><span class="p">:</span> <span class="nb">Date</span><span class="p">.</span><span class="nf">now</span><span class="p">(),</span>
        <span class="na">CreatedAt</span><span class="p">:</span> <span class="k">new</span> <span class="nc">Date</span><span class="p">().</span><span class="nf">toISOString</span><span class="p">()</span>
    <span class="p">};</span>

    <span class="k">await</span> <span class="nx">docClient</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="k">new</span> <span class="nc">PutCommand</span><span class="p">({</span>
        <span class="na">TableName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">EcommerceData</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">Item</span><span class="p">:</span> <span class="nx">item</span>
    <span class="p">}));</span>
<span class="p">}</span>

<span class="c1">// Usage</span>
<span class="k">await</span> <span class="nf">writeShardedEntity</span><span class="p">(</span><span class="dl">"</span><span class="s2">12345</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">action</span><span class="p">:</span> <span class="dl">"</span><span class="s2">purchase</span><span class="dl">"</span><span class="p">,</span> <span class="na">productId</span><span class="p">:</span> <span class="dl">"</span><span class="s2">ABC123</span><span class="dl">"</span> <span class="p">},</span> <span class="mi">5</span><span class="p">);</span>
</code></pre></div></div>

<p>This approach distributes writes for the same user across 5 shards, reducing the risk of hot partitions.</p>

<h4 id="gsi-projection-strategy">GSI Projection Strategy</h4>

<p>Use <code class="language-plaintext highlighter-rouge">KEYS_ONLY</code> projections when you only need to identify related items.
Use <code class="language-plaintext highlighter-rouge">INCLUDE</code> projections for frequently accessed attributes.
Reserve <code class="language-plaintext highlighter-rouge">ALL</code> projections for read-heavy access patterns where you need complete item data.</p>

<h4 id="aggregation-consistency">Aggregation Consistency</h4>

<ul>
  <li>Use eventually consistent aggregations for non-critical metrics</li>
  <li>Implement strong consistency patterns for financial or inventory data</li>
  <li>Consider the trade-offs between real-time updates and system performance</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>The Adjacency List pattern is a powerful technique for modeling relational data in DynamoDB. It enables you to maintain the flexibility of relational modeling while leveraging DynamoDB’s performance characteristics. By storing entities and relationships together using hierarchical keys, you can support complex queries with excellent performance.</p>

<p>The key to success with this pattern is careful planning of your access patterns and consistent use of meaningful key structures. When implemented correctly, it can handle complex relational scenarios while maintaining the scalability and performance benefits that make DynamoDB attractive for modern applications.</p>

<p>Remember that NoSQL data modeling is fundamentally different from relational modeling – embrace the denormalisation and design for your specific access patterns rather than trying to maintain traditional normal forms.</p>

<h2 id="complete-working-example">Complete Working Example</h2>

<p>A complete TypeScript implementation of this adjacency list pattern is available, including:</p>

<ul>
  <li><strong>Full sample data</strong>: Users, products, orders, reviews, and categories with complete relationships</li>
  <li><strong>Deployment scripts</strong>: Easy AWS setup and data loading</li>
  <li><strong>Type-safe queries</strong>: Strongly typed TypeScript interfaces for all entities and operations</li>
</ul>

<p>The implementation demonstrates real-world patterns including write sharding, hierarchical data, and complex many-to-many relationships.</p>

<p><a href="https://github.com/glenthomas/DynamoDB-Adjacency-List-Pattern-Example">DynamoDB-Adjacency-List-Pattern-Example</a></p>]]></content><author><name>Glen Thomas</name></author><category term="Software Engineering" /><category term="Software Engineering" /><category term="AWS" /><category term="Data" /><summary type="html"><![CDATA[One of the biggest challenges when transitioning from relational databases to DynamoDB is figuring out how to model relationships between entities. General design principles in Amazon DynamoDB recommend that you keep the number of tables you use to a minimum. In the majority of cases, it is recommended that you use a single table. The Adjacency List pattern is a powerful technique that allows you to store multiple entity types and their relationships in a single DynamoDB table while supporting efficient queries for various access patterns.]]></summary></entry><entry><title type="html">Mapping Complex Distributed Systems with C4 Diagrams and Structurizr DSL</title><link href="https://blog.glen-thomas.com/architecture/2025/08/27/mapping-complex-distributed-systems-with-c4-diagrams-and-structurizr-dsl.html" rel="alternate" type="text/html" title="Mapping Complex Distributed Systems with C4 Diagrams and Structurizr DSL" /><published>2025-08-27T11:54:00+01:00</published><updated>2025-08-27T11:54:00+01:00</updated><id>https://blog.glen-thomas.com/architecture/2025/08/27/mapping-complex-distributed-systems-with-c4-diagrams-and-structurizr-dsl</id><content type="html" xml:base="https://blog.glen-thomas.com/architecture/2025/08/27/mapping-complex-distributed-systems-with-c4-diagrams-and-structurizr-dsl.html"><![CDATA[<p>As distributed systems grow in complexity, architects face an increasingly challenging task: how do you effectively communicate the structure and relationships within a system that spans dozens of services, multiple teams, and various technology stacks?</p>

<p>Traditional architectural diagrams, especially those based on <a href="https://en.wikipedia.org/wiki/Unified_Modeling_Language">UML (Unified Modeling Language)</a>, were once the industry standard, but have largely fallen out of favour for documenting modern software systems. UML’s comprehensive notation often leads to overly detailed, hard-to-maintain diagrams that struggle to keep pace with the rapid evolution of distributed architectures. As a result, many teams find that classic boxes-and-arrows drawings, whether UML or ad hoc, quickly become unwieldy and confusing rather than clarifying.</p>

<p>Enter the C4 model and <a href="https://structurizr.com/">Structurizr</a>, a powerful combination that provides a structured, scalable approach to documenting distributed systems architecture.</p>

<h2 id="understanding-the-c4-model">Understanding the C4 Model</h2>

<p>The C4 model, created by software architect Simon Brown, provides a hierarchical approach to documenting software architecture through four levels of abstraction.</p>

<p>The different levels of zoom allow you to tell different stories to different audiences. You don’t need to use all four levels of diagram; only those that add value. The system context (level 1) and container diagrams (level 2) are sufficient for most software development teams.</p>

<h3 id="context-level-1">Context (Level 1)</h3>

<p>The system context diagram shows how your software system fits into the world around it. A software system is typically something that a single team are responsible for producing. This high-level view focuses on users, external systems, and the primary purposes of your system. Think of it as a map showing your system as a single box surrounded by its users and neighboring systems.</p>

<p>Here is an example system context diagram for a <strong>Payments &amp; Subscriptions Platform</strong>. At the highest level, the Payments &amp; Subscriptions Platform sits between users, front-end applications, and external services like payment gateways and tax providers.</p>

<p><a href="/assets/images/system_context.png" class="align-center"><img src="/assets/images/system_context.png" alt="System Context" /></a></p>

<h3 id="containers-level-2">Containers (Level 2)</h3>

<p>Container diagrams breaks down your system into major technical building blocks. In distributed systems, these are typically your microservices, databases, message queues, API gateways and web applications. Each container represents a separately deployable unit that executes code or stores data.</p>

<p>Zooming in on our Payments &amp; Subscriptions Platform, we can see the individual containers that make up the system. This includes a set of services, data stores and infrastructure containers.</p>

<p><a href="/assets/images/container_diagram.png" class="align-center"><img src="/assets/images/container_diagram.png" alt="Container Diagram" /></a></p>

<h3 id="components-level-3">Components (Level 3)</h3>

<p>The component diagram zooms into individual containers to show their internal structure. This level reveals the major structural building blocks within each service; the controllers, services, repositories, and other architectural patterns you’ve implemented.</p>

<h3 id="code-level-4">Code (Level 4)</h3>

<p>The code-level diagram provides the most detailed view, showing how components are implemented in code. While useful for complex algorithms or critical components, this level is often omitted for distributed systems documentation as it becomes too granular.</p>

<h2 id="why-the-c4-model-works-for-distributed-systems">Why the C4 Model Works for Distributed Systems</h2>

<p>The hierarchical nature of C4 diagrams makes them particularly well suited for distributed architectures. You can start with a system context diagram that shows your entire platform as a single entity, then progressively drill down into containers (your microservices), and finally into the internal structure of specific services when needed.</p>

<p>This approach solves several common problems with distributed system documentation. It eliminates the temptation to create massive, incomprehensible diagrams that try to show everything at once. Instead, it provides multiple views at appropriate levels of detail for different audiences; executives see the context, teams see the containers, and developers see the components.</p>

<h2 id="introducing-structurizr-dsl">Introducing Structurizr DSL</h2>

<p>While many tools can create C4 diagrams, Structurizr DSL (Domain Specific Language) stands out for its code-like approach to defining architecture. Instead of dragging and dropping boxes in a visual editor, you write textual descriptions of your system’s structure.</p>

<p>Here’s why this matters for distributed systems. As your architecture evolves (and distributed systems evolve constantly), maintaining visual diagrams becomes a significant overhead. Teams often abandon architectural documentation because keeping visual diagrams in sync with reality requires too much manual effort. Structurizr DSL addresses this by treating your architecture as code, enabling version control, automated generation, and integration with your development workflow.</p>

<h2 id="getting-started-with-structurizr-dsl">Getting Started with Structurizr DSL</h2>

<p>Let’s walk through creating documentation for a typical distributed e-commerce system. We’ll start with a basic workspace definition and build up our architecture model with a context diagram. We’ll then move on to the container diagram.</p>

<h3 id="workspace">Workspace</h3>

<p>A workspace is a wrapper for a software architecture model, views and other documentation.</p>

<p>The tool that we will use for rendering, Structurizr Lite, expects a file called workspace.dsl, so let’s create that file in our architecture repository and add the code for the workspace (format: <code class="language-plaintext highlighter-rouge">workspace "{workspace_name}" "{workspace_description}"</code>):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>workspace "E-commerce Platform" "Architecture for our distributed e-commerce system" {
}
</code></pre></div></div>

<h3 id="model">Model</h3>

<p>Inside our workspace we add the model. The model includes definitions using types:</p>

<ul>
  <li><strong>softwareSystem:</strong> the e-commerce platform that we are modelling, and other external systems that it interacts with. Format: <code class="language-plaintext highlighter-rouge">{system_id} = softwareSystem "{system_name}" "{system_description}"</code></li>
  <li><strong>person:</strong> the people who interact with the software system. Format: <code class="language-plaintext highlighter-rouge">{person_id} = person "{person_name}" "{person_description}"</code></li>
  <li><strong>-&gt;:</strong> relationships between different software systems and persons. Format: <code class="language-plaintext highlighter-rouge">{from_id} -&gt; {to_id} "{relationship_description}"</code></li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>workspace "E-commerce Platform" "Architecture for our distributed e-commerce system" {
    model {
        # Define people and external systems
        customer = person "Customer" "A customer of our e-commerce platform"
        admin = person "Administrator" "Administrative user managing the platform"
        
        paymentProvider = softwareSystem "Payment Provider" "External payment processing"
        emailService = softwareSystem "Email Service" "External email delivery service"
        
        # Define our main system
        ecommerceSystem = softwareSystem "E-commerce Platform" "Core e-commerce functionality"
        
        # Define relationships at the system level
        customer -&gt; ecommerceSystem "Uses"
        admin -&gt; ecommerceSystem "Administers"
        ecommerceSystem -&gt; paymentProvider "Processes payments via"
        ecommerceSystem -&gt; emailService "Sends emails via"
    }
}
</code></pre></div></div>

<h3 id="view">View</h3>

<p>The view DSL element is where we define the diagrams that we would like to build for our model. In this example we are creating a system context diagram for the e-commerce system (<code class="language-plaintext highlighter-rouge">systemContext ecommerceSystem "SystemContext"</code>) and including everything else from the model that has a relationship to it (<code class="language-plaintext highlighter-rouge">include *</code>). The <code class="language-plaintext highlighter-rouge">autoLayout</code> option instructs the rendering tool to generate a readable layout automatically. The <code class="language-plaintext highlighter-rouge">styles</code> element allows for control of the appearance of the model elements on the rendered diagram (background, color, fontSize, shape).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>workspace "E-commerce Platform" "Architecture for our distributed e-commerce system" {
    model {
        # Define people and external systems
        customer = person "Customer" "A customer of our e-commerce platform"
        admin = person "Administrator" "Administrative user managing the platform"
        
        paymentProvider = softwareSystem "Payment Provider" "External payment processing"
        emailService = softwareSystem "Email Service" "External email delivery service"
        
        # Define our main system
        ecommerceSystem = softwareSystem "E-commerce Platform" "Core e-commerce functionality"
        
        # Define relationships at the system level
        customer -&gt; ecommerceSystem "Uses"
        admin -&gt; ecommerceSystem "Administers"
        ecommerceSystem -&gt; paymentProvider "Processes payments via"
        ecommerceSystem -&gt; emailService "Sends emails via"
    }

    views {
        systemContext ecommerceSystem "SystemContext" {
            include *
            autoLayout
        }

        styles {
            element "Person" {
                color #ffffff
                fontSize 22
                shape Person
            }
            element "Software System" {
                background #1168bd
                color #ffffff
            }
        }
    }
}
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">include</code> and <code class="language-plaintext highlighter-rouge">exclude</code> keywords within the <code class="language-plaintext highlighter-rouge">view</code> element can be used to filter which items appear within the view.</p>

<p><code class="language-plaintext highlighter-rouge">* -&gt; *</code>: all relationships between all elements
<code class="language-plaintext highlighter-rouge">source -&gt; *</code>: all relationships from source to any element
<code class="language-plaintext highlighter-rouge">* -&gt; destination</code>: all relationships from any element to destination
<code class="language-plaintext highlighter-rouge">source -&gt; destination</code>: all relationships from source to destination</p>

<h3 id="viewing-the-diagram-with-structurizr-lite">Viewing the diagram with Structurizr Lite</h3>

<p>Now that we have a basic system context diagram, we can render and view the diagram using Structurizr Lite, a free version of Structurizr, packaged as a Docker container, and designed for developers who want to quickly author and/or view software architecture diagrams, documentation, and architecture decision records (ADRs).</p>

<p>If you don’t have docker/podman installed, I would suggest installing podman with Homebrew:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>podman
</code></pre></div></div>

<p>Then run a container with the structurizr/lite image, exposing port 8080 to the host and mapping the directory containing your workspace.dsl file to the container’s <code class="language-plaintext highlighter-rouge">/usr/local/structurizr</code> directory.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>podman run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="nt">-p</span> 8080:8080 <span class="nt">-v</span> /Users/glen.thomas/Documents/ecomm_platform:/usr/local/structurizr structurizr/lite
</code></pre></div></div>

<p>Load the Structurizr light web page in your browser (http://localhost:8080) and see the diagram rendered in the UI with automatic layout.</p>

<p><a href="/assets/images/structurizer_system_context.png" class="align-center"><img src="/assets/images/structurizer_system_context.png" alt="Structurizr Lite System Context" /></a></p>

<h3 id="container-level-diagram">Container level diagram</h3>

<p>Now we can expand our architecture diagrams with a container diagram for the e-commerce system. Within the <code class="language-plaintext highlighter-rouge">softwareSystem</code> element we will add definitions for the containers and within the model element we will map the relationships between the containers. The format for each container definition is: <code class="language-plaintext highlighter-rouge">{container_id} = container "{container_name}" "{container_description}" "{technology}"</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>workspace "E-commerce Platform" "Architecture for our distributed e-commerce system" {

    model {
        # Define people and external systems
        customer = person "Customer" "A customer of our e-commerce platform"
        admin = person "Administrator" "Administrative user managing the platform"
        
        paymentProvider = softwareSystem "Payment Provider" "External payment processing"
        emailService = softwareSystem "Email Service" "External email delivery service"
        
        # Define our main system
        ecommerceSystem = softwareSystem "E-commerce Platform" "Core e-commerce functionality" {
            
            # Container level - our microservices
            webApp = container "Web Application" "Customer-facing web interface" "React/JavaScript"
            adminPanel = container "Admin Panel" "Administrative interface" "React/JavaScript"
            apiGateway = container "API Gateway" "Entry point for all API requests" "Spring Cloud Gateway"
            
            userService = container "User Service" "Manages customer accounts and authentication" "Spring Boot/Java"
            catalogService = container "Catalog Service" "Manages product catalog" "Node.js/Express"
            orderService = container "Order Service" "Handles order processing" "Spring Boot/Java"
            paymentService = container "Payment Service" "Processes payments" "Python/FastAPI"
            
            userDatabase = container "User Database" "Stores user account information" "PostgreSQL"
            catalogDatabase = container "Catalog Database" "Stores product information" "MongoDB"
            orderDatabase = container "Order Database" "Stores order data" "PostgreSQL"
            
            messageQueue = container "Message Queue" "Handles asynchronous communication" "Apache Kafka"
        }
        
        # Define relationships at the system level
        customer -&gt; ecommerceSystem "Uses"
        admin -&gt; ecommerceSystem "Administers"
        ecommerceSystem -&gt; paymentProvider "Processes payments via"
        ecommerceSystem -&gt; emailService "Sends emails via"
        
        # Define relationships at the container level
        customer -&gt; webApp "Uses"
        admin -&gt; adminPanel "Uses"
        
        webApp -&gt; apiGateway "Makes API calls to"
        adminPanel -&gt; apiGateway "Makes API calls to"
        
        apiGateway -&gt; userService "Routes requests to"
        apiGateway -&gt; catalogService "Routes requests to"
        apiGateway -&gt; orderService "Routes requests to"
        
        userService -&gt; userDatabase "Reads from and writes to"
        catalogService -&gt; catalogDatabase "Reads from and writes to"
        orderService -&gt; orderDatabase "Reads from and writes to"
        orderService -&gt; paymentService "Initiates payments via"
        
        paymentService -&gt; paymentProvider "Processes payments via"
        paymentService -&gt; messageQueue "Publishes events to"
        orderService -&gt; messageQueue "Publishes events to"
        userService -&gt; messageQueue "Publishes events to"
        
        orderService -&gt; emailService "Sends order confirmations via"
    }
    
    views {
        systemContext ecommerceSystem "SystemContext" {
            include *
            autoLayout
        }
        
        container ecommerceSystem "Containers" {
            include *
            autoLayout
        }
        
        styles {
            element "Person" {
                color #ffffff
                fontSize 22
                shape Person
            }
            element "Software System" {
                background #1168bd
                color #ffffff
            }
            element "Container" {
                background #438dd5
                color #ffffff
            }
        }
    }
}
</code></pre></div></div>

<p>This DSL definition creates multiple views of our e-commerce system. The system context view shows how our platform interacts with users and external services. The container view reveals the internal microservices architecture, showing how services communicate through the API gateway and message queue.</p>

<p>Save your workspace.dl file and reload the Structurizer page in the browser to render the new diagram. You can navigate into the container diagram using either the diagram list view on the left side panel, or by clicking the zoom cursor on the software system.</p>

<p><a href="/assets/images/structurizer_containers.png" class="align-center"><img src="/assets/images/structurizer_containers.png" alt="Structurizr Lite Container" /></a></p>

<h3 id="further-styling-options">Further Styling Options</h3>

<p>The database containers in our diagram are currently rectangular boxes, the same as our other containers. A visual distinction would make them more easily identifiable by the viewer. We could change the shape of the databases to enable this.</p>

<p>Lets add a “Database” tag to the database container definitions that we can use in an element style.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>userDatabase = container "User Database" "Stores user account information" "PostgreSQL" {
    tags "Database"
}
</code></pre></div></div>

<p>Then add a new style for the Database-tagged element with shape “cylinder”.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>element "Database" {
    shape cylinder
}
</code></pre></div></div>

<p>Reload Structurizr Lite and our database containers are now rendered as cylinders rather than rectangles.</p>

<p><a href="/assets/images/structurizer_containers_db.png" class="align-center"><img src="/assets/images/structurizer_containers_db.png" alt="Structurizr Lite Container" /></a></p>

<h3 id="modeling-component-level-detail">Modeling Component-Level Detail</h3>

<p>For critical services, you can add component-level detail to show internal structure. Here’s how you might model the User Service components:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>userService = container "User Service" "Manages customer accounts and authentication" "Spring Boot/Java" {
    authController = component "Authentication Controller" "Handles login and registration requests" "Spring MVC Controller"
    userController = component "User Controller" "Handles user profile operations" "Spring MVC Controller"
    authService = component "Authentication Service" "Implements authentication logic" "Spring Service"
    userService = component "User Service" "Implements user management logic" "Spring Service"
    userRepository = component "User Repository" "Data access for users" "Spring Data JPA"
    tokenService = component "Token Service" "Manages JWT tokens" "Spring Service"
}
</code></pre></div></div>

<p>This level of detail helps development teams understand the internal structure of services they’re working on while keeping the higher-level views clean and focused.</p>

<h2 id="adrs-architecture-decision-records">ADRs (Architecture Decision Records)</h2>

<p>Architecture Decision Records (ADRs) are concise documents that capture important architectural decisions and their context. Structurizr supports managing ADRs alongside your architecture model, making it easy to keep decisions visible and version-controlled.</p>

<h3 id="adding-adrs-in-structurizr-dsl">Adding ADRs in Structurizr DSL</h3>

<p>You can add source-controlled ADRs in your architecture repository and use Structurizr to browse and read them.</p>

<p>Create a directory at docs/architecture-decision-records to hold your ADR markdown documents and add an example:</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># Implement Event-Driven Architecture with Message Queues</span>

Date: 2025-08-28

<span class="gu">## Status</span>

Accepted

<span class="gu">## Context</span>

Our microservices architecture requires reliable communication between services. We need to handle:
<span class="p">
-</span> Order processing workflows that span multiple services
<span class="p">-</span> Real-time inventory updates across fulfillment and e-commerce systems
<span class="p">-</span> Marketing campaign triggers based on customer behavior
<span class="p">-</span> Asynchronous notifications and email sending
<span class="p">-</span> Decoupling of services to prevent cascading failures

Synchronous API calls alone would create tight coupling and potential cascade failures.

<span class="gu">## Decision</span>

We will implement event-driven architecture using message queues for asynchronous communication:
<span class="p">
-</span> <span class="gs">**Apache Kafka**</span> for high-throughput event streaming (e-commerce and marketing systems)
<span class="p">-</span> <span class="gs">**AWS SQS**</span> for reliable message delivery (fulfillment system)

Services will publish domain events (order placed, payment processed, inventory updated) and subscribe to relevant events from other services.

<span class="gu">## Consequences</span>

<span class="gs">**Positive:**</span>
<span class="p">-</span> Loose coupling between services
<span class="p">-</span> Better resilience - services can handle temporary unavailability of dependencies
<span class="p">-</span> Scalability - async processing allows for better resource utilisation
<span class="p">-</span> Audit trail - events provide natural business process logging
<span class="p">-</span> Flexibility - new services can easily subscribe to existing events

<span class="gs">**Negative:**</span>
<span class="p">-</span> Eventual consistency - data may be temporarily inconsistent across services
<span class="p">-</span> Complexity in handling message ordering and duplicate processing
<span class="p">-</span> Debugging distributed workflows becomes more challenging
<span class="p">-</span> Need for sophisticated monitoring of message queues
<span class="p">-</span> Potential message loss scenarios need handling

<span class="gs">**Mitigation:**</span>
<span class="p">-</span> Implement idempotent message handlers
<span class="p">-</span> Use message deduplication strategies
<span class="p">-</span> Establish clear event schemas and versioning
<span class="p">-</span> Implement comprehensive monitoring and alerting
<span class="p">-</span> Design for graceful degradation when message queues are unavailable
</code></pre></div></div>

<p>Update your workspace to load the ADRs from the directory (you can also use the <code class="language-plaintext highlighter-rouge">!adrs</code> directive within a software system or a container element).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>workspace "Retail Business" "Architecture for our online retail business" {
    !adrs docs/architecture-decision-records
}
</code></pre></div></div>

<h3 id="viewing-adrs-in-structurizr-lite">Viewing ADRs in Structurizr Lite</h3>

<p>When you load your workspace in Structurizr Lite, ADRs appear in the documentation section. You can link decisions to specific elements (e.g. containers or systems) by referencing their IDs, providing context for why certain architectural choices were made.</p>

<p><a href="/assets/images/structurizer_adrs_list.png" class="align-center"><img src="/assets/images/structurizer_adrs_list.png" alt="ADRs" /></a></p>

<p><a href="/assets/images/structurizer_adr.png" class="align-center"><img src="/assets/images/structurizer_adr.png" alt="ADR" /></a></p>

<h2 id="best-practices-for-distributed-systems">Best Practices for Distributed Systems</h2>

<p>When documenting distributed systems with C4 and Structurizr DSL, several practices will make your diagrams more valuable and maintainable.</p>

<p><strong>Start with the system context</strong> and work your way down. Don’t try to model everything at once. Begin with a clear understanding of your system’s boundaries and external dependencies, then gradually add detail.</p>

<p><strong>Group related containers</strong> using <a href="https://docs.structurizr.com/dsl/language#tags">tags</a> or <a href="https://docs.structurizr.com/dsl/language#group">groups</a>. This helps when your system grows to dozens of services. You might tag all payment-related services or group services by business capability. The <a href="https://docs.structurizr.com/dsl/language#group">group</a> keyword provides a way to define a named grouping of elements at the same level of abstraction, which will be rendered as a boundary around those elements.</p>

<p><strong>Document key quality attributes</strong> in your descriptions. Don’t just say what a service does, mention if it’s high-availability, handles sensitive data, or has specific performance requirements.</p>

<p><strong>Keep your DSL in version control</strong> alongside your code. This enables tracking changes over time and correlating architectural evolution with code changes.</p>

<h2 id="managing-complexity-as-systems-grow">Managing Complexity as Systems Grow</h2>

<p>As your distributed system evolves, your C4 models will naturally grow more complex. Here are strategies for managing this complexity without losing clarity.</p>

<p><strong>Create focused views</strong> for specific audiences or scenarios. You might create a <a href="https://docs.structurizr.com/dsl/language#deployment-view">deployment view</a> showing how containers map to infrastructure, or a security view highlighting trust boundaries and data flows.</p>

<p><strong>Use <a href="https://docs.structurizr.com/dsl/language#dynamic-view">dynamic diagrams</a></strong> to show how your system behaves during key scenarios. Structurizr supports sequence diagrams that can illustrate the flow of requests through your distributed system during checkout, user registration, or other critical processes.</p>

<p><strong>Establish governance practices</strong> around your architectural documentation. Assign ownership for keeping different parts of the model current, and include architecture reviews as part of your change management process.</p>

<h3 id="decentralising-architecture-diagrams">Decentralising Architecture Diagrams</h3>

<p>As distributed systems scale, it becomes impractical for a single team to maintain a monolithic DSL file describing the entire architecture. Structurizr DSL supports modularisation, allowing you to split your architecture model into multiple files. This enables different teams to own and update their respective parts of the system independently.</p>

<h4 id="using-include-to-compose-models">Using <code class="language-plaintext highlighter-rouge">!include</code> to Compose Models</h4>

<p>You can break up your DSL into logical modules, such as per team, service, or bounded context, and use the <code class="language-plaintext highlighter-rouge">!include</code> directive to assemble them into a complete model. For example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>workspace "E-commerce Platform" "Architecture for our distributed e-commerce system" {
    !include teams/customer-team.dsl
    !include teams/order-team.dsl
    !include teams/payment-team.dsl

    views {
        !include views/system-context.dsl
        !include views/containers.dsl
        !include views/styles.dsl
    }
}
</code></pre></div></div>

<p>Each included file can define people, systems, containers, relationships, or views relevant to that domain. Teams can manage their own files in separate repositories or directories, enabling decentralised ownership and parallel development.</p>

<h4 id="using-extends-to-compose-models">Using <code class="language-plaintext highlighter-rouge">extends</code> to Compose Models</h4>

<p>Structurizr DSL provides a way to extend an existing workspace. This allows you to define your software systems and their relationships in a central file.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>workspace "Retail Business" "Architecture for our online retail business" {
    !identifiers hierarchical

    model {
        # Define people and external systems
        customer = person "Customer" "A customer of our e-commerce platform"
        admin = person "Administrator" "Administrative user managing the platform"
        
        paymentProvider = softwareSystem "Payment Provider" "External payment processing"
        emailService = softwareSystem "Email Service" "External email delivery service"
        
        ecommerceSystem = softwareSystem "E-commerce Platform" "Core e-commerce functionality"
        fulfilmentSystem = softwareSystem "Fulfilment Platform" "Order fulfilment system"
        crmSystem = softwareSystem "CRM System" "Customer relationship management platform"
        analyticsSystem = softwareSystem "Analytics Platform" "Aggregates and analyzes business data"
        marketingSystem = softwareSystem "Marketing Automation" "Manages campaigns and customer engagement"
        inventorySystem = softwareSystem "Inventory Management" "Tracks stock levels and warehouse operations"
        supportSystem = softwareSystem "Customer Support" "Handles customer inquiries and support tickets"
        recommendationEngine = softwareSystem "Recommendation Engine" "Provides personalized product recommendations"
        searchService = softwareSystem "Search Service" "Enables product search functionality"
        shippingProvider = softwareSystem "Shipping Provider" "External shipping and logistics integration"
        taxService = softwareSystem "Tax Calculation Service" "Handles tax calculations and compliance"

        # Define relationships at the system level
        customer -&gt; ecommerceSystem "Uses"
        admin -&gt; ecommerceSystem "Administers"
        ecommerceSystem -&gt; paymentProvider "Processes payments via"
        ecommerceSystem -&gt; emailService "Sends emails via"
    }
}
</code></pre></div></div>

<p>Then you can extend the base workspace for each system using the <code class="language-plaintext highlighter-rouge">extends</code> keyword. The workspace being extended can be a local filename or a public HTTP URL.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>workspace extends retail-business.dsl {
    model {
        !element ecommerceSystem {
            webApp = container "Web Application" "Customer-facing web interface" "React/JavaScript"
            adminPanel = container "Admin Panel" "Administrative interface" "React/JavaScript"
            apiGateway = container "API Gateway" "Entry point for all API requests" "Spring Cloud Gateway"
            
            userService = container "User Service" "Manages customer accounts and authentication" "Spring Boot/Java"
            catalogService = container "Catalog Service" "Manages product catalog" "Node.js/Express"
            orderService = container "Order Service" "Handles order processing" "Spring Boot/Java"
            paymentService = container "Payment Service" "Processes payments" "Python/FastAPI"
            
            userDatabase = container "User Database" "Stores user account information" "PostgreSQL" {
                tags "Database"
            }
            catalogDatabase = container "Catalog Database" "Stores product information" "MongoDB" {
                tags "Database"
            }
            orderDatabase = container "Order Database" "Stores order data" "PostgreSQL" {
                tags "Database"
            }
            
            messageQueue = container "Message Queue" "Handles asynchronous communication" "Apache Kafka"

            # Define relationships at the container level
            customer -&gt; webApp "Uses"
            admin -&gt; adminPanel "Uses"
            
            webApp -&gt; apiGateway "Makes API calls to"
            adminPanel -&gt; apiGateway "Makes API calls to"
            
            apiGateway -&gt; userService "Routes requests to"
            apiGateway -&gt; catalogService "Routes requests to"
            apiGateway -&gt; orderService "Routes requests to"
            
            userService -&gt; userDatabase "Reads from and writes to"
            catalogService -&gt; catalogDatabase "Reads from and writes to"
            orderService -&gt; orderDatabase "Reads from and writes to"
            orderService -&gt; paymentService "Initiates payments via"
            
            paymentService -&gt; paymentProvider "Processes payments via"
            paymentService -&gt; messageQueue "Publishes events to"
            orderService -&gt; messageQueue "Publishes events to"
            userService -&gt; messageQueue "Publishes events to"
            
            orderService -&gt; emailService "Sends order confirmations via"
        }
    }
    
    views {
        systemContext ecommerceSystem "SystemContext" {
            include *
            autoLayout
        }
        
        container ecommerceSystem "Containers" {
            include *
            autoLayout
        }
        
        styles {
            element "Person" {
                color #ffffff
                fontSize 22
                shape Person
            }
            element "Software System" {
                background #1168bd
                color #ffffff
            }
            element "Container" {
                background #438dd5
                color #ffffff
            }
            element "Database" {
                shape cylinder
            }
        }
    }
}
</code></pre></div></div>

<h4 id="decentralised-architecture-diagrams">Decentralised Architecture Diagrams</h4>

<p>The <code class="language-plaintext highlighter-rouge">!includes</code> and <code class="language-plaintext highlighter-rouge">extends</code> directives can be used to build a complete picture of your systems landscape, with decentralised architecture diagrams managed by different teams. See my GitHub repository <a href="https://github.com/glenthomas/Mapping-Distributed-Systems-with-C4-Diagrams-and-Structurizr-DSL">Mapping-Distributed-Systems-with-C4-Diagrams-and-Structurizr-DSL</a> for a complete example.</p>

<p>View a live demo of a static website with limited features at <a href="https://structurizr-sample.glen-thomas.com">structurizr-sample.glen-thomas.com</a>.</p>

<h4 id="best-practices-for-modular-dsl">Best Practices for Modular DSL</h4>

<ul>
  <li><strong>Define clear boundaries:</strong> Agree on naming conventions and IDs to avoid conflicts between modules.</li>
  <li><strong>Centralise shared elements:</strong> Common definitions (e.g. external systems, global styles) can live in a shared file included by all modules.</li>
  <li><strong>Automate integration:</strong> Use CI/CD pipelines to validate and assemble the full model from all parts, ensuring consistency and correctness.</li>
</ul>

<h2 id="automating-documentation-generation">Automating Documentation Generation</h2>

<p>One of the biggest advantages of using Structurizr DSL is the ability to integrate architectural documentation into your development workflow. You can set up continuous integration pipelines that automatically regenerate diagrams when your DSL files change.</p>

<p>Consider creating a simple script that validates your DSL syntax and uploads diagrams to a shared location. This ensures your architectural documentation stays current without manual intervention.</p>

<p>You can also integrate with tools like PlantUML or export static images for inclusion in other documentation. The key is making architectural documentation a byproduct of your development process rather than a separate, easily-forgotten activity.</p>

<p>You could also use your DSL models to generate other artifacts; API documentation, service catalogs, or even infrastructure-as-code templates. This creates a single source of truth for your system’s structure that flows into multiple downstream uses.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The C4 model and Structurizr provide a powerful framework for documenting distributed systems that grows with your architecture. By treating architectural documentation as code, you can create maintainable, version-controlled representations of your system that integrate naturally with your development process.</p>

<p>The hierarchical nature of C4 diagrams ensures that different stakeholders get appropriate levels of detail, while Structurizr DSL’s textual approach makes it practical to keep documentation current in fast-moving distributed system environments.</p>

<p>Start simple with a system context diagram, add container-level detail for your key services, and gradually expand your model as your understanding and system complexity grow. Your future self, and your team, will thank you for the clarity and maintainability that this approach brings to distributed system architecture documentation.</p>]]></content><author><name>Glen Thomas</name></author><category term="Architecture" /><category term="Software Engineering" /><summary type="html"><![CDATA[As distributed systems grow in complexity, architects face an increasingly challenging task: how do you effectively communicate the structure and relationships within a system that spans dozens of services, multiple teams, and various technology stacks?]]></summary></entry></feed>