<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Krishan Thisera</title>
    <description>The latest articles on DEV Community by Krishan Thisera (@krishanthisera).</description>
    <link>https://dev.to/krishanthisera</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1126663%2F19ef509e-6bed-4b2a-8e7b-f6b20b58a7c4.jpeg</url>
      <title>DEV Community: Krishan Thisera</title>
      <link>https://dev.to/krishanthisera</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krishanthisera"/>
    <language>en</language>
    <item>
      <title>Designing for Assumed Compromise: Securing Autonomous AI Agents on Kubernetes</title>
      <dc:creator>Krishan Thisera</dc:creator>
      <pubDate>Fri, 18 Sep 2026 08:44:06 +0000</pubDate>
      <link>https://dev.to/krishanthisera/designing-for-assumed-compromise-securing-autonomous-ai-agents-on-kubernetes-38a2</link>
      <guid>https://dev.to/krishanthisera/designing-for-assumed-compromise-securing-autonomous-ai-agents-on-kubernetes-38a2</guid>
      <description>&lt;h3&gt;
  
  
  A Defense-in-Depth Blueprint from Orchestration to Identity, Credentials, and Scale
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;An agent sandbox&lt;/strong&gt; is a runtime instance, a container, microVM, or Kubernetes pod, built to run an autonomous AI agent's generated code and tool calls behind an isolation boundary. That boundary is meant to keep a compromised session from reaching the host machine or other tenants. How effectively it does that depends on which isolation technique backs it. Autonomous coding and browsing agents built on large language models now routinely generate and execute code, install packages, browse the live web, and handle credentials. Each of those actions carries only probabilistic guarantees about what the model will do, and that uncertainty is the problem an agent sandbox exists to solve.&lt;/p&gt;

&lt;p&gt;That combination compounds two risks: &lt;strong&gt;arbitrary code execution&lt;/strong&gt; and instructions an attacker can influence through &lt;strong&gt;prompt injection&lt;/strong&gt;. Together, they have pushed the infrastructure conversation away from asking whether a container is hardened enough, toward &lt;strong&gt;assuming the container will eventually be compromised&lt;/strong&gt; and designing every layer around that assumption.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtdo1cp7abecqo6592n5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtdo1cp7abecqo6592n5.png" alt="Diagram of nested sandbox, runtime, network, identity and governance layers defending a Kubernetes-orchestrated agent against malicious code execution, prompt injection and data exfiltration" width="800" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article covers that security posture, together with what it costs and how you govern a fleet of sessions rather than one, once you run it at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 1: The Kubernetes-Native Orchestration Primitive
&lt;/h2&gt;

&lt;p&gt;Kubernetes offers two dominant workload abstractions. Deployments manage stateless, interchangeable pods, and StatefulSets manage numbered fleets with generic identities. Neither fits an agent session, a singleton needing a stable hostname, persistent storage, and a lifecycle that pauses and resumes rather than only starting and stopping. Teams hand-assembled a StatefulSet of one, a Service, and a persistent volume claim (PVC). That combination had no shared lifecycle controller.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Sandbox&lt;/strong&gt; custom resource definition (CRD), maintained by &lt;a href="https://github.com/kubernetes-sigs/agent-sandbox" rel="noopener noreferrer"&gt;&lt;code&gt;kubernetes-sigs/agent-sandbox&lt;/code&gt;&lt;/a&gt; under Kubernetes SIG Apps, closes that gap with a declarative API for one stateful pod with a stable identity. Three CRDs extend it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SandboxTemplate&lt;/strong&gt; enforces security defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SandboxWarmPool&lt;/strong&gt; pre-provisions instances to cut cold-start latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SandboxClaim&lt;/strong&gt; lets a framework request an instance without managing the template.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikpa0ydh7sfbxmusvqxx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fikpa0ydh7sfbxmusvqxx.png" alt="Flow diagram from SandboxTemplate through SandboxWarmPool and SandboxClaim to an active Sandbox instance, with RuntimeClass providing delegated isolation" width="799" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 2: Choosing an Isolation Technique
&lt;/h2&gt;

&lt;p&gt;The Sandbox CRD manages a pod under the hood, so it inherits the standard Kubernetes &lt;code&gt;runtimeClassName&lt;/code&gt; field for selecting the runtime, the same field any pod-based workload can set. &lt;code&gt;runc&lt;/code&gt;, gVisor, and Kata Containers are the common runtime options it selects between, and seccomp layers a syscall filter on top of whichever one is chosen. Together the four trade isolation strength against cost differently. &lt;code&gt;runc&lt;/code&gt; is the default low-level runtime that Docker and containerd use to create every container. It offers the least isolation of the four. It isolates a process using the host kernel's own namespaces and control groups (cgroups), so every container on a node shares a single kernel. That is fine for trusted code, but risky once an autonomous agent might run attacker-influenced instructions inside it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zbqc37syli2o43gavn5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zbqc37syli2o43gavn5.png" alt="Diagram comparing the layers of full virtual machines, microVMs, containers and gVisor, from application to hardware" width="800" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;seccomp (secure computing mode)&lt;/strong&gt; is a Linux kernel facility that lets an operator allowlist or denylist the syscalls a container may invoke, rejecting anything outside that list. It adds no new kernel boundary. The host kernel still handles every allowed syscall directly, which keeps it cheap enough to layer onto almost any container. According to NVIDIA, it is one item in a defense-in-depth stack, suited to trusted internal automation rather than genuinely untrusted, agent-generated code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gvisor.dev/" rel="noopener noreferrer"&gt;gVisor&lt;/a&gt; adds the kernel boundary that seccomp does not. It interposes a user-space component called &lt;strong&gt;the Sentry&lt;/strong&gt; between a container's processes and the real kernel. The Sentry intercepts every system call and handles it through a restricted reimplementation of the kernel surface. That shrinks the hundreds of syscalls a workload can reach down to a minimal, vetted subset. &lt;strong&gt;gVisor's overhead&lt;/strong&gt; is commonly cited as &lt;strong&gt;10 to 30 percent&lt;/strong&gt; on I/O-heavy workloads, with little impact on compute-heavy work such as model inference. According to gVisor's performance guide, though, overhead varies considerably by platform and workload, and small, syscall-heavy operations can exceed that range. That makes gVisor a strong fit for CPU-bound tasks and a weaker one for constant file or network I/O.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://katacontainers.io/" rel="noopener noreferrer"&gt;Kata Containers&lt;/a&gt; provides the most isolation of the four, running a pod inside hardware-assisted virtualisation instead of a shared kernel. A hypervisor such as &lt;a href="https://firecracker-microvm.github.io/" rel="noopener noreferrer"&gt;Firecracker&lt;/a&gt; creates a genuinely separate virtual machine with its own guest kernel, using the processor's virtualisation features rather than the host kernel's namespaces. Firecracker is a purpose-built hypervisor developed at AWS to back Lambda and Fargate, with reported boot times clustering around &lt;strong&gt;100 to 125 milliseconds&lt;/strong&gt; and memory overhead &lt;strong&gt;under 5 megabytes per microVM&lt;/strong&gt;. Firecracker speaks its own API rather than Kubernetes', so Kata Containers closes that gap by implementing the standard &lt;strong&gt;Container Runtime Interface (CRI)&lt;/strong&gt;. A Kata-backed pod then schedules through the same &lt;code&gt;kubectl apply&lt;/code&gt; workflow, with only a &lt;code&gt;runtimeClassName&lt;/code&gt; field marking the difference. Kata supports &lt;a href="https://www.cloudhypervisor.org/" rel="noopener noreferrer"&gt;Cloud Hypervisor&lt;/a&gt; as its default backend, with Firecracker and QEMU as alternatives, and boots in roughly &lt;strong&gt;150 to 300 milliseconds&lt;/strong&gt;. Because the isolation boundary is an entire separate kernel, &lt;strong&gt;an attacker who compromises the workload still has to escape both the guest kernel and the hypervisor to reach the host&lt;/strong&gt;. That closes off namespace abuse and kernel-level exploits regardless of what the agent inside does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1eppb09p2ptvovt2gyqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1eppb09p2ptvovt2gyqx.png" alt="Diagram of selecting a runtime via runtimeClassName — runc, gVisor or Kata Containers — with seccomp layered on top and each path's isolation boundary traced down to the host Kernel" width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of this comes free. Even at 100 to 300 milliseconds, microVM boot time is measurable overhead once multiplied across every agent turn in a busy pipeline. Running Kata in production also means managing nested virtualisation support and a larger per-pod memory footprint than a bare container carries. seccomp and gVisor trade some of that isolation strength back for lower cost and higher throughput, which is exactly why the &lt;code&gt;RuntimeClass&lt;/code&gt; mechanism matters. A single cluster can run a short-lived code interpreter session under gVisor, tolerating little latency budget but running cheaply at high volume, alongside a long-running, browser-equipped agent under Kata or Firecracker, through the same API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker-in-Docker, isolated at the hypervisor level
&lt;/h3&gt;

&lt;p&gt;Letting an agent build or run its own containers has conventionally meant &lt;strong&gt;Docker-in-Docker (DinD)&lt;/strong&gt;, a nested daemon in privileged mode, or the host's Docker socket mounted directly into the container. Both are well-known escape vectors, acceptable for trusted CI/CD pipelines but not for an autonomous agent whose build steps could be influenced by a prompt injected through a compromised dependency.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.docker.com/products/docker-sandboxes/" rel="noopener noreferrer"&gt;Docker Sandboxes&lt;/a&gt; removes both vectors instead of trying to harden them. Each agent runs inside its own rootless microVM with its own private Docker daemon, isolated at the hypervisor level rather than sharing the host's, with full privileges granted only inside that guest. In its "direct" mode, file changes still sync back to the host filesystem, so &lt;strong&gt;any git hooks, install scripts, or task configuration the agent touched need review before a human executes them locally.&lt;/strong&gt; Closing this gap requires review discipline, not additional infrastructure. It means diffing hooks and checking scripts after each session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 3: Threat Modelling for Assumed Compromise
&lt;/h2&gt;

&lt;p&gt;The central threat behind every isolation decision is &lt;strong&gt;indirect prompt injection&lt;/strong&gt;. Direct prompt injection, a user typing malicious instructions into a chat box, is comparatively well understood. Indirect prompt injection hides those instructions inside content the agent is expected to read as normal work. Vectors include a compromised file in a repository, a pull request description, a configuration file such as &lt;code&gt;.cursorrules&lt;/code&gt; or &lt;code&gt;CLAUDE.md&lt;/code&gt;, and a response from an &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; (Model Context Protocol) server. According to NVIDIA's AI Red Team guidance on sandboxing agentic workflows, &lt;strong&gt;an agent cannot reliably tell an operator's instructions apart from text inside a document it was asked to process&lt;/strong&gt;. That means anyone who can put content in front of it gains a channel for influencing its actions.&lt;/p&gt;

&lt;p&gt;Filtering the prompt cannot close this gap. Agentic tools execute arbitrary code by design. Once an action passes into a subprocess, the application has no visibility into or control over what that subprocess does. A compromised agent can also route around an allowlist by calling a restricted tool indirectly through one that is already approved. According to NVIDIA's guidance, &lt;strong&gt;only isolation with a kernel boundary of its own can reliably contain the risk, because containment then does not depend on the model behaving as instructed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NVIDIA's guidance replaces a binary trusted-or-not judgement with four escalating rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-overridable enterprise denylist.&lt;/strong&gt; An absolute floor of blocked operations, such as reads or writes to credential files or hooks, that neither a user nor the agent can approve away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workspace-scoped, allow-by-default access.&lt;/strong&gt; Read and write permission inside the agent's active project directory, granted without approval for every action, since constant friction on routine work undermines the practice of approval altogether.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narrow allowlisted exceptions.&lt;/strong&gt; Specific operations outside the workspace that the agent's job still requires, such as a named SSH key for a legitimate git operation, approved individually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Default-deny.&lt;/strong&gt; Fresh manual approval required for everything else, never cached. A cached "yes" from an earlier legitimate action can be silently reused by an attacker-influenced action that looks similar later in the same session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same principle underlies all four rules. Each applies &lt;strong&gt;least privilege continuously, rather than only once at startup&lt;/strong&gt;. A kernel boundary limits what a compromised process can touch. A network allowlist limits where it can send data. A credential proxy, covered in Chapter 4, limits what it can ever possess. Each is the same idea, enforced at a different layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4h1dvnwht0kwvgpu69tx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4h1dvnwht0kwvgpu69tx.png" alt="Inverted pyramid of four escalating access rules, from a non-overridable denylist to default-deny approval" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 4: Network, Credential, and Identity Boundaries
&lt;/h2&gt;

&lt;p&gt;Chapter 3 closed by naming three layers where least privilege gets enforced: a kernel boundary, a network allowlist, and a credential proxy. This chapter covers the latter two, network and credential boundaries, and adds a third: identity management. Together, egress control, credential handling, and identity management govern what a compromised agent can still do once it is talking to the outside world. That is where most of the actual damage from indirect prompt injection gets carried out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Egress filtering is not content-level safety
&lt;/h3&gt;

&lt;p&gt;Network egress control matters independently of kernel isolation, because even a sandbox that holds does not stop a compromised agent from sending data out through a connection it was always permitted to make. According to NVIDIA's guidance, &lt;strong&gt;egress filtering is a mandatory baseline control, not optional hardening&lt;/strong&gt;. It calls for blocking outbound connections to unknown destinations by default. That boundary should be enforced through enterprise denylists, HTTP proxies, and DNS-level restriction, rather than trusting the agent's own code to self-police.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Domain-level allowlisting is not the same thing as content-level safety.&lt;/strong&gt; Permitting a broad domain such as &lt;code&gt;github.com&lt;/code&gt; allows access to any content hosted there. An attacker who can post data to an already-allowed gist or issue comment therefore has an exfiltration channel a domain filter will never flag.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs1xflwhty58maocnmdo6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs1xflwhty58maocnmdo6.png" alt="Diagram of a network proxy allowing all of github.com, so a legitimate repository request and an attacker's exfiltration path both pass" width="799" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Production implementations span a real spectrum rather than one fixed policy. At one end, a narrow allowlist permits only a model API endpoint and a package registry. At the other, full air-gapping removes all internet access for high-risk batch jobs with no legitimate need to reach it.&lt;/p&gt;

&lt;p&gt;The same logic applies inside an organisation's network, not only at its external edge: a destination being reachable doesn't mean it's safe to trust. &lt;strong&gt;Zero-trust segmentation&lt;/strong&gt; is what enforces that internally. An agent sandbox that can reach a production database or a deployment credential store has simply moved the exfiltration risk internally rather than removed it. In multi-tenant designs, that internal segmentation needs to be mandatory, not optional, on equal footing with external egress control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Credentials that never enter the sandbox
&lt;/h3&gt;

&lt;p&gt;The conventional pattern for handling secrets is to inject an API key into a sandbox as an environment variable through a Kubernetes &lt;code&gt;secretKeyRef&lt;/code&gt;. That pattern has an irreducible weakness. The agent process itself can still read those environment variables at runtime.&lt;/p&gt;

&lt;p&gt;Three failure modes follow from that fact alone, regardless of how well the surrounding sandbox is isolated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent inadvertently includes the secret in a generated response or log.&lt;/li&gt;
&lt;li&gt;A prompt-injected agent deliberately exfiltrates it through an otherwise-legitimate outbound call.&lt;/li&gt;
&lt;li&gt;An adversarial input tricks the agent into printing or transmitting its own environment variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;credential-proxy pattern&lt;/strong&gt;, implemented independently by projects including Infisical's &lt;a href="https://infisical.com/blog/agent-vault-the-open-source-credential-proxy-and-vault-for-agents" rel="noopener noreferrer"&gt;Agent Vault&lt;/a&gt;, inverts this. The sandbox holds no secret at all. It routes outbound HTTPS traffic to a proxy, typically through the standard &lt;code&gt;HTTPS_PROXY&lt;/code&gt; variable. That proxy terminates TLS and strips the placeholder credential the agent's request carried. It then injects the real credential from an encrypted store and re-establishes the connection to the actual upstream service. A &lt;strong&gt;session-level agent token&lt;/strong&gt; issued at proxy handshake ties every request to a specific agent. That is what stops one sandbox from riding another's credentials, and it lets the proxy log, rate-limit, and revoke access mid-session without touching the sandbox itself. Because the sandbox never holds the real secret, none of the three failure modes above can occur. There is nothing for the agent to leak, exfiltrate, or be tricked into revealing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj91j4dnldhydd3gbixc0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj91j4dnldhydd3gbixc0.png" alt="Diagram of the credential-proxy pattern: a sandbox sends a placeholder token, and the proxy swaps in the real credential before forwarding it to the upstream API" width="799" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three approaches implement this pattern today, and they differ in what infrastructure they assume and how mature each one is. A team already running Istio can build it directly on &lt;code&gt;EnvoyFilter&lt;/code&gt; and &lt;code&gt;ext_authz&lt;/code&gt;. That path requires Istio's full service mesh to already be in place, and it inherits the fragility that Istio's documentation attributes to that escape-hatch mechanism. A purpose-built AI gateway requires no service mesh at all. &lt;a href="https://agentgateway.dev/" rel="noopener noreferrer"&gt;agentgateway&lt;/a&gt; and &lt;a href="https://aigateway.envoyproxy.io/" rel="noopener noreferrer"&gt;Envoy AI Gateway&lt;/a&gt; both run as a standalone binary. Infisical's &lt;a href="https://infisical.com/blog/agent-proxy" rel="noopener noreferrer"&gt;Agent Proxy&lt;/a&gt; is the third option, a vendor-managed proxy. Agent Proxy also requires no service mesh, but delegates secret storage to Infisical's own service rather than an operator-controlled vault. All three remain a single point of failure if the proxy itself is compromised.&lt;/p&gt;

&lt;h3&gt;
  
  
  No single mechanism covers identity completely
&lt;/h3&gt;

&lt;p&gt;Three largely separate mechanisms address what identity an agent, or a sub-agent it spawns, actually carries, and none covers the problem completely. Bearer tokens and long-lived API keys assume a human authenticates once and performs a bounded set of actions. Handing an autonomous agent one broad token instead creates &lt;strong&gt;ambient authority&lt;/strong&gt;, letting it exercise everything the token permits at any time, with no link between a specific tool call and a specific authorisation decision.&lt;/p&gt;

&lt;p&gt;The problem compounds with sub-agents. Whichever of three common patterns a team picks carries its own risk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Passing the parent's token down to a sub-agent&lt;/strong&gt; erases the audit trail's ability to distinguish parent from child.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issuing a new static credential per sub-agent&lt;/strong&gt; multiplies the credentials that can leak.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running a sub-agent unauthenticated&lt;/strong&gt; is a compliance failure outright.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://spiffe.io/" rel="noopener noreferrer"&gt;SPIFFE&lt;/a&gt; and its SPIRE implementation issue a short-lived, cryptographically verifiable identity based on runtime attestation rather than a static secret. SPIRE, however, expects every workload variant pre-registered ahead of time and delivers identity through a pull-based call, both of which sit awkwardly against a sandbox whose useful life might be seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud workload identity federation&lt;/strong&gt; authenticates a pod to its cloud provider, but only coarsely. Google Kubernetes Engine (GKE) implements this through &lt;a href="https://docs.cloud.google.com/kubernetes-engine/docs/concepts/workload-identity" rel="noopener noreferrer"&gt;Workload Identity Federation&lt;/a&gt;, which makes every pod sharing one Kubernetes ServiceAccount cryptographically indistinguishable to Google Cloud IAM. Amazon Elastic Kubernetes Service (EKS) offers a choice between two mechanisms, each with a different tradeoff. &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html" rel="noopener noreferrer"&gt;IAM Roles for Service Accounts (IRSA)&lt;/a&gt; refreshes credentials without a pod restart, at the cost of AWS Security Token Service (STS) quota. &lt;a href="https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html" rel="noopener noreferrer"&gt;EKS Pod Identity&lt;/a&gt; avoids that quota cost but &lt;strong&gt;can cache credentials for up to six hours&lt;/strong&gt; before a permission change takes effect. Delegation-aware authorisation systems such as &lt;a href="https://openfga.dev/" rel="noopener noreferrer"&gt;OpenFGA&lt;/a&gt; and &lt;a href="https://authzed.com/spicedb" rel="noopener noreferrer"&gt;SpiceDB&lt;/a&gt; fill the remaining gap, checking each individual tool call against an explicit graph of user and sub-agent permissions rather than authorising a session once at the start.&lt;/p&gt;

&lt;p&gt;At the time of writing, stitching these three layers together remains real engineering work rather than a documented, turnkey integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 5: Scaling and Scheduling Agent Workloads
&lt;/h2&gt;

&lt;p&gt;Chapters 2 through 4 covered how to secure a single sandbox. Running thousands of them at once introduces a different problem. It means avoiding a multi-second wait every time an agent starts a new turn, and stopping one tenant's burst of activity from crowding out every other tenant on the same infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Balancing statefulness and disposability
&lt;/h3&gt;

&lt;p&gt;An agent sandbox has to satisfy two requirements that pull in opposite directions. Many agent workloads are genuinely stateful. A persistent notebook kernel needs to preserve variables across cells, and a coding-agent workspace needs its file changes to survive across many separate tool calls. Both want &lt;strong&gt;a stable hostname and durable storage&lt;/strong&gt;. At the same time, the premise of a sandbox, as distinct from a persistent server, is that it should be &lt;strong&gt;cheap to start per task and safe to tear down immediately afterward&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This differs from the ephemeral containers used in continuous integration and delivery (CI/CD). A CI/CD container starts, runs a predetermined pipeline of steps, and is discarded, with no expectation of resuming mid-pipeline. An agent sandbox typically needs to persist across many discrete tool calls over an extended, possibly paused, conversation. It might sit idle for minutes while a person reviews its output, then resume with the same files, the same running processes, and the same hostname. Chapter 1 covered the interface built for this, the &lt;code&gt;Sandbox&lt;/code&gt; custom resource's declarative API for a single stateful pod with a stable identity. It exists because a hand-rolled StatefulSet, Service, and persistent volume claim had no shared lifecycle controller to reproduce that fidelity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Warm pools and checkpoint-restore
&lt;/h3&gt;

&lt;p&gt;Chapter 1 also introduced &lt;code&gt;SandboxWarmPool&lt;/code&gt;, a pool of pre-booted sandboxes that lets a new session claim one instead of creating it from scratch. This brings allocation down to &lt;strong&gt;the millisecond range&lt;/strong&gt;. A fresh pod with a hardware-isolated kernel boot (Chapter 2) costs seconds instead, an order of magnitude slower.&lt;/p&gt;

&lt;p&gt;GKE adds a further refinement. &lt;a href="https://docs.cloud.google.com/kubernetes-engine/docs/concepts/pod-snapshots" rel="noopener noreferrer"&gt;Pod Snapshots&lt;/a&gt; is a checkpoint-and-restore capability that captures a running pod's full state and &lt;strong&gt;cuts startup time from minutes to seconds&lt;/strong&gt;, including for GPU-backed workloads that are expensive to cold-boot. A companion capability, snapshot-pausing, reclaims an idle sandbox's compute without discarding its state the way a hard termination would.&lt;/p&gt;

&lt;p&gt;EKS has no equivalent yet. AWS does support checkpoint and restore, built on &lt;a href="https://criu.org/" rel="noopener noreferrer"&gt;Checkpoint/Restore In Userspace (CRIU)&lt;/a&gt; through the Kubernetes kubelet checkpoint API. Its purpose is different. It exists for forensic evidence preservation, capturing a suspicious container's state without killing it so an investigator can examine it later. This forensic checkpointing is not a substitute for GKE's cold-start capability.&lt;/p&gt;

&lt;p&gt;One project outside AWS closes part of this gap today. &lt;a href="https://github.com/agent-substrate/substrate" rel="noopener noreferrer"&gt;Agent Substrate&lt;/a&gt;, a joint effort between Solo.io and Google, runs its own cross-cloud snapshot layer, independent of any single vendor's kubelet feature, coordinating full-state snapshots across an agent sandbox's suspend and resume cycles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fquk3x5ounm0fvtsq3v5b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fquk3x5ounm0fvtsq3v5b.png" alt="Bar chart comparing allocation time for cold boot, GKE Pod Snapshots and SandboxWarmPool, from seconds down to milliseconds" width="799" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Quotas, tokens, and rate limits for a sandbox fleet
&lt;/h3&gt;

&lt;p&gt;Kubernetes' standard &lt;code&gt;ResourceQuota&lt;/code&gt; object sets a namespace-level ceiling on aggregate CPU, memory, and object counts, the same primitive any multi-tenant cluster already relies on. &lt;code&gt;LimitRange&lt;/code&gt; complements it by bounding what any single pod or container inside that namespace can request, so one workload cannot claim the entire quota for itself. Neither one governs what a workload does over the network, which the egress allowlists from Chapter 4 control separately. &lt;strong&gt;Many of the threat categories catalogued by &lt;a href="https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/" rel="noopener noreferrer"&gt;OWASP&lt;/a&gt; for agentic systems operate entirely within otherwise-permitted resource and network boundaries&lt;/strong&gt;. That means an attack can satisfy both checks at once and still succeed. A compromised agent exfiltrating data through an already-allowed API call, for instance, breaches no quota at all. &lt;strong&gt;Quota and network policy alone cannot catch that&lt;/strong&gt;, which is why agent workloads need a further, behavioural layer constraining the pattern of an agent's actions rather than only their volume.&lt;/p&gt;

&lt;p&gt;This behavioural layer governs token consumption most directly, typically enforced at the same AI gateway introduced in Chapter 4. Envoy AI Gateway's &lt;code&gt;QuotaPolicy&lt;/code&gt; and agentgateway's token budgets both implement it as a three-level hierarchy. An overall platform budget subdivides into per-tenant budgets, which further subdivide into optional per-agent budgets. The gateway rejects a request once its level's budget runs out.&lt;/p&gt;

&lt;p&gt;Rate limiting for agent fleets operates at two distinct layers, and mixing them up causes real problems. One governs how many outbound calls an agent may make against an external service. The other governs how many sandboxes, and how much aggregate compute, a tenant may have running at once. Confusing them produces either over-restrictive throttling or under-enforcement. Over-restrictive throttling kicks in when compute quota looks tight but the real bottleneck is API calls. Under-enforcement lets a tenant within its compute quota keep overloading an external API for everyone sharing that connection.&lt;/p&gt;

&lt;p&gt;The first layer sits naturally at the network and proxy layer. The same egress point that injects credentials in Chapter 4 typically enforces it too, so a platform already routing traffic through that proxy for credentials gets per-tenant throttling as an adjacent policy. The second layer sits at the scheduler and quota layer described above.&lt;/p&gt;

&lt;p&gt;Agent workloads are bursty. A single user task can fan out into several rapid tool calls, or spawn sub-agents that act concurrently. A per-tenant limit checked independently by each agent cannot see the other agents' concurrent draws against the same shared budget. An external rate-limit service closes this gap. Envoy AI Gateway and agentgateway both call one to share a budget across replicas. The service checks and decrements one shared counter atomically, so concurrent agents see a consistent, current balance rather than a stale per-replica count.&lt;/p&gt;

&lt;h3&gt;
  
  
  What isolation actually costs
&lt;/h3&gt;

&lt;p&gt;Chapter 2 covered isolation's overhead in compute and latency terms. What it costs in dollars is harder to establish. No source reviewed for this article publishes a like-for-like comparison of what a single provider charges for the same workload under &lt;code&gt;runc&lt;/code&gt;, seccomp, gVisor, and Kata Containers. &lt;strong&gt;You cannot currently look up what stronger isolation costs while holding everything else constant. You have to benchmark it yourself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Published pricing does make one part of the spectrum concrete, the deployment model. At a reference scale of 200 concurrently running sandboxes, a managed, shared platform lists monthly pricing around &lt;strong&gt;US\$7,200&lt;/strong&gt;. A comparable footprint under a bring-your-own-cloud model costs about &lt;strong&gt;US\$2,060&lt;/strong&gt; (&lt;strong&gt;more than a threefold difference&lt;/strong&gt;) for broadly similar isolation and capability. Most of that gap is operational and multi-tenancy overhead a fully managed platform absorbs on a customer's behalf, separate from the isolation technology itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 6: Benchmark Evidence and Attack Surfaces Beyond Code
&lt;/h2&gt;

&lt;p&gt;Chapters 2 and 3 argued, from engineering principles, that hardware isolation (gVisor's kernel boundary, Kata's separate guest kernel) is necessary once an agent might execute attacker-influenced code. This chapter tests that argument against evidence, drawing on a 2026 benchmark measuring real escape attempts and an attack surface those technologies were never built to cover.&lt;/p&gt;

&lt;h3&gt;
  
  
  What SandboxEscapeBench found
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/html/2603.02277v1" rel="noopener noreferrer"&gt;SandboxEscapeBench&lt;/a&gt; tests whether a model with root access inside a container can find its way out to the host. It spans 18 scenarios across the orchestration, runtime, and kernel layers, each rated by difficulty. Frontier models reliably solved easy misconfigurations, such as privileged containers and exposed Docker sockets. On medium-difficulty exploits requiring genuine multi-step work, &lt;strong&gt;GPT-5 and Claude Opus 4.5 both scored around 0.40&lt;/strong&gt;. On kernel-level exploits, &lt;strong&gt;every model tested scored zero&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The gap between model tiers was large. Claude Opus scored 0.49 overall against Haiku's 0.18, &lt;strong&gt;roughly threefold&lt;/strong&gt;, and performance scaled with the compute a model was given. &lt;strong&gt;Plain Docker isolation is insufficient by default. Real deployments should be stress-tested rather than assumed safe.&lt;/strong&gt; No novel vulnerabilities were used, only known misconfigurations and disclosed Common Vulnerabilities and Exposures (CVE) entries. &lt;strong&gt;This remains a single study, not yet independently replicated.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These results are worth reading together: escape success scaled with the compute a model was given, and every model scored zero on every kernel-level exploit. Together, they suggest the current zero-success ceiling reflects &lt;strong&gt;where model capability stands today, not a permanent limit&lt;/strong&gt;. If that holds, three directions follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hardware isolation stops being reserved for high-risk workloads and becomes the default for any untrusted one.&lt;/li&gt;
&lt;li&gt;Credential proxying and egress filtering move from a feature teams opt into to one platforms ship by default.&lt;/li&gt;
&lt;li&gt;Escape benchmarking becomes a routine part of due diligence, the way penetration testing did for cloud infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The browser as a different kind of risk
&lt;/h3&gt;

&lt;p&gt;Browsing agents introduce a second execution surface that Chapter 2's isolation technologies do not cover. The risk is semantic rather than technical. &lt;strong&gt;A malicious instruction hidden in a web page's rendered content does not need to escape a sandbox. It manipulates the agent's reasoning directly&lt;/strong&gt;, the same indirect prompt injection risk from Chapter 3, with a page rather than a file as the vector. The stakes are higher because of what a browsing session carries, live authentication cookies and sometimes payment details, which a manipulated agent can leak through actions the browser was always permitted to take.&lt;/p&gt;

&lt;p&gt;Browser isolation complements code-execution sandboxing rather than replacing it, typically through an HTTP-level policy layer sitting above the browser's existing process sandboxing. It remains younger and less standardised than code-execution isolation, with no equivalent yet of Chapter 1's &lt;code&gt;RuntimeClass&lt;/code&gt; for an isolated browser session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 7: Buy Versus Build
&lt;/h2&gt;

&lt;p&gt;Everything from Chapter 1's orchestration primitive to Chapter 6's benchmark evidence describes a stack a team could build. Most teams do not build it themselves. They buy a hosted sandbox instead, from a provider such as &lt;a href="https://e2b.dev/" rel="noopener noreferrer"&gt;E2B&lt;/a&gt;, &lt;a href="https://fly.io/" rel="noopener noreferrer"&gt;Fly.io&lt;/a&gt;, &lt;a href="https://modal.com/" rel="noopener noreferrer"&gt;Modal&lt;/a&gt; or &lt;a href="https://northflank.com/" rel="noopener noreferrer"&gt;Northflank&lt;/a&gt;. Each sells a scheduled, isolated, credential-brokered sandbox. None of them require the buyer to assemble the underlying cluster, isolation configuration, credential proxy and governance layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The isolation layer is not a differentiator among these vendors.&lt;/strong&gt; Nearly every one uses microVM or gVisor isolation, the two strongest techniques Chapter 2 covered. The industry already treats both as sufficient for untrusted, LLM-generated code. Vendors compete instead on &lt;strong&gt;cold-start speed, session persistence, pricing and developer experience&lt;/strong&gt;. That makes the buy decision simple for most teams. Vendors' published isolation stacks appear broadly comparable to what this article has described, at a fraction of the build cost. Building it yourself only becomes worthwhile at organisations already running Kubernetes at scale, or held to compliance requirements that rule out shared infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 8: Conclusion
&lt;/h2&gt;

&lt;p&gt;One fact underlies why agent sandboxes exist at all. A large language model can now execute code it wrote itself, in response to instructions nobody can fully verify. &lt;strong&gt;That same capability gives the agent its value and makes it dangerous.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nothing this article has covered removes that risk. Every layer instead narrows it, each covered in an earlier chapter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the orchestration primitive&lt;/li&gt;
&lt;li&gt;the isolation technique&lt;/li&gt;
&lt;li&gt;the threat model&lt;/li&gt;
&lt;li&gt;network and credential boundaries&lt;/li&gt;
&lt;li&gt;fleet scaling and governance&lt;/li&gt;
&lt;li&gt;the benchmark evidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these ideas were invented specifically for AI agents. Multi-tenant cloud security supplied the isolation techniques, and a decade of browser security research supplied the browser controls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result is defense-in-depth, a posture rather than a solution.&lt;/strong&gt; It treats every layer as something that will eventually fail, and it builds each one so that failure does not cascade into whatever comes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 9: References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Chapter 1&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/kubernetes-sigs/agent-sandbox" rel="noopener noreferrer"&gt;GitHub — kubernetes-sigs/agent-sandbox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://agent-sandbox.sigs.k8s.io/docs/getting_started/overview/" rel="noopener noreferrer"&gt;Agent Sandbox — Overview (sigs.k8s.io)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://northflank.com/blog/agent-sandbox-on-kubernetes" rel="noopener noreferrer"&gt;Northflank — Agent Sandbox on Kubernetes: how it works and how to run it in production&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://northflank.com/blog/sandboxes-on-kubernetes" rel="noopener noreferrer"&gt;Northflank — Sandboxes on Kubernetes: isolation options and how to run them in production&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Chapter 2&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://northflank.com/blog/how-to-sandbox-ai-agents" rel="noopener noreferrer"&gt;Northflank — How to sandbox AI agents in 2026: MicroVMs, gVisor &amp;amp; isolation strategies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://northflank.com/blog/kata-containers-vs-firecracker-vs-gvisor" rel="noopener noreferrer"&gt;Northflank — Kata Containers vs Firecracker vs gVisor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gvisor.dev/docs/architecture_guide/performance/" rel="noopener noreferrer"&gt;gVisor — Performance Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Chapter 4&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/kubernetes-sigs/agent-sandbox/issues/1045" rel="noopener noreferrer"&gt;GitHub Issue — feat: credential vault proxy to prevent AI agents from reading secrets directly (kubernetes-sigs/agent-sandbox #1045)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://infisical.com/blog/agent-vault-the-open-source-credential-proxy-and-vault-for-agents" rel="noopener noreferrer"&gt;Infisical — Agent Vault: The Open Source Credential Proxy and Vault for Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://infisical.com/blog/agent-proxy" rel="noopener noreferrer"&gt;Infisical — Agent Proxy: Secure Credential Brokering for Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Infisical/agent-vault" rel="noopener noreferrer"&gt;GitHub — Infisical/agent-vault&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/blog/untrusted-autonomous-workload-ai-sandboxes/" rel="noopener noreferrer"&gt;Docker Blog — The Untrusted Autonomous Workload and AI Sandboxes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.solo.io/blog/designing-agentgateway-a-unified-high-performance-gateway-for-ai-and-api-traffic" rel="noopener noreferrer"&gt;Solo.io Blog — Designing agentgateway: A Unified High-Performance Gateway for AI and API Traffic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://oneuptime.com/blog/post/2026-02-09-istio-envoyfilter-customization/view" rel="noopener noreferrer"&gt;OneUptime — Istio EnvoyFilter Customisation Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/danielezonca/02c8c139f294663a9060b90ea851371f" rel="noopener noreferrer"&gt;Daniele Zonca (Gist) — Envoy AI Gateway: Composability Feasibility Analysis, Istio Gateway Support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aigateway.envoyproxy.io/blog/v1.0-release-announcement/" rel="noopener noreferrer"&gt;Envoy AI Gateway Blog — Announcing Envoy AI Gateway 1.0: A Stable, Production-Ready AI Gateway&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zylos.ai/research/2026-05-07-ai-agent-multi-tenant-architecture/" rel="noopener noreferrer"&gt;Zylos Research — AI Agent Multi-Tenant Architecture: Isolation, Resource Governance, and Shared Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stacklok.com/blog/agentic-identity-explained-how-to-apply-spiffe-and-relationship-based-authorization-to-ai-agents-in-2026/" rel="noopener noreferrer"&gt;Stacklok Blog — How SPIFFE and Relationship-Based Auth Work for AI Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://riptides.io/blog/how-to-deliver-spiffe-identity-to-ai-agents/" rel="noopener noreferrer"&gt;Riptides Blog — SPIFFE Is What AI Agents Need for Identity, The Question Is How to Deliver It&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.cloud.google.com/kubernetes-engine/docs/concepts/workload-identity" rel="noopener noreferrer"&gt;Google Cloud Docs — About Workload Identity Federation for GKE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.armosec.io/blog/sandboxing-ai-agents-gke-workload-identity/" rel="noopener noreferrer"&gt;ARMO Blog — Securing AI Agents on GKE: Where gVisor, Workload Identity, and VPC Service Controls Stop Working&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hidekazu-konishi.com/entry/amazon_eks_pod_identity_and_irsa_decision_guide.html" rel="noopener noreferrer"&gt;hidekazu-konishi.com — Amazon EKS Pod Identity and IRSA Decision Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Chapter 5&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.redhat.com/articles/2026/07/15/red-hat-build-agent-sandbox-isolated-workload-management-kubernetes" rel="noopener noreferrer"&gt;Red Hat Developer — Red Hat build of Agent Sandbox: Isolated workload management with Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/blog/ja/products/containers-kubernetes/agentic-ai-on-kubernetes-and-gke?hl=ja" rel="noopener noreferrer"&gt;Google Cloud Blog — Agentic AI on Kubernetes and GKE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/containers/forensic-container-checkpointing-on-amazon-eks/" rel="noopener noreferrer"&gt;AWS Blog — Forensic container checkpointing on Amazon Elastic Kubernetes Service (Amazon EKS)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.solo.io/blog/agent-substrate-powers-kubernetes-agents-with-kagent" rel="noopener noreferrer"&gt;Solo.io Blog — Agent Substrate can power agents on Kubernetes with kagent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agent-substrate/substrate" rel="noopener noreferrer"&gt;GitHub — agent-substrate/substrate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/html/2604.28138v1" rel="noopener noreferrer"&gt;arXiv — Crab: A Semantics-Aware Checkpoint/Restore Runtime for Agent Sandboxes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.armosec.io/blog/what-is-ai-agent-sandboxing-kubernetes-native-enforcement-explained/" rel="noopener noreferrer"&gt;ARMO Blog — What Is AI Agent Sandboxing? Kubernetes-Native Enforcement Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/machine-learning/shared-infrastructure-isolated-tenants-pool-model-multi-tenancy-with-amazon-bedrock-agentcore/" rel="noopener noreferrer"&gt;AWS Machine Learning Blog — Shared infrastructure, isolated tenants: Pool model multi-tenancy with Amazon Bedrock AgentCore&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://northflank.com/blog/ai-sandbox-pricing" rel="noopener noreferrer"&gt;Northflank Blog — AI Sandbox pricing comparison (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aigateway.envoyproxy.io/docs/next/capabilities/traffic/quota-policy/" rel="noopener noreferrer"&gt;Envoy AI Gateway Docs — QuotaPolicy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Chapter 6&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/html/2603.02277v1" rel="noopener noreferrer"&gt;arXiv — Quantifying Frontier LLM Capabilities for Container Sandbox Escape (SandboxEscapeBench)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/blog/docker-sandboxes-run-claude-code-and-other-coding-agents-unsupervised-but-safely/" rel="noopener noreferrer"&gt;Docker Blog — Docker Sandboxes: Run Claude Code and More Safely&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blaxel.ai/blog/browser-sandboxing-for-coding-agents" rel="noopener noreferrer"&gt;Blaxel — Browser Sandboxing for Coding Agents: 2026 Security Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Chapter 7&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bertomill.medium.com/e2b-vs-fly-machines-which-sandbox-runtime-is-right-for-your-ai-agents-56684a8931bb" rel="noopener noreferrer"&gt;Bertomill (Medium) — E2B vs Fly Machines: Which Sandbox Runtime Is Right for Your AI Agents?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://northflank.com/blog/best-sandboxes-for-coding-agents" rel="noopener noreferrer"&gt;Northflank — Best sandboxes for coding agents in 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.nvidia.com/blog/practical-security-guidance-for-sandboxing-agentic-workflows-and-managing-execution-risk/" rel="noopener noreferrer"&gt;NVIDIA Technical Blog — Practical Security Guidance for Sandboxing Agentic Workflows and Managing Execution Risk&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Monitoring Mixins, Observability Libraries, and the Signal Pattern: A Practitioner's Guide</title>
      <dc:creator>Krishan Thisera</dc:creator>
      <pubDate>Thu, 13 Aug 2026 07:53:44 +0000</pubDate>
      <link>https://dev.to/krishanthisera/monitoring-mixins-observability-libraries-and-the-signal-pattern-a-practitioners-guide-5505</link>
      <guid>https://dev.to/krishanthisera/monitoring-mixins-observability-libraries-and-the-signal-pattern-a-practitioners-guide-5505</guid>
      <description>&lt;p&gt;A few months ago, I completed a significant revamp of our metrics and monitoring implementation. That work meant taking a hard look at how we package and distribute Prometheus alerts and Grafana dashboards across services, and led me down a rabbit hole most DevOps and SRE folks eventually encounter: &lt;strong&gt;monitoring mixins&lt;/strong&gt;, &lt;strong&gt;observability libraries&lt;/strong&gt;, and Grafana's newer &lt;strong&gt;signal pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article is a write-up of what I found, centred on Grafana's own &lt;a href="https://github.com/grafana/jsonnet-libs" rel="noopener noreferrer"&gt;jsonnet-libs repository&lt;/a&gt;. If you're comfortable with Prometheus and Grafana but haven't yet explored how the ecosystem handles reusable, composable monitoring configuration, this is meant as a practical starting point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 1: The Dashboard That Wouldn't Load
&lt;/h2&gt;

&lt;p&gt;You've done this before. You find a highly-rated Kubernetes dashboard on the Grafana community site, import it into your own Grafana instance, and wait for the panels to populate. Instead, half of them show "No data." The PromQL queries underneath assume a job label or selector that doesn't match how you name things. The dashboard isn't broken. It's written for someone else's Prometheus.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F724jdvpx3uxbu2yzicsm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F724jdvpx3uxbu2yzicsm.png" alt="Partially useful Grafana dashboard panels" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This kind of failure is common enough to be a recognised pattern in the observability community. A dashboard built for one environment carries hidden assumptions about label names, selectors, and job structures that rarely survive contact with a different one.&lt;/p&gt;

&lt;p&gt;The root cause is structural, not accidental. The engineers building a piece of infrastructure software are best positioned to know which metrics matter and how to interpret them. But they are rarely the ones running that software in someone else's production environment. It's like a car built by one team and serviced by another: the people who know the engine best rarely turn the wrench.&lt;/p&gt;

&lt;p&gt;For years, the default response was to route around this gap manually: build dashboards from scratch, or copy one from the internet and hope. Neither scales. What changed the equation was a specific piece of tooling built to close that gap directly, and that's where the story of monitoring mixins begins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 2: How Mixins Fixed Monitoring Distribution
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A monitoring mixin is a Jsonnet program.&lt;/strong&gt; When you evaluate it with your own configuration values, it produces three things at once: a Grafana dashboard, a set of Prometheus alerting rules, and a set of Prometheus recording rules, all built from the same label and selector logic. Think of it as a recipe rather than a finished meal: the mixin ships with instructions and defaults, and you supply the ingredients specific to your kitchen.&lt;/p&gt;

&lt;p&gt;Tom Wilkie and Frederic Branczyk proposed this format in 2018, packaging the idea as a way to distribute Grafana dashboards and Prometheus rules together rather than as separate, disconnected artifacts. The premise, as &lt;a href="https://grafana.com/blog/everything-you-need-to-know-about-monitoring-mixins/" rel="noopener noreferrer"&gt;Grafana Labs later described it on its own blog&lt;/a&gt;, was simple: system authors should not only emit metrics, they should also tell you how to use them. That shifts the burden of building good observability artifacts onto the open-source community, instead of leaving every team to solve the same problem alone.&lt;/p&gt;

&lt;p&gt;What a mixin actually produces follows a &lt;a href="https://github.com/monitoring-mixins/docs" rel="noopener noreferrer"&gt;community-maintained style guide&lt;/a&gt;. Alert names use camelCase with no whitespace (&lt;code&gt;NodeFilesystemAlmostOutOfFiles&lt;/code&gt; is one example), and every alert carries a &lt;code&gt;severity&lt;/code&gt; label: &lt;code&gt;critical&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, or &lt;code&gt;info&lt;/code&gt;. Every alert also carries two mandatory annotations, &lt;code&gt;summary&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;, so the person paged at 2 a.m. knows what fired and why.&lt;/p&gt;

&lt;p&gt;The other core capability is &lt;strong&gt;customisation&lt;/strong&gt;: you inject or override configuration values, such as job selectors and thresholds, at build time, without touching the mixin's source. That's what lets an upstream author ship something useful without hard-coding your specific configurations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But that flexibility has a ceiling.&lt;/strong&gt; A generic dashboard stays generic no matter how many configuration knobs it exposes; deciding which panels matter to your team requires local knowledge no upstream author has. Understanding why requires understanding the language mixins are built on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 3: The Jsonnet Engine Underneath
&lt;/h2&gt;

&lt;p&gt;Every mixin is written in &lt;strong&gt;&lt;a href="https://jsonnet.org/learning/tutorial.html" rel="noopener noreferrer"&gt;Jsonnet&lt;/a&gt;&lt;/strong&gt;, a data-templating language developed at Google. Grafana Labs describes it as a powerful extension of JSON, one that adds variables, functions, conditionals, imports, and error handling on top of the plain JSON data model. Tom Wilkie has pointed out that this lineage isn't accidental: Kubernetes borrowed from Google's internal Borg, Prometheus borrowed from Borgmon, and Jsonnet followed the same pattern for monitoring configuration.&lt;/p&gt;

&lt;p&gt;Before settling on Jsonnet, the mixin's authors ruled out several alternatives. YAML was judged insufficiently dynamic for injecting labels into templates. JSON, a strict subset of YAML, offered even less. String substitution was rejected because it doesn't understand the structure it's editing; a bad substitution can silently produce broken syntax. Templating engines like Jinja only allow structural changes at points the template author explicitly anticipated. And general-purpose languages such as Go or Python were set aside because, in Wilkie's words, writing full programs to produce configuration "feels too much like writing programs, and it's generally a bit of a pain."&lt;/p&gt;

&lt;p&gt;A handful of &lt;a href="https://jsonnet.org/learning/tutorial.html" rel="noopener noreferrer"&gt;Jsonnet features&lt;/a&gt; do the actual structural work. The &lt;code&gt;+&lt;/code&gt; operator merges two objects, and when fields collide, the right-hand side wins. Fields declared with &lt;code&gt;::&lt;/code&gt; are hidden: they exist during evaluation but never appear in the final rendered JSON, which is how internal objects like &lt;code&gt;_config&lt;/code&gt; stay invisible until a build step extracts them. The &lt;code&gt;self&lt;/code&gt; keyword is a late-bound reference to the current object, letting a mixin refer to configuration you haven't supplied yet. &lt;code&gt;super&lt;/code&gt; reaches into a base object during a merge, so you can extend rather than replace inherited behavior.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvg8lx1tpg923t6gv9cl1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvg8lx1tpg923t6gv9cl1.png" alt="Jsonnet templating example" width="800" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Jsonnet isn't the only answer to this problem. &lt;strong&gt;&lt;a href="https://cuelang.org/docs/concept/configuration-use-case/" rel="noopener noreferrer"&gt;CUE&lt;/a&gt;&lt;/strong&gt;, created by former Google engineer Marcel van Lohuizen, takes a validation-first approach instead of a templating-first one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa2hfkk6r7emr5xdnz2qr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa2hfkk6r7emr5xdnz2qr.png" alt="CUE templating example" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll return to that trade-off, and to the teams who've made the switch, in a later chapter. For now, these operators are what turned a single mixin into something composable, which is exactly what observability libraries build on next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 4: From Mixins to Observability Libraries
&lt;/h2&gt;

&lt;p&gt;A plain mixin (see legacy &lt;code&gt;istio-mixin&lt;/code&gt;) has a ceiling: it's a self-contained bundle built for one piece of software. Two independent mixins generally can't merge cleanly into a single dashboard set; you end up reconciling conflicts by hand. &lt;strong&gt;Observability libraries, or observ-libs, exist to remove that ceiling.&lt;/strong&gt; According to &lt;a href="https://github.com/grafana/jsonnet-libs" rel="noopener noreferrer"&gt;Grafana's own &lt;code&gt;jsonnet-libs&lt;/code&gt; repository&lt;/a&gt;, an observ-lib is a flexible format for describing dashboards and alerts in a modular way, built specifically so libraries can import into one another, or into a mixin.&lt;/p&gt;

&lt;p&gt;You'll recognise an observ-lib by its folder name: &lt;code&gt;kafka-observ-lib&lt;/code&gt;, &lt;code&gt;jvm-observ-lib&lt;/code&gt;, &lt;code&gt;windows-observ-lib&lt;/code&gt;, &lt;code&gt;snmp-observ-lib&lt;/code&gt;, &lt;code&gt;process-observ-lib&lt;/code&gt;, &lt;code&gt;golang-observ-lib&lt;/code&gt;. A shared common library applies consistent style across all of them.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://deepwiki.com/grafana/jsonnet-libs" rel="noopener noreferrer"&gt;directory structure&lt;/a&gt; tells you what changed. A legacy plain mixin ships &lt;code&gt;dashboards.libsonnet&lt;/code&gt;, &lt;code&gt;alerts.libsonnet&lt;/code&gt;, and &lt;code&gt;config.libsonnet&lt;/code&gt;. An observ-lib adds three things: a &lt;code&gt;signals/&lt;/code&gt; directory, a &lt;code&gt;panels.libsonnet&lt;/code&gt; file for reusable panel definitions, and a &lt;code&gt;rows.libsonnet&lt;/code&gt; file for reusable dashboard row groupings. It also splits its entry point in two: &lt;code&gt;main.libsonnet&lt;/code&gt; for the full modern feature set, and &lt;code&gt;mixin.libsonnet&lt;/code&gt; retained purely so older tooling built around the plain-mixin standard keeps working.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwao9npn4ywkbwb5r3jcs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwao9npn4ywkbwb5r3jcs.png" alt="Using an observ-lib in mixins" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is composition, not just organisation.&lt;/strong&gt; A team building observability for a Java application can import the JVM library for garbage-collection and heap metrics, then layer application-specific dashboards on top, with no need to reimplement JVM monitoring from scratch. It's the difference between building with prefabricated modules and pouring a new foundation every time.&lt;/p&gt;

&lt;p&gt;The payoff shows up in the dashboards themselves, which tend to follow a consistent shape across the ecosystem: an Overview section for health indicators, a Performance section for throughput and latency, a Resources section for CPU and memory, and a Details section for diagnostics.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;signals/&lt;/code&gt; directory isn't decorative. It houses the ecosystem's most consequential idea yet, and it's where we're headed to see next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 5: Signals: Define Once, Render Everywhere
&lt;/h2&gt;

&lt;p&gt;Here's a failure mode every SRE has lived through: a dashboard panel and its corresponding alert quietly drift apart. Someone edits the panel's query to fix a bug. Nobody updates the alert. Months later, what you see on the dashboard during an incident no longer matches the condition that actually paged you. &lt;strong&gt;The signal pattern exists to make that drift structurally impossible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A signal lets you declare a metric once and render it as a time series panel, a stat panel, a table column, or a Prometheus alert, all from that single declaration. The &lt;a href="https://deepwiki.com/grafana/jsonnet-libs" rel="noopener noreferrer"&gt;core abstraction lives in &lt;code&gt;common-lib/common/signal&lt;/code&gt;&lt;/a&gt;, inside files like &lt;code&gt;base.libsonnet&lt;/code&gt;, &lt;code&gt;signal.libsonnet&lt;/code&gt;, and one file per supported type: &lt;code&gt;counter.libsonnet&lt;/code&gt;, &lt;code&gt;gauge.libsonnet&lt;/code&gt;, &lt;code&gt;histogram.libsonnet&lt;/code&gt;, &lt;code&gt;info.libsonnet&lt;/code&gt;, &lt;code&gt;raw.libsonnet&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffgezelnpwbef0yt3wk5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffgezelnpwbef0yt3wk5.png" alt="Signal core" width="800" height="671"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The type you choose does real work. Each signal type carries an automatic PromQL transformation, so you don't have to remember to apply it by hand:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Transformation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gauge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rendered as-is&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;counter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wrapped in &lt;code&gt;rate(...[interval])&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;histogram&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wrapped in &lt;code&gt;histogram_quantile(q, sum(rate(...[interval])) by (le, ...))&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rendered as static values, not a time series&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;raw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No transformation, used verbatim&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table is arguably the single most consequential design choice in the whole pattern. Graphing a raw counter instead of its rate, or averaging a histogram's &lt;code&gt;_bucket&lt;/code&gt; (i.e., &lt;code&gt;request_duration_seconds_bucket&lt;/code&gt;, &lt;code&gt;http_client_request_body_size_bytes_bucket&lt;/code&gt;) series without &lt;code&gt;histogram_quantile&lt;/code&gt;, is one of the most common Prometheus mistakes there is. Tying the transform to the declared type, instead of to each place the metric gets used, removes that error class entirely.&lt;/p&gt;

&lt;p&gt;Once declared, a signal exposes render functions: &lt;code&gt;asTimeSeries()&lt;/code&gt;, &lt;code&gt;asStat()&lt;/code&gt;, &lt;code&gt;asGauge()&lt;/code&gt;, &lt;code&gt;asTable()&lt;/code&gt; for panels, and &lt;code&gt;asRuleExpression()&lt;/code&gt; for a Prometheus alert. A separate family of modification functions, including &lt;code&gt;withTopK()&lt;/code&gt;, &lt;code&gt;withQuantile()&lt;/code&gt;, &lt;code&gt;withLegendFormat()&lt;/code&gt;, and &lt;code&gt;withInterval()&lt;/code&gt;, lets you reshape the output without redefining the underlying query. You can declare signals in bulk, JSON-style, for simple cases, or through a lower-level imperative API when you need finer control over an individual metric.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fsgbua6qu88xeakfn8t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fsgbua6qu88xeakfn8t.png" alt="Signal declaration" width="800" height="858"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd804sd2fq8oo9300gjrp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd804sd2fq8oo9300gjrp.png" alt="Using signals with alerts" width="800" height="724"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk02b9hpg0g0itkaym0fk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk02b9hpg0g0itkaym0fk.png" alt="Using signals with panels" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Think of a signal as a single master recording that gets remixed for different formats: same source material, different output, guaranteed to stay in sync.&lt;/strong&gt; Because &lt;code&gt;asTimeSeries()&lt;/code&gt; and &lt;code&gt;asRuleExpression()&lt;/code&gt; pull from the same underlying expression, the panel and the alert can never quietly disagree.&lt;/p&gt;

&lt;p&gt;That guarantee comes with a caveat. The signal pattern is explicitly marked &lt;strong&gt;experimental&lt;/strong&gt; by its own maintainers at the time of writing. Its render-function surface is large, with more than a dozen panel functions alone, and the interface isn't stabilised. Adopting it today means accepting some risk of backward-incompatible change in exchange for the correctness it buys you.&lt;/p&gt;

&lt;p&gt;Understanding what a signal is only gets you so far. Next, you'll want to know how to actually run one: which tools you need installed, and what the workflow looks like from a fresh clone to a deployed dashboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 6: Getting Hands-On: The Toolchain and Workflow
&lt;/h2&gt;

&lt;p&gt;Three tools cover most of what you need.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/google/jsonnet" rel="noopener noreferrer"&gt;Jsonnet&lt;/a&gt;&lt;/strong&gt; is the language runtime itself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;jb&lt;/strong&gt; (&lt;a href="https://github.com/jsonnet-bundler/jsonnet-bundler" rel="noopener noreferrer"&gt;jsonnet-bundler&lt;/a&gt;) is the package manager that fetches mixins and libraries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/monitoring-mixins/mixtool" rel="noopener noreferrer"&gt;mixtool&lt;/a&gt;&lt;/strong&gt; validates and scaffolds mixins against the standard schema.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trying an existing mixin takes three commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &amp;lt;mixin-repo-url&amp;gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &amp;lt;mixin-repo&amp;gt;/&amp;lt;mixin-dir&amp;gt;
jb &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Customising a mixin follows a different path. You don't edit the upstream source; you start a fresh project instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jb init
jb &lt;span class="nb"&gt;install &lt;/span&gt;github.com/kubernetes-monitoring/kubernetes-mixin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then write a local &lt;code&gt;mixin.libsonnet&lt;/code&gt; that imports the upstream mixin and overrides its &lt;code&gt;_config&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsonnet"&gt;&lt;code&gt;&lt;span class="k"&gt;local&lt;/span&gt; &lt;span class="nx"&gt;kubernetes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"kubernetes-mixin/mixin.libsonnet"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;kubernetes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;_config&lt;/span&gt;&lt;span class="p"&gt;+::&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;kubeStateMetricsSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'job="kube-state-metrics"'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use the &lt;code&gt;jsonnet&lt;/code&gt; CLI, or the &lt;a href="https://marketplace.visualstudio.com/items?itemName=Grafana.vscode-jsonnet" rel="noopener noreferrer"&gt;Jsonnet Language Server extension for VS Code&lt;/a&gt; for inline evaluation, linting, and navigation.&lt;/p&gt;

&lt;p&gt;From here, deployment branches based on your preference, for instance, through your own CI/CD, bundled with &lt;a href="https://github.com/prometheus-operator/kube-prometheus" rel="noopener noreferrer"&gt;kube-prometheus&lt;/a&gt; for a full Kubernetes-native stack, or through &lt;a href="https://tanka.dev/" rel="noopener noreferrer"&gt;Tanka&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is the entire mixin/observ-lib/signal pattern in working form. It's also not the only way to solve this problem, and the next chapter looks squarely at the alternatives.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 7: The Other Side of the Fence: Alternatives Worth Knowing
&lt;/h2&gt;

&lt;p&gt;The strongest critique of this entire ecosystem comes from inside Grafana Labs itself. &lt;a href="https://grafana.com/blog/a-complete-guide-to-managing-grafana-as-code-tools-tips-and-tricks/" rel="noopener noreferrer"&gt;Grafana's own documentation&lt;/a&gt; states plainly that &lt;a href="https://github.com/grafana/grafonnet" rel="noopener noreferrer"&gt;Grafonnet&lt;/a&gt;, while useful for generating dashboard JSON, "requires knowing Jsonnet, so that can be unappealing to some users." That's not outside commentary. &lt;strong&gt;It's the tool's primary vendor naming its own barrier to adoption.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/dashboard.html" rel="noopener noreferrer"&gt;Grafana Terraform provider&lt;/a&gt; is the most direct answer to that barrier. Its &lt;code&gt;grafana_dashboard&lt;/code&gt; resource holds the dashboard model in a &lt;code&gt;config_json&lt;/code&gt; field, populated inline with &lt;code&gt;jsonencode()&lt;/code&gt;, loaded from a JSON file, or templated with Terraform's &lt;code&gt;templatefile()&lt;/code&gt; function. &lt;a href="https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/" rel="noopener noreferrer"&gt;Grafana documentation&lt;/a&gt; lists three more non-Jsonnet paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/ansible/" rel="noopener noreferrer"&gt;Grafana Ansible collection&lt;/a&gt;&lt;/strong&gt;, for teams already standardised on Ansible playbooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://grafana.com/docs/grafana/latest/as-code/infrastructure-as-code/grafana-operator/" rel="noopener noreferrer"&gt;Grafana Operator&lt;/a&gt;&lt;/strong&gt;, a Kubernetes-native controller that fits naturally into ArgoCD-style GitOps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/grafana/crossplane-provider-grafana" rel="noopener noreferrer"&gt;Grafana Crossplane provider&lt;/a&gt;&lt;/strong&gt; (experimental), exposing the same resource surface as native Kubernetes manifests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule management has a parallel alternative, distinct from dashboards. A large share of the Kubernetes-native monitoring community manages Prometheus alerting and recording rules through the Prometheus Operator's &lt;code&gt;PrometheusRule&lt;/code&gt; custom resource: native Kubernetes manifests reconciled by a controller, rather than YAML generated ahead of time from Jsonnet. The two approaches are often complementary rather than competing. Many teams use a mixin, or kube-prometheus (which bundles several mixins together), specifically to generate the &lt;code&gt;PrometheusRule&lt;/code&gt; manifests that the Operator then deploys: Jsonnet handles authoring, and Kubernetes-native reconciliation handles runtime.&lt;/p&gt;

&lt;p&gt;Terraform and the Operator both change how configuration gets deployed. &lt;a href="https://cuelang.org/docs/concept/configuration-use-case/" rel="noopener noreferrer"&gt;CUE&lt;/a&gt; makes a different argument entirely. CUE is &lt;strong&gt;validation-first&lt;/strong&gt; where Jsonnet is &lt;strong&gt;templating-first&lt;/strong&gt;: its merge operation is associative, commutative, and idempotent, a guarantee that the same configuration fragments always produce the same result regardless of combination order. Jsonnet's simpler right-hand-side-wins merge doesn't make that promise. &lt;a href="https://engineering.mercari.com/en/blog/entry/20220122-adventures-of-using-cue-at-scale/" rel="noopener noreferrer"&gt;Mercari's observability platform team&lt;/a&gt; adopted CUE for parts of their workflow specifically to cut down on the number of configuration languages engineers needed to know.&lt;/p&gt;

&lt;p&gt;None of this makes Jsonnet's approach wrong. It has the deepest community track record here: the mixin idea dates to 2018 and has years of production use. Nothing else discussed offers an equivalent to the signal pattern's single-definition, multi-render guarantee. &lt;strong&gt;The honest answer is contextual:&lt;/strong&gt; teams mainly consuming community mixins get the most value staying in Jsonnet, since that's where the maintained content lives.&lt;/p&gt;

&lt;p&gt;That answer holds for the tools available right now. Where Grafana Labs is taking the platform next is a separate question, and it's the one Chapter 8 takes on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 8: Where It's Heading, And Why It's Worth Exploring
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://grafana.com/blog/observability-as-code-grafana-12/" rel="noopener noreferrer"&gt;Grafana Labs' own roadmap&lt;/a&gt; tells you where this is going. Grafana 12 introduced the Foundation SDK, entering public preview with libraries for defining Grafana resources programmatically in Go, Java, PHP, Python, and TypeScript: mainstream languages, not Jsonnet. Alongside it came the App Platform APIs and Dashboard schema v2, both experimental, and &lt;code&gt;grafanactl&lt;/code&gt; for validating and pushing resources. By 2026, the &lt;a href="https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/dashboard.html" rel="noopener noreferrer"&gt;Grafana Terraform provider&lt;/a&gt; had already split its documentation between a legacy HTTP API for Grafana 12 and earlier, and a newer Kubernetes-style API for Grafana 13 and beyond. The resource model itself is mid-migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't read this as Jsonnet losing.&lt;/strong&gt; The Foundation SDK solves a different problem than mixins do. It answers how do I define a Grafana resource in a language my team already knows. Mixins and observ-libs answer how do I package and distribute best-practice monitoring so other organisations can use and improve it. &lt;a href="https://grafana.github.io/grafonnet/index.html" rel="noopener noreferrer"&gt;Grafonnet&lt;/a&gt; sits at the bridge between the two: it's a Jsonnet library, but it's generated straight from the Foundation SDK's OpenAPI specs. &lt;strong&gt;These are complementary tracks, not a fork in the road.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What stays constant is the idea, not the tooling: define your signal once, render it everywhere, and let configuration, not a fork, absorb the differences between environments.&lt;/strong&gt; That's the part worth carrying forward regardless of which SDK wins.&lt;/p&gt;

&lt;p&gt;If you're running Prometheus and Grafana already, you have a fast way to test this yourself. Clone the &lt;a href="https://github.com/grafana/jsonnet-libs" rel="noopener noreferrer"&gt;upstream mixin&lt;/a&gt;, run &lt;code&gt;jb install&lt;/code&gt;, and generate its dashboards against your own cluster. You'll see the pattern this whole article has been describing, in about ten minutes.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cuelang.org/docs/concept/configuration-use-case/" rel="noopener noreferrer"&gt;CUE Configuration Use Case&lt;/a&gt; - cuelang.org&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://grafana.com/blog/everything-you-need-to-know-about-monitoring-mixins/" rel="noopener noreferrer"&gt;Everything You Need to Know About Monitoring Mixins&lt;/a&gt; - Grafana Blog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://povilasv.me/getting-started-with-monitoring-mixins/" rel="noopener noreferrer"&gt;Getting Started with Monitoring Mixins&lt;/a&gt; - povilasv.me&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://grafana.com/blog/a-complete-guide-to-managing-grafana-as-code-tools-tips-and-tricks/" rel="noopener noreferrer"&gt;Grafana as Code: A Complete Guide to Tools, Tips, and Tricks&lt;/a&gt; - Grafana Blog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/grafana/jsonnet-libs" rel="noopener noreferrer"&gt;Grafana Labs' Jsonnet Libraries&lt;/a&gt; - GitHub&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/dashboard.html" rel="noopener noreferrer"&gt;Grafana Terraform Provider - grafana_dashboard Resource&lt;/a&gt; - Terraform Registry&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://deepwiki.com/grafana/jsonnet-libs" rel="noopener noreferrer"&gt;grafana/jsonnet-libs Overview&lt;/a&gt; - DeepWiki&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://grafana.github.io/grafonnet/index.html" rel="noopener noreferrer"&gt;Grafonnet&lt;/a&gt; - GitHub Pages&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://oneuptime.com/blog/post/2026-01-20-grafana-dashboards-as-code/view" rel="noopener noreferrer"&gt;How to Provision Dashboards as Code in Grafana&lt;/a&gt; - OneUptime&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://jsonnet.org/learning/tutorial.html" rel="noopener noreferrer"&gt;Jsonnet Tutorial&lt;/a&gt; - jsonnet.org&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/monitoring-mixins/docs" rel="noopener noreferrer"&gt;Monitoring Mixins Documentation&lt;/a&gt; - GitHub&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://grafana.com/blog/observability-as-code-grafana-12/" rel="noopener noreferrer"&gt;Observability as Code in Grafana 12&lt;/a&gt; - Grafana Blog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://engineering.mercari.com/en/blog/entry/20220122-adventures-of-using-cue-at-scale/" rel="noopener noreferrer"&gt;Observability-kit: Adventures of Using CUE at Scale&lt;/a&gt; - Mercari Engineering&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://grafana.com/docs/grafana-cloud/developer-resources/infrastructure-as-code/" rel="noopener noreferrer"&gt;Provision Grafana Cloud with Infrastructure as Code&lt;/a&gt; - Grafana Documentation&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>kubernetes</category>
      <category>grafana</category>
      <category>observability</category>
    </item>
    <item>
      <title>How We Solved ArgoCD Notifications' Single-Secret Limitation with Kyverno</title>
      <dc:creator>Krishan Thisera</dc:creator>
      <pubDate>Sat, 04 Jul 2026 06:50:00 +0000</pubDate>
      <link>https://dev.to/krishanthisera/-how-we-solved-argocd-notifications-single-secret-limitation-with-kyverno-3ak2</link>
      <guid>https://dev.to/krishanthisera/-how-we-solved-argocd-notifications-single-secret-limitation-with-kyverno-3ak2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I wrote this post a while back, before Kyverno introduced its new Mutating Policies (released in &lt;a href="https://release-1-17-0.kyverno.io/blog/2025/07/31/announcing-kyverno-release-1.15/" rel="noopener noreferrer"&gt;Kyverno 1.15&lt;/a&gt;). The Kyverno manifests here use the older approach, but the core concept remains the same.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you run ArgoCD Notifications with multiple integrations (for example, GitLab + Grafana), you will eventually hit the single-secret constraint. This post walks through the options I evaluated, why most of them were not so elegant or risky, and the Kyverno policy pattern that worked cleanly in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;ArgoCD Notifications is hardcoded to read from a single secret. When you need credentials for multiple services — each managed independently — that becomes a problem fast.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The constraint:&lt;/strong&gt; ArgoCD Notifications only reads from &lt;code&gt;argocd-notifications-secret&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The complication:&lt;/strong&gt; Two Vault Secret Operator (VSO) resources (&lt;em&gt;static or dynamic&lt;/em&gt;) cannot safely co-manage the same Kubernetes Secret object — they’ll fight and flap&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The fix:&lt;/strong&gt; Use a Kyverno &lt;code&gt;mutate&lt;/code&gt; policy to merge specific keys from individual source secrets into one target secret&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Bit of Context
&lt;/h2&gt;

&lt;p&gt;We run a GitOps workflow using ArgoCD. App code lives in one repo, rendered manifests are committed to a deploy repo by GitLab CI, and ArgoCD reconciles those manifests across clusters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffwi1uukbku6naa8egeyt.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffwi1uukbku6naa8egeyt.webp" alt="High-level GitOps flow: GitLab CI renders manifests to deploy repo and ArgoCD syncs clusters" width="799" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After each deployment, &lt;strong&gt;ArgoCD Notifications&lt;/strong&gt; sends a webhook back to the GitLab pipeline with the sync status. This webhook setup tells us whether the deployment actually landed successfully.&lt;/p&gt;

&lt;p&gt;For secret management, we use &lt;a href="https://github.com/hashicorp/vault-secrets-operator" rel="noopener noreferrer"&gt;&lt;strong&gt;Vault Secret Operator (VSO)&lt;/strong&gt;&lt;/a&gt;. It talks to &lt;a href="https://www.hashicorp.com/en/products/vault" rel="noopener noreferrer"&gt;HashiCorp Vault&lt;/a&gt; and provisions secrets directly into the cluster.&lt;/p&gt;

&lt;p&gt;Below is a very high-level overview of VSO for the context. The internals of VSO are outside the scope of this post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgblf34v4mbetcacmpk9z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgblf34v4mbetcacmpk9z.webp" alt="High-level Vault Secret Operator flow showing secrets synced from Vault into Kubernetes" width="694" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;This setup is very common in the industry, and ArgoCD Notifications is a great way to send deployment updates to external systems. It supports Slack, Microsoft Teams, Grafana, custom webhooks (which is how GitLab is integrated in this case), etc.&lt;/p&gt;

&lt;p&gt;In our case, we needed two notifications firing after every deployment: a GitLab webhook and a Grafana annotation on our dashboards.&lt;/p&gt;

&lt;p&gt;See the Docs: &lt;a href="https://argo-cd.readthedocs.io/en/latest/operator-manual/notifications/services/grafana/" rel="noopener noreferrer"&gt;ArgoCD Grafana notification docs&lt;/a&gt; for details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6e8bfc0gar9tukidkh34.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6e8bfc0gar9tukidkh34.webp" alt="ArgoCD Notifications Grafana service configuration overview" width="799" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Grafana notification support is already built into ArgoCD — it just needs a Grafana API token and a few configuration changes on the ArgoCD application side (See &lt;a href="https://argo-cd.readthedocs.io/en/latest/operator-manual/notifications/services/grafana/" rel="noopener noreferrer"&gt;ArgoCD Grafana notification docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;However, there is one catch: all service credentials for the ArgoCD notification controller must live in a single Kubernetes Secret named &lt;code&gt;argocd-notifications-secret&lt;/code&gt;. That is the only place the ArgoCD Notifications controller will look.&lt;/p&gt;

&lt;p&gt;If you are to notify multiple services, all credentials need to be consolidated into that one secret.&lt;/p&gt;

&lt;p&gt;At this point, the GitLab token was already being managed by VSO. Now we needed the Grafana token to live alongside it.&lt;/p&gt;

&lt;p&gt;So it would look like this,&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fue5l60eas4w4y08qrz0k.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fue5l60eas4w4y08qrz0k.webp" alt="GitLab and Grafana tokens needing to be combined into one ArgoCD notifications secret" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At least at the time of writing, there was no way to point ArgoCD Notifications to multiple secrets (for example, one for GitLab and one for Grafana), or to another secret name.&lt;/p&gt;

&lt;p&gt;So it was one secret, both the tokens had to end up in the same secret, but still be managed separately.&lt;/p&gt;

&lt;p&gt;Co-locating unrelated credentials in one place raises both operational and security concerns, worth avoiding.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpwf7fmb7zfu4gns7qxn0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpwf7fmb7zfu4gns7qxn0.webp" alt="Separate service tokens versus ArgoCD single-secret requirement" width="800" height="759"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At that point, a few options were left.&lt;/p&gt;

&lt;h2&gt;
  
  
  Options Considered
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Option 1 — A separate secret for the Grafana token&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We already established this that (at the time of writing) this was not possible. ArgoCD Notifications was hardcoded to use &lt;code&gt;argocd-notifications-secret&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2a — Combine both tokens into one VSO secret&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Technically possible. Store both tokens in one Vault path (a single location in Vault that contains both keys), then let VSO create &lt;strong&gt;one&lt;/strong&gt; Kubernetes Secret from it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvxokds1aoj0wjvz9g3h2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvxokds1aoj0wjvz9g3h2.webp" alt="HCP Vault ArgoCD shared secrets" width="800" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But that means co-locating unrelated credentials in the same Vault path — not ideal from a security standpoint, and it creates an operational headache too. Especially once you factor in dynamic secrets, where each service should own and rotate its own credentials independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2b — Use two VSO resources to manage the same secret&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This sounds cleaner: one VSO resource per token, both targeting &lt;code&gt;argocd-notifications-secret&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The issue is that VSO manages the &lt;strong&gt;entire&lt;/strong&gt; Secret resource, not individual fields. If two VSO resources claim the same Secret, they continuously reconcile against each other and overwrite each other’s updates. The result is a flapping Secret that does not stabilise.&lt;/p&gt;

&lt;p&gt;Here is a sample secret managed by VSO. Check &lt;code&gt;managedFields&lt;/code&gt; to see ownership.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqaiiukqxpw21lqchp3a.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqaiiukqxpw21lqchp3a.webp" alt="VSO-managed Secret showing managedFields ownership" width="800" height="1398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3 — Use an Istio EnvoyFilter to inject the token&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since we use Istio service mesh, an &lt;a href="https://istio.io/latest/docs/reference/config/networking/envoy-filter/" rel="noopener noreferrer"&gt;EnvoyFilter&lt;/a&gt; could intercept requests to Grafana and inject the API token as an HTTP header, without ArgoCD knowing about it.&lt;/p&gt;

&lt;p&gt;A clever workaround, but it comes with trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Wrong abstraction layer:&lt;/strong&gt; token management becomes network behaviour.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Coupling risk:&lt;/strong&gt; upgrades in Istio/Envoy can break filter behaviour.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Testing complexity:&lt;/strong&gt; harder to validate compared to policy/controller logic.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Scope risk:&lt;/strong&gt; Host-based matches can affect unintended traffic after routing changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option 4 — Write a custom controller or use a templating controller&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We already had internal controller tooling, so this was possible. We also considered &lt;a href="https://github.com/kluctl/kluctl" rel="noopener noreferrer"&gt;&lt;code&gt;kluctl&lt;/code&gt;&lt;/a&gt; as a templating-controller path.&lt;/p&gt;

&lt;p&gt;Both are valid, but they add operational overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 5 — Use Kyverno&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kyverno.io/" rel="noopener noreferrer"&gt;Kyverno&lt;/a&gt; was already on our DevOps roadmap, just not yet adopted.&lt;/p&gt;

&lt;p&gt;Kyverno is a Kubernetes-native policy engine that can &lt;strong&gt;mutate&lt;/strong&gt; resources. Instead of writing a custom controller, we could use policy-driven mutation to handle secret composition.&lt;/p&gt;

&lt;p&gt;So, I decided to go ahead with Kyverno.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;The plan was straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; VSO creates &lt;strong&gt;two source secrets&lt;/strong&gt; (one per token):

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;argocd-notifications-secret-gitlab-merge&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;argocd-notifications-secret-grafana-merge&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; A &lt;strong&gt;Kyverno mutate policy&lt;/strong&gt; watches those source secrets and patches &lt;code&gt;argocd-notifications-secret&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzmhr45qtoh3kp6hxn6xb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzmhr45qtoh3kp6hxn6xb.webp" alt="VSO source secrets trigger Kyverno mutate policy to patch argocd-notifications-secret" width="800" height="759"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One important detail:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kyverno cannot watch a Secret and generate another Secret from it — it cannot generate a resource of the same kind as the trigger. What it &lt;strong&gt;can&lt;/strong&gt; do is mutate a resource of the same kind.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the approach is: use a &lt;code&gt;mutate&lt;/code&gt; rule that is triggered by the source secrets and patches &lt;code&gt;argocd-notifications-secret&lt;/code&gt; directly. So this means &lt;code&gt;argocd-notifications-secret&lt;/code&gt; needs to be provisioned first.&lt;/p&gt;

&lt;p&gt;The easiest way is to use the &lt;a href="https://github.com/argoproj/argo-helm/blob/ac92cdc8bb19b885116013f3214792bc949670df/charts/argo-cd/values.yaml#L3989" rel="noopener noreferrer"&gt;ArgoCD chart’s built-in secret provisioning&lt;/a&gt; to create an empty placeholder Secret (resource shell only). Kyverno then updates its data fields when source secrets change.&lt;/p&gt;

&lt;p&gt;As we do not want Kyverno mutating a resource that does not exist yet, we can use ArgoCD sync-waves to ensure the empty &lt;code&gt;argocd-notifications-secret&lt;/code&gt; is created first (by setting its sync-wave to a value lower than the VSO source secrets).&lt;/p&gt;

&lt;p&gt;This is the Kyverno policy that ties it all together. It watches both source secrets (Kubernetes) and patches their keys into &lt;code&gt;argocd-notifications-secret&lt;/code&gt; whenever they change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kyverno.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Policy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;policies.kyverno.io/subject&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secrets&lt;/span&gt;
    &lt;span class="na"&gt;policies.kyverno.io/title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd-notifications-secret&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd-notifications-secret&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mutate-secret-on-gitlab-secret-update&lt;/span&gt;
    &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;any&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;kinds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
          &lt;span class="na"&gt;names&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;argocd-notifications-secret-gitlab-merge&lt;/span&gt;
          &lt;span class="na"&gt;operations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CREATE&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;UPDATE&lt;/span&gt;
    &lt;span class="na"&gt;mutate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;mutateExistingOnPolicyUpdate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
        &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd-notifications-secret&lt;/span&gt;
      &lt;span class="na"&gt;patchStrategicMerge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;gitlab-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;request.object.data."gitlab-token"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mutate-secret-on-grafana-secret-update&lt;/span&gt;
    &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;any&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;kinds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
          &lt;span class="na"&gt;names&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;argocd-notifications-secret-grafana-merge&lt;/span&gt;
          &lt;span class="na"&gt;operations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CREATE&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;UPDATE&lt;/span&gt;
    &lt;span class="na"&gt;mutate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;mutateExistingOnPolicyUpdate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
        &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd-notifications-secret&lt;/span&gt;
      &lt;span class="na"&gt;patchStrategicMerge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;grafana-api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;request.object.data."grafana-api-key"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few useful notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Two rules, one policy:&lt;/strong&gt; each rule updates a specific field from a specific source secret.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;mutateExistingOnPolicyUpdate: true&lt;/code&gt;:&lt;/strong&gt; reconciles immediately on policy apply/update.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;patchStrategicMerge&lt;/code&gt;:&lt;/strong&gt; merges specific keys instead of replacing the whole &lt;code&gt;data&lt;/code&gt; map (&lt;a href="https://release-1-13-0.kyverno.io/docs/writing-policies/mutate/#strategic-merge-patch" rel="noopener noreferrer"&gt;Kyverno strategic merge patch docs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;match&lt;/code&gt;:&lt;/strong&gt; Kyverno watches for &lt;code&gt;CREATE&lt;/code&gt; and &lt;code&gt;UPDATE&lt;/code&gt; events on the source secrets (&lt;code&gt;argocd-notifications-secret-gitlab-merge&lt;/code&gt; and &lt;code&gt;argocd-notifications-secret-grafana-merge&lt;/code&gt;). Any change to either of those triggers the corresponding rule.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;targets&lt;/code&gt;:&lt;/strong&gt; this tells Kyverno which resource to patch — in this case &lt;code&gt;argocd-notifications-secret&lt;/code&gt;, the empty shell provisioned by the ArgoCD Helm chart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that is really it. VSO manages each source secret independently, Kyverno watches for changes and patches the right keys into &lt;code&gt;argocd-notifications-secret&lt;/code&gt;, and ArgoCD Notifications picks it up without knowing any of this is happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Kyverno Tracks Ownership — Managed Fields
&lt;/h3&gt;

&lt;p&gt;One thing worth knowing is how Kubernetes handles field ownership here. Because multiple controllers are touching the same Secret, Kubernetes managed fields track which controller owns which keys — so nothing gets blindly overwritten.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fry4crdud4blvlb82mwah.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fry4crdud4blvlb82mwah.webp" alt="Kubernetes managedFields view showing field ownership between ArgoCD chart and Kyverno" width="800" height="890"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  ArgoCD chart owns the shell (empty secret resource)&lt;/li&gt;
&lt;li&gt;  Kyverno background controller owns the token fields that it writes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can verify this directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get secrets argocd-notifications-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--show-managed-fields&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; argocd &lt;span class="nt"&gt;-o&lt;/span&gt; yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output includes a &lt;code&gt;managedFields&lt;/code&gt; section showing which manager owns which keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to Keep in Mind When Running Kyverno in Production
&lt;/h2&gt;

&lt;p&gt;These are some of the points that I make a note of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Install all CRDs.&lt;/strong&gt; Some Kyverno controllers can error if required CRDs are missing, even when those CRDs are not actively used. I observed this in the version we ran at that time, so check your version and release notes.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Give Kyverno enough resources.&lt;/strong&gt; If the admission controller capacity is insufficient, resource creation can fail cluster-wide.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Test policies early.&lt;/strong&gt; Two solid options:
– &lt;strong&gt;Kyverno CLI tests&lt;/strong&gt; (fast, no cluster required): &lt;a href="https://kyverno.io/docs/kyverno-cli/usage/test/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;
– &lt;strong&gt;Chainsaw tests&lt;/strong&gt; (integration style, with real cluster or mock API server): &lt;a href="https://github.com/kyverno/chainsaw" rel="noopener noreferrer"&gt;repo&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My preference is Chainsaw for realism, while CLI tests are great for quick validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;What started as a “just add one more token” request ended up being a neat Kyverno adoption point for us. Sometimes a constraint in one tool opens the door to doing something smarter in another.&lt;/p&gt;

&lt;p&gt;If you are running ArgoCD with multiple notification integrations and have hit this exact wall, hopefully, this gives you a concrete path forward.&lt;/p&gt;

&lt;p&gt;And if you have solved it differently — especially with the newer Kyverno mutation features — I would genuinely love to hear how you approached it.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>gitops</category>
      <category>argocd</category>
    </item>
    <item>
      <title>GitOps for Devs - Part 03: CI/CD</title>
      <dc:creator>Krishan Thisera</dc:creator>
      <pubDate>Tue, 09 Jan 2024 04:24:12 +0000</pubDate>
      <link>https://dev.to/krishanthisera/gitops-for-devs-part-03-cicd-hhe</link>
      <guid>https://dev.to/krishanthisera/gitops-for-devs-part-03-cicd-hhe</guid>
      <description>&lt;p&gt;In the previous article, we discussed the codebase of the Album-App. This article will discuss how we can implement CI/CD for the Album-App.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq52dynvfyni3oxxex4mv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq52dynvfyni3oxxex4mv.png" alt="CI/CD Workflow" width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD Workflow
&lt;/h2&gt;

&lt;p&gt;There are many ways to implement CI/CD workflow. But in summary,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Testing and Readiness Check&lt;/strong&gt;: Once the changes to the application code are thoroughly tested and verified for release, the process can proceed to the release phase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triggering the Release Pipeline&lt;/strong&gt;: This phase can be initiated manually or through an automated process, depending on your workflow and requirements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building and Pushing Docker Images&lt;/strong&gt;: The release process involves building the application code and pushing the resulting images to the designated Docker registry or container repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updating Helm Chart Values&lt;/strong&gt;: Post-image creation, the Helm chart values in the &lt;strong&gt;configuration repo&lt;/strong&gt; need an update with the new Docker image tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ArgoCD Trigger and Sync Process&lt;/strong&gt;: ArgoCD, being a continuous deployment tool,  detects changes within the configuration repository. ArgoCD compares the desired state (defined in the Git repository) with the actual state of the cluster. Any disparities trigger the synchronization process to ensure alignment between the desired and actual state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync and Deployment&lt;/strong&gt;: Upon detecting changes, ArgoCD starts the sync process, pulling the updated configurations from the Git repository. ArgoCD applies these changes to the Kubernetes cluster, managing deployments, updates, or rollbacks as necessary to align the cluster with the desired state defined in the Helm charts.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;album-app&lt;/code&gt; 's release pipeline leverages GitHub actions. &lt;em&gt;The pipeline is implemented in the application repository.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Publish Docker Images&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;created&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;REGISTRY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&lt;/span&gt;
  &lt;span class="na"&gt;CONFIG_REPO&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.repository }}-config&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-and-publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check tag name&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check_tag&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "::set-output name=tag_name::${GITHUB_REF#refs/tags/}"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check app name&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check_app&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;app_name=$(echo ${{ steps.check_tag.outputs.tag_name }} | awk -F'-' '{print $1}')&lt;/span&gt;
          &lt;span class="s"&gt;echo "::set-output name=app_name::$app_name"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check Docker Image Version&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check_version&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;version=$(echo ${{ steps.check_tag.outputs.tag_name }} | awk -F'-' '{print $2}')&lt;/span&gt;
          &lt;span class="s"&gt;echo "::set-output name=version::$version"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to GitHub Packages&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.REGISTRY }}&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GHCR_TOKEN }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push frontend or backend image&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker_build&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/build-push-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.check_app.outputs.app_name }}&lt;/span&gt;
          &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/${{ github.repository }}-${{ steps.check_app.outputs.app_name }}:${{ steps.check_version.outputs.version }}&lt;/span&gt;
          &lt;span class="na"&gt;build-args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;VERSION=${{ steps.check_version.outputs.version }}&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.check_version.outputs.version }}&lt;/span&gt;
      &lt;span class="na"&gt;app_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.check_app.outputs.app_name }}&lt;/span&gt;

  &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-and-publish&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="c1"&gt;# Checkout album-app-config repository&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout album-app-config&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.CONFIG_REPO }}&lt;/span&gt; &lt;span class="c1"&gt;# Replace with the URL or name of the album-app-config repository&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CONFIG_REPO_TOKEN }}&lt;/span&gt;

        &lt;span class="c1"&gt;# Step to update album-app-config repository&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update album-app-config repository&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CONFIG_REPO_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Get the release tag and app name&lt;/span&gt;
          &lt;span class="s"&gt;version='${{ needs.build-and-publish.outputs.version }}'&lt;/span&gt;
          &lt;span class="s"&gt;app_name='${{ needs.build-and-publish.outputs.app_name }}'&lt;/span&gt;

          &lt;span class="s"&gt;# Set the release branch name&lt;/span&gt;
          &lt;span class="s"&gt;release_branch=release/${app_name}_${version}&lt;/span&gt;

          &lt;span class="s"&gt;# Modify the Helm value file with the new tag&lt;/span&gt;
          &lt;span class="s"&gt;sed -i.bak "s/tag: \".*\"/tag: \"${version}\"/" ${app_name}/values.yaml&lt;/span&gt;

          &lt;span class="s"&gt;# Create a new branch&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.email ${{ github.actor }}@github.com&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.name ${{ github.actor }}&lt;/span&gt;
          &lt;span class="s"&gt;git checkout -b ${release_branch}&lt;/span&gt;

          &lt;span class="s"&gt;# Commit changes&lt;/span&gt;
          &lt;span class="s"&gt;git add ${app_name}/values.yaml&lt;/span&gt;
          &lt;span class="s"&gt;git commit -m "release: ${app_name} ${version}"&lt;/span&gt;

          &lt;span class="s"&gt;# Push the changes to the remote repository&lt;/span&gt;
          &lt;span class="s"&gt;git push origin ${release_branch}&lt;/span&gt;

          &lt;span class="s"&gt;# Body Content&lt;/span&gt;
          &lt;span class="s"&gt;pr_body="${{ github.repository }} ${app_name} release ${version}. Please note that this is an automated PR."&lt;/span&gt;

          &lt;span class="s"&gt;# Title Content&lt;/span&gt;
          &lt;span class="s"&gt;pr_title="release: ${app_name} ${version}"&lt;/span&gt;

          &lt;span class="s"&gt;# Open a pull request&lt;/span&gt;
          &lt;span class="s"&gt;gh pr create --title "${pr_title}" --body "${pr_body}" --base main --head ${release_branch} --repo ${{ env.CONFIG_REPO }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Triggers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event:&lt;/strong&gt; Triggered when a new release is created.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Registry:&lt;/strong&gt; The container registry used (GitHub Container Registry - &lt;code&gt;ghcr.io&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config Repository:&lt;/strong&gt; Repository used for configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Jobs
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;build-and-publish&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Steps:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Checkout:&lt;/strong&gt; Fetches the repository code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check tag name:&lt;/strong&gt; Extracts the tag name from the release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check app name:&lt;/strong&gt; Retrieves the application name from the tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check Docker Image Version:&lt;/strong&gt; Determines the version of the Docker image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Login to GitHub Packages:&lt;/strong&gt; Authenticates to the GitHub Container Registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build and push image:&lt;/strong&gt; Uses Docker Build-Push Action to build and push the Docker image to the registry. It uses the extracted application name and version from earlier steps to tag the image appropriately.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;deployment&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies:&lt;/strong&gt; Depends on the completion of the 'build-and-publish' job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Checkout album-app-config:&lt;/strong&gt; Fetches the configuration repository (&lt;code&gt;CONFIG_REPO&lt;/code&gt;) code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update album-app-config repository:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Retrieves the version and app name from the 'build-and-publish' job's outputs.&lt;/li&gt;
&lt;li&gt;Creates a new release branch with the format &lt;code&gt;release/${app_name}_${version}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Modifies a Helm value file in the config repository with the new tag.&lt;/li&gt;
&lt;li&gt;Commits the changes and pushes them to the remote repository.&lt;/li&gt;
&lt;li&gt;Creates a pull request with automated content.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the pull request has been merged, ArgoCD will detect the changes/disparities and start the sync process.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/6bcoTivOVT4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Sync Policies
&lt;/h2&gt;

&lt;p&gt;You can use ArgoCD sync policies to control the sync behaviour.&lt;/p&gt;

&lt;p&gt;Note: By default, changes that are made to the live cluster will not trigger automated sync.&lt;/p&gt;

&lt;p&gt;To enable &lt;strong&gt;self-healing&lt;/strong&gt;, we might need to include below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;automated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;selfHeal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;By the time you read this article, the &lt;code&gt;selfHeal&lt;/code&gt; option may or may not be included. See &lt;a href="https://github.com/krishanthisera/album-app-config/blob/main/apps/templates/album-app.yaml" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://argo-cd.readthedocs.io/en/stable/user-guide/auto_sync/#automated-sync-policy" rel="noopener noreferrer"&gt;docs&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sync Options
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Sync options&lt;/strong&gt; specify how the synchronization should occur, providing additional configurations and parameters for the synchronization process itself.&lt;/p&gt;

&lt;p&gt;When using &lt;code&gt;auto-sync&lt;/code&gt; in Argo CD, it currently applies all objects in an application, causing delays and strain on the API server, but enabling selective sync will only sync resources that are out-of-sync, reducing time and server load for applications with many objects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;syncPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;syncOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ApplyOutOfSyncOnly=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the &lt;a href="https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Across these articles, we've gone from the basics to some advanced concepts in GitOps using ArgoCD. We started by setting up ArgoCD and deploying a sample app, giving a solid foundation. Then, we dive deep into the code intricacies. Lastly, we explored CI/CD implementation, an essential part of any developer's toolkit. &lt;/p&gt;

&lt;p&gt;Hopefully, these pieces have been a practical guide, making GitOps more accessible and showing its potential to streamline development workflows. As we close this series, keep experimenting and leveraging GitOps—it's a game-changer for modern development.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>githubactions</category>
      <category>cicd</category>
      <category>automation</category>
    </item>
    <item>
      <title>GitOps for Devs - Part 2: Navigating the Code</title>
      <dc:creator>Krishan Thisera</dc:creator>
      <pubDate>Sun, 31 Dec 2023 04:39:11 +0000</pubDate>
      <link>https://dev.to/krishanthisera/gitops-for-devs-part-2-the-code-4g5f</link>
      <guid>https://dev.to/krishanthisera/gitops-for-devs-part-2-the-code-4g5f</guid>
      <description>&lt;p&gt;We're continuing our exploration of GitOps with ArgoCD. In the first part, we got a grasp of GitOps basics and set up an example app called Album-App. Now, let's explore the repositories that drive this application:&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Repositories
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Application Code&lt;/strong&gt; - This contains the actual code for both the Frontend and Backend applications.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: A simple REST API written in Go, accessible on port 8080.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: A React application that communicates with the backend.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config Repo&lt;/strong&gt; - Hosts Helm charts defining the application's infrastructure.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Application Repository
&lt;/h2&gt;

&lt;p&gt;The application repository contains the code for both Frontend and Backend applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend
&lt;/h3&gt;

&lt;p&gt;The backend application itself is a simple REST API written in Go.&lt;/p&gt;

&lt;p&gt;The application exposes port &lt;code&gt;8080&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can test the application on your docker environment by,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 ghcr.io/krishanthisera/album-app-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the container is up click &lt;a href="http://localhost:8080/docs/index.html" rel="noopener noreferrer"&gt;here&lt;/a&gt; to see API documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend
&lt;/h3&gt;

&lt;p&gt;The Frontend is a React application that talks to the backend.&lt;/p&gt;

&lt;p&gt;To test the application locally,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 ghcr.io/krishanthisera/album-app-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Please note that the backend app should be running on port 8080.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration Repository
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/krishanthisera/album-app-config" rel="noopener noreferrer"&gt;Config Repo&lt;/a&gt; contains Helm charts that define the application infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faevvhrq7sb51x5idmsi6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faevvhrq7sb51x5idmsi6.png" alt="Helm Charts" width="800" height="650"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we have four helm charts ⎈,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;root-app&lt;/code&gt;: Acts as ArgoCD's entry point, managing and deploying the whole application stack.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apps&lt;/code&gt;: Contains ArgoCD applications.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frontend and backend&lt;/code&gt;: Helm charts for respective Kubernetes deployments.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Understanding Root App
&lt;/h3&gt;

&lt;p&gt;As the name suggests, the &lt;code&gt;root app&lt;/code&gt; is the entry point for ArgoCD to manage and deploy the entire application stack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root-app&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;
  &lt;span class="na"&gt;finalizers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;resources-finalizer.argocd.argoproj.io&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;album-app&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;in-cluster&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/krishanthisera/album-app-config&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajmn5dv8ciln65ky7lkt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajmn5dv8ciln65ky7lkt.png" alt="Root App" width="391" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a snippet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Destination:&lt;/strong&gt; Specifies where the application will be deployed (namespace and cluster).&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; Defines the ArgoCD project the application belongs to. Projects offer a way to group and organize applications based on shared policies, permissions, and settings.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Default ArgoCD Project&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AppProject&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;  &lt;span class="c1"&gt;# Name of the project&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;  &lt;span class="c1"&gt;# Namespace where the project resides&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;clusterResourceWhitelist&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Whitelisted cluster resources&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;  &lt;span class="c1"&gt;# All resource groups allowed&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;   &lt;span class="c1"&gt;# All resource kinds allowed&lt;/span&gt;
  &lt;span class="na"&gt;destinations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Deployment destinations&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;  &lt;span class="c1"&gt;# Deploy to all namespaces&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;  &lt;span class="c1"&gt;# Deploy to all servers&lt;/span&gt;
  &lt;span class="na"&gt;sourceRepos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Allowed source repositories&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;  &lt;span class="c1"&gt;# Allow all repositories&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt; Indicates the source of the application's configuration (Git repository URL, path, and target revision).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here the &lt;code&gt;root-app&lt;/code&gt; points to the &lt;code&gt;/apps&lt;/code&gt; directory in the config repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apps Helm Chart
&lt;/h2&gt;

&lt;p&gt;As mentioned in the previous article, here we follow the ArgoCD App-of-Apps pattern.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzckvmh5dfxd9rsj4t1jh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzckvmh5dfxd9rsj4t1jh.png" alt="Apps of Apps" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Above are ArgoCD application resources residing in the argocd namespace. They define their respective apps' deployment targets, projects, and source repositories.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Backend Application&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;album-app-backend&lt;/span&gt;  &lt;span class="c1"&gt;# Name of the ArgoCD application&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;  &lt;span class="c1"&gt;# Namespace where the ArgoCD application resides&lt;/span&gt;
  &lt;span class="na"&gt;finalizers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;resources-finalizer.argocd.argoproj.io&lt;/span&gt;  &lt;span class="c1"&gt;# Action to be taken when deleting the resource&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;album-app&lt;/span&gt;  &lt;span class="c1"&gt;# Deployment target namespace for the backend application&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;  &lt;span class="c1"&gt;# Kubernetes server for deployment&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;album-app&lt;/span&gt;  &lt;span class="c1"&gt;# Project to which the backend application belongs&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;  &lt;span class="c1"&gt;# Directory containing backend code/configuration&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/krishanthisera/album-app-config&lt;/span&gt;  &lt;span class="c1"&gt;# Git repository URL&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;  &lt;span class="c1"&gt;# Target revision of the repository&lt;/span&gt;

&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;# Frontend Application&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argoproj.io/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;album-app-frontend&lt;/span&gt;  &lt;span class="c1"&gt;# Name of the ArgoCD application&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;argocd&lt;/span&gt;  &lt;span class="c1"&gt;# Namespace where the ArgoCD application resides&lt;/span&gt;
  &lt;span class="na"&gt;finalizers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;resources-finalizer.argocd.argoproj.io&lt;/span&gt;  &lt;span class="c1"&gt;# Action to be taken when deleting the resource&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;album-app&lt;/span&gt;  &lt;span class="c1"&gt;# Deployment target namespace for the frontend application&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://kubernetes.default.svc&lt;/span&gt;  &lt;span class="c1"&gt;# Kubernetes server for deployment&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;album-app&lt;/span&gt;  &lt;span class="c1"&gt;# Project to which the frontend application belongs&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;  &lt;span class="c1"&gt;# Directory containing frontend code/configuration&lt;/span&gt;
    &lt;span class="na"&gt;repoURL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/krishanthisera/album-app-config&lt;/span&gt;  &lt;span class="c1"&gt;# Git repository URL&lt;/span&gt;
    &lt;span class="na"&gt;targetRevision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HEAD&lt;/span&gt;  &lt;span class="c1"&gt;# Target revision of the repository&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgg0rs21nesjzziajb49.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgg0rs21nesjzziajb49.png" alt="Apps of Apps" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you follow the repo, the helm template creates the namespace and the ArgoCD project.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you want to ensure that child apps and all of their resources are deleted when the parent app is deleted, it's essential to add the appropriate finalizer to your Application definition. This finalizer action ensures proper cleanup and deletion of all related resources when the parent application is removed. See &lt;a href="https://argo-cd.readthedocs.io/en/stable/user-guide/app_deletion/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I have included all the ArgoCD applications this repo intended to deploy.&lt;/p&gt;

&lt;p&gt;The frontend and backend ArgoCD Application resources point to their respective directory in the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend and Backend Helm Charts
&lt;/h2&gt;

&lt;p&gt;These Helm charts define how the frontend and backend applications will be deployed on Kubernetes. They contain configuration details encapsulated within &lt;code&gt;values.yaml&lt;/code&gt;. files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;replicaCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;album-app&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/krishanthisera/album-app-backend&lt;/span&gt;
  &lt;span class="na"&gt;pullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1.3.2"&lt;/span&gt; &lt;span class="c1"&gt;# Important: This tag signifies the version of the backend application.&lt;/span&gt;
  &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;

&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;album-app-backend&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo552iaye7kvvg7jyynoi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo552iaye7kvvg7jyynoi.png" alt="Backend Resources" width="799" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The value files are pretty straightforward. The only thing to note is the &lt;code&gt;tag&lt;/code&gt; value. It is the image tag of the backend application.&lt;/p&gt;

&lt;p&gt;During the release process of the application, the &lt;code&gt;tag&lt;/code&gt; value will be replaced with the respective image tag.&lt;/p&gt;

&lt;p&gt;Part 3 of the article series will cover the release process including CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;Stay tuned!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>go</category>
      <category>microservices</category>
      <category>cicd</category>
    </item>
    <item>
      <title>GitOps for Devs - Part 01: Installation</title>
      <dc:creator>Krishan Thisera</dc:creator>
      <pubDate>Thu, 21 Dec 2023 12:09:27 +0000</pubDate>
      <link>https://dev.to/krishanthisera/gitops-for-devs-part-01-installation-2ach</link>
      <guid>https://dev.to/krishanthisera/gitops-for-devs-part-01-installation-2ach</guid>
      <description>&lt;p&gt;GitOps is a hot topic in DevOps circles these days, and this article series aims to break down its core concepts starting from the basics. We'll kick things off by setting up ArgoCD a GitOps operator and deploying a sample application in this initial article. The following parts will dive deeper into configuration specifics.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitOps 101
&lt;/h2&gt;

&lt;p&gt;GitOps automates modern cloud infrastructure setup by treating configuration files like code—similar to application source code. It ensures consistent and replicable infrastructure deployments, aligning with DevOps practices for quick code deployment, efficient cloud resource management, and adaptation to rapid development cycles.&lt;/p&gt;

&lt;p&gt;In the context of Kubernetes and microservices, GitOps serves as a model for managing infrastructure and application deployment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Declarative Infrastructure Management:&lt;/strong&gt; GitOps takes a declarative approach, defining the desired state of the Kubernetes cluster and its resources in configuration files (often YAML) stored in a Git repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Synchronization:&lt;/strong&gt; Changes to these configuration files trigger an automated synchronization process in GitOps, ensuring the Kubernetes cluster's actual state matches the defined state in the repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version Control Using Git:&lt;/strong&gt; Leveraging Git for version control allows teams to track changes, revert as needed, and collaborate on configurations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now that we've covered the basics of GitOps, let's jump into the example. We'll deploy a simple application on Kubernetes using &lt;code&gt;minikube&lt;/code&gt; to illustrate these concepts.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI/CD Workflow
&lt;/h3&gt;

&lt;p&gt;There are primarily two workflows: pull-based and push-based.&lt;/p&gt;

&lt;h4&gt;
  
  
  Push-based GitOps Workflow
&lt;/h4&gt;

&lt;p&gt;In a push-based GitOps workflow, the deployment and synchronization are initiated externally by a CI/CD system or an operator. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6yk7lwddc2erwme13tbc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6yk7lwddc2erwme13tbc.png" alt="GitOps Push" width="800" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application Repo:&lt;/strong&gt; Contains the application source code, which is intended to package and later deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config Repo:&lt;/strong&gt; The desired state of the infrastructure and applications is defined in this Git repository, often through declarative configuration files like YAML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitOps Operator:&lt;/strong&gt; An external system, like a CI/CD tool, monitors the codebase and, upon changes, triggers the GitOps workflow.&lt;/p&gt;

&lt;p&gt;Developers make changes to the Application Repo, either by creating pull requests or directly committing alterations to the codebase. As a result, a CI/CD pipeline is triggered, initiating the creation and pushing of new container images to the container registry.&lt;/p&gt;

&lt;p&gt;Subsequently, these newly generated images or their tags are updated in the Config Repo. This step could be executed through an automated commit or by means of a pull request intended for manual merging.&lt;/p&gt;

&lt;p&gt;The GitOps operator, an external system like a CI/CD tool, closely monitors the codebase for any changes. Once alterations to the Config Repo are detected, the GitOps operator pulls the most recent configuration from the repository. It then proceeds to deploy or update the infrastructure and applications based on this updated configuration. This process is continuous, as the operator consistently monitors the Config Repo for any further changes, ready to repeat the deployment cycle when new updates are detected.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pull-based GitOps Workflow
&lt;/h4&gt;

&lt;p&gt;In a pull-based GitOps workflow, the deployment and synchronization of the infrastructure and applications are triggered by changes in the Git repository. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx3111h5q3k1u1bdjd8gg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx3111h5q3k1u1bdjd8gg.png" alt="GitOps Pull" width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Similar to the push-based workflow, there are two primary repositories: the &lt;strong&gt;Application Repo&lt;/strong&gt; and the &lt;strong&gt;Config Repo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitOps Operator:&lt;/strong&gt; The GitOps operator or tool continuously monitors the Git repository for changes. This operator usually sits closely with infrastructure, in this case, deployed in a Kubernetes cluster.&lt;/p&gt;

&lt;p&gt;Developers, again, make changes to the &lt;strong&gt;Application Repo&lt;/strong&gt;, either by creating pull requests or directly committing changes to the codebase. This triggers a CI/CD pipeline, resulting in the creation and pushing of new container images to the container registry.&lt;/p&gt;

&lt;p&gt;Following this, the updated image or its tag is modified in the Config Repo, either through an automated commit or a pull request for manual merging.&lt;/p&gt;

&lt;p&gt;However, the GitOps operator's role is distinct in this workflow. Instead of external initiation, the GitOps operator constantly monitors the Config Repo for any changes. As soon as changes are detected, the operator pulls the latest configuration from the repository and proceeds to deploy or update the infrastructure and applications based on this revised configuration.&lt;/p&gt;

&lt;p&gt;Similar to the push-based workflow, this process remains continuous, with the GitOps operator continuously monitoring the Config Repo for any subsequent changes, ready to redeploy or update as needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install minikube
&lt;/h2&gt;

&lt;p&gt;If you haven't installed Minikube already you can follow the official documentation &lt;a href="https://minikube.sigs.k8s.io/docs/start/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or you can use below shell script to upgrade/install &lt;code&gt;minikube&lt;/code&gt; if you are a Linux user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#! /bin/sh&lt;/span&gt;

&lt;span class="c"&gt;# Minikube update script file&lt;/span&gt;
&lt;span class="c"&gt;# Ref: https://stackoverflow.com/questions/57821066/how-to-update-minikube-latest-version&lt;/span&gt;

minikube delete &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /usr/local/bin/minikube &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-Lo&lt;/span&gt; minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
&lt;span class="nb"&gt;sudo chmod&lt;/span&gt; +x minikube &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
&lt;span class="nb"&gt;sudo cp &lt;/span&gt;minikube /usr/local/bin/ &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
&lt;span class="nb"&gt;sudo rm &lt;/span&gt;minikube &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt; 
minikube start &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;

&lt;span class="c"&gt;# Enabling addons: ingress, dashboard&lt;/span&gt;
minikube addons &lt;span class="nb"&gt;enable &lt;/span&gt;ingress &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
minikube addons &lt;span class="nb"&gt;enable &lt;/span&gt;dashboard &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
minikube addons &lt;span class="nb"&gt;enable &lt;/span&gt;metrics-server &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="c"&gt;# Showing enabled addons&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'\n\n\033[4;33m Enabled Addons \033[0m'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
minikube addons list | &lt;span class="nb"&gt;grep &lt;/span&gt;STATUS &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; minikube addons list | &lt;span class="nb"&gt;grep &lt;/span&gt;enabled &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;

&lt;span class="c"&gt;# Showing the current status of Minikube&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'\n\n\033[4;33m Current status of Minikube \033[0m'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; minikube status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To install &lt;code&gt;kubectl&lt;/code&gt; and &lt;code&gt;helm&lt;/code&gt; please follow the respective official documentation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;kubectl&lt;/code&gt;: &lt;a href="https://kubernetes.io/docs/tasks/tools/#kubectl" rel="noopener noreferrer"&gt;https://kubernetes.io/docs/tasks/tools/#kubectl&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;helm&lt;/code&gt;:  &lt;a href="https://helm.sh/docs/intro/install/" rel="noopener noreferrer"&gt;https://helm.sh/docs/intro/install/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install Argo CD
&lt;/h2&gt;

&lt;p&gt;Now if your &lt;code&gt;minikube&lt;/code&gt; cluster is up and running you are ready to install the &lt;a href="https://argo-cd.readthedocs.io/en/stable/" rel="noopener noreferrer"&gt;ArgoCD&lt;/a&gt;. GitOps operator in your cluster.&lt;/p&gt;

&lt;p&gt;ArgoCD installation is straightforward.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create the namespace for the ArgoCD operator:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace argocd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Install the Operator and CRDs
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-n&lt;/span&gt; argocd &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should expect to find similar resources within the argocd namespace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;NAME                                                    READY   STATUS    RESTARTS       AGE
pod/argocd-application-controller-0                     1/1     Running   0              137m
pod/argocd-applicationset-controller-6b67b96c9f-kcbxl   1/1     Running   0              137m
pod/argocd-dex-server-c9d4d46b5-phltz                   1/1     Running   2 &lt;span class="o"&gt;(&lt;/span&gt;136m ago&lt;span class="o"&gt;)&lt;/span&gt;   137m
pod/argocd-notifications-controller-6975bff68d-c5cdl    1/1     Running   0              137m
pod/argocd-redis-7d8d46cc7f-gjk25                       1/1     Running   0              137m
pod/argocd-repo-server-59f5479b7-xznv6                  1/1     Running   0              137m
pod/argocd-server-7d7fdcb49-st9vj                       1/1     Running   0              137m

NAME                                              TYPE           CLUSTER-IP       EXTERNAL-IP   PORT&lt;span class="o"&gt;(&lt;/span&gt;S&lt;span class="o"&gt;)&lt;/span&gt;                      AGE
service/argocd-applicationset-controller          ClusterIP      10.102.75.218    &amp;lt;none&amp;gt;        7000/TCP,8080/TCP            137m
service/argocd-dex-server                         ClusterIP      10.98.223.112    &amp;lt;none&amp;gt;        5556/TCP,5557/TCP,5558/TCP   137m
service/argocd-metrics                            ClusterIP      10.107.209.210   &amp;lt;none&amp;gt;        8082/TCP                     137m
service/argocd-notifications-controller-metrics   ClusterIP      10.108.175.166   &amp;lt;none&amp;gt;        9001/TCP                     137m
service/argocd-redis                              ClusterIP      10.107.100.213   &amp;lt;none&amp;gt;        6379/TCP                     137m
service/argocd-repo-server                        ClusterIP      10.96.189.5      &amp;lt;none&amp;gt;        8081/TCP,8084/TCP            137m
service/argocd-server                             LoadBalancer   10.100.189.178   &amp;lt;pending&amp;gt;     80:31040/TCP,443:30770/TCP   137m
service/argocd-server-metrics                     ClusterIP      10.109.158.60    &amp;lt;none&amp;gt;        8083/TCP                     137m

NAME                                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/argocd-applicationset-controller   1/1     1            1           137m
deployment.apps/argocd-dex-server                  1/1     1            1           137m
deployment.apps/argocd-notifications-controller    1/1     1            1           137m
deployment.apps/argocd-redis                       1/1     1            1           137m
deployment.apps/argocd-repo-server                 1/1     1            1           137m
deployment.apps/argocd-server                      1/1     1            1           137m

NAME                                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/argocd-applicationset-controller-6b67b96c9f   1         1         1       137m
replicaset.apps/argocd-dex-server-c9d4d46b5                   1         1         1       137m
replicaset.apps/argocd-notifications-controller-6975bff68d    1         1         1       137m
replicaset.apps/argocd-redis-7d8d46cc7f                       1         1         1       137m
replicaset.apps/argocd-repo-server-59f5479b7                  1         1         1       137m
replicaset.apps/argocd-server-7d7fdcb49                       1         1         1       137m

NAME                                             READY   AGE
statefulset.apps/argocd-application-controller   1/1     137m

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you focus on the pods deployed in the namespace, there are three main pods, which are noteworthy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pod/argocd-application-controller-0&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The controller continuously monitors running applications, identifies and resolves inconsistencies in their states (OutOfSync), and executes user-defined hooks for synchronization lifecycle events.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pod/argocd-repo-server-59f5479b7-xznv6&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The Repo server serves as an internal service supporting the application infrastructure by maintaining a local cache of Git repositories and generating Kubernetes manifests using repository-specific details.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pod/argocd-server-7d7fdcb49-st9vj&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Argo CD server aka the API server acts as a central interface for various components, managing application operations, credentials, authentication, RBAC, and Git webhook events while serving as a gRPC/REST server for Web UI, CLI, and CI/CD systems.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a part of the installation, following CRDs should be created in your cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;applications.argoproj.io     
applicationsets.argoproj.io   
appprojects.argoproj.io      
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Terminlogies
&lt;/h2&gt;

&lt;p&gt;Assuming you're familiar with core concepts in Git, Docker, Kubernetes, Continuous Delivery, and GitOps, here are some specific Argo CD terminologies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Application:&lt;/strong&gt; A defined group of Kubernetes resources outlined in a manifest, treated as a Custom Resource Definition (CRD).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target State:&lt;/strong&gt; The intended state of an application, represented by files in a Git repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live State:&lt;/strong&gt; The current operational state of that application, including deployed pods and other components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync Status:&lt;/strong&gt; Indicates whether the live state matches the intended target state described in Git—essentially, is the deployed application aligned with what's defined in the repository?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync:&lt;/strong&gt; The process of transitioning an application to its target state, often done by applying changes to a Kubernetes cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync Operation Status:&lt;/strong&gt; Indicates the success or failure of a sync process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh:&lt;/strong&gt; The action of comparing the latest code in Git with the live state to identify any differences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health:&lt;/strong&gt; Reflects the operational health of the application—whether it's functioning correctly and able to handle requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;a href="https://argo-cd.readthedocs.io/en/stable/core_concepts/" rel="noopener noreferrer"&gt;here&lt;/a&gt; for further information&lt;/p&gt;

&lt;h3&gt;
  
  
  ArgoCD UI
&lt;/h3&gt;

&lt;p&gt;ArgoCD ships with a very powerful User Interface. To access the UI,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set the port forwarding&lt;/li&gt;
&lt;li&gt;Grab the admin password
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Port Forwarding&lt;/span&gt;
kubectl port-forward svc/argocd-server &lt;span class="nt"&gt;-n&lt;/span&gt; argocd 8080:443

&lt;span class="c"&gt;# Intial admin password&lt;/span&gt;
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; argocd get secret argocd-initial-admin-secret &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{.data.password}"&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you should be able to access ArgoCD UI using &lt;code&gt;https://127.0.0.1:8080&lt;/code&gt; using the user name &lt;strong&gt;"admin"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;ArgoCD has its own CLI as well, follow &lt;a href="https://argo-cd.readthedocs.io/en/stable/getting_started/#2-download-argo-cd-cli" rel="noopener noreferrer"&gt;this&lt;/a&gt; article to install the &lt;code&gt;argocli&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the Sample App - Album App
&lt;/h2&gt;

&lt;p&gt;For this tutorial, I am using two repos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Config&lt;/strong&gt;: &lt;a href="https://github.com/krishanthisera/album-app-config" rel="noopener noreferrer"&gt;https://github.com/krishanthisera/album-app-config&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application&lt;/strong&gt;:  &lt;a href="https://github.com/krishanthisera/album-app" rel="noopener noreferrer"&gt;https://github.com/krishanthisera/album-app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As the names suggest, the config repo contains the infrastructure configuration, in this case, helm charts, while the application repo contains the code for &lt;strong&gt;frontend&lt;/strong&gt; and &lt;strong&gt;backend&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;From now on &lt;strong&gt;application&lt;/strong&gt; is referred to &lt;strong&gt;ArgoCD Application&lt;/strong&gt; objects inside the Kubernetes cluster.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To install the Album App,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the config repo (I would recommend you to fork the repo and then clone the fork)&lt;/li&gt;
&lt;li&gt;Install the helm chart&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here we are using ArgoCD &lt;strong&gt;App-of-Apps pattern&lt;/strong&gt;. The root ArgoCD application object is defined in the &lt;code&gt;root-app&lt;/code&gt; helm chart. The root application is responsible for creating ArgoCD application objects for Frontend and Backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the repo&lt;/span&gt;
git clone https://github.com/krishanthisera/album-app-config

&lt;span class="c"&gt;# Create a namespace for the application&lt;/span&gt;
kubectl create ns album-app

&lt;span class="c"&gt;# Install the root app&lt;/span&gt;
helm &lt;span class="nb"&gt;install &lt;/span&gt;root-app ./root-app &lt;span class="nt"&gt;-n&lt;/span&gt; album-app

&lt;span class="c"&gt;# Check the installed application&lt;/span&gt;
kubectl get applications.argoproj.io &lt;span class="nt"&gt;-n&lt;/span&gt; argocd
NAME       SYNC STATUS   HEALTH STATUS
root-app   OutOfSync     Healthy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you install the helm chart, you should be able to see &lt;code&gt;root-app&lt;/code&gt; under the applications in ArgoCD UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fortnggscjo17a3p8dghg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fortnggscjo17a3p8dghg.png" alt="Argo UI" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajmn5dv8ciln65ky7lkt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fajmn5dv8ciln65ky7lkt.png" alt="Root App" width="391" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you look closely, the &lt;code&gt;root-app&lt;/code&gt; is &lt;code&gt;out-sync&lt;/code&gt;; let's sync the &lt;code&gt;root-app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For now let's keep the default settings as it is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fow9lx4oyuqg4cvbh9ttu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fow9lx4oyuqg4cvbh9ttu.png" alt="Sync Root App" width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the &lt;code&gt;root-app&lt;/code&gt; is synced, both the Frontend and Backend ArgoCD application objects are created but they are yet to be synced.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgg0rs21nesjzziajb49.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgg0rs21nesjzziajb49.png" alt="Unsync Album Apps" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's do the same for the Frontend and Backend application; let's get them synced.&lt;/p&gt;

&lt;p&gt;Once both the Frontend and Backed apps are synced your ArgoCD applications should be look like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyq4ye14w8fh440urvuuj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyq4ye14w8fh440urvuuj.png" alt="Synced Frontend" width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo552iaye7kvvg7jyynoi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo552iaye7kvvg7jyynoi.png" alt="Synced Backend" width="799" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All the applications should be synced now 💫&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get applications.argoproj.io &lt;span class="nt"&gt;-n&lt;/span&gt; argocd        
NAME                 SYNC STATUS   HEALTH STATUS
album-app-backend    Synced        Healthy
album-app-frontend   Synced        Healthy
root-app             Synced        Healthy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you do a &lt;code&gt;kubectl get all&lt;/code&gt; on the &lt;code&gt;album-app&lt;/code&gt; namespace,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;NAME                                               READY   STATUS    RESTARTS   AGE
pod/album-app-backend-backend-dcbbc657-stqmc       1/1     Running   0          11h
pod/album-app-frontend-frontend-789b5bbc67-9vd5x   1/1     Running   0          11h

NAME                                  TYPE        CLUSTER-IP       EXTERNAL-IP   PORT&lt;span class="o"&gt;(&lt;/span&gt;S&lt;span class="o"&gt;)&lt;/span&gt;    AGE
service/album-app-backend             ClusterIP   10.104.144.238   &amp;lt;none&amp;gt;        8080/TCP   11h
service/album-app-frontend-frontend   ClusterIP   10.108.6.129     &amp;lt;none&amp;gt;        80/TCP     11h

NAME                                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/album-app-backend-backend     1/1     1            1           11h
deployment.apps/album-app-frontend-frontend   1/1     1            1           11h

NAME                                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/album-app-backend-backend-dcbbc657       1         1         1       11h
replicaset.apps/album-app-frontend-frontend-789b5bbc67   1         1         1       11h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Further, you can port-forward the &lt;code&gt;album-app&lt;/code&gt; Frontend service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward &lt;span class="nt"&gt;-n&lt;/span&gt; album-app services/album-app-frontend-frontend 8090:80
Forwarding from 127.0.0.1:8090 -&amp;gt; 3000
Forwarding from &lt;span class="o"&gt;[&lt;/span&gt;::1]:8090 -&amp;gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nhbmohda5qa3gz3583m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nhbmohda5qa3gz3583m.png" alt="Album App UI" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that we have installed the Album App, let's dive deep into the ArgoCD-specific configuration.&lt;/p&gt;

&lt;p&gt;Stay tuned. To be continued...&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>cloud</category>
      <category>docker</category>
    </item>
    <item>
      <title>AWS Static Hosting - Part 02: CloudFront Edge Functions</title>
      <dc:creator>Krishan Thisera</dc:creator>
      <pubDate>Wed, 23 Aug 2023 10:26:55 +0000</pubDate>
      <link>https://dev.to/krishanthisera/aws-static-hosting-part-02-cloudfront-edge-functions-4fpb</link>
      <guid>https://dev.to/krishanthisera/aws-static-hosting-part-02-cloudfront-edge-functions-4fpb</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/krishanthisera/host-a-static-website-in-aws-using-cloudfront-s3-and-terraform-hgg"&gt;previous article&lt;/a&gt; we discussed how we can put together AWS CloudFront, S3 bucket and other associated services using Terraform to host our static website. We now need to make our site SEO-friendly, especially if it contains dynamic content.  &lt;/p&gt;

&lt;p&gt;Before we get started, let's discuss some theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does it mean: Pre-rendering a website in the context of CSR
&lt;/h2&gt;

&lt;p&gt;When a web crawler visits a website, it follows the same process as a regular user's browser: it sends request to the website's server, and in response, the server sends back the necessary files, such as HTML, CSS, JavaScript, and images. The web crawler uses this information to build an index of the website's content, which allows search engines to provide relevant results to users when they conduct searches.  &lt;/p&gt;

&lt;p&gt;Now, to speed up this indexing process and provide a more efficient experience for search engines. This is when pre-rendering comes into play. Pre-rendering involves sending a pre-rendered static HTML version of the webpage to the web crawler instead of just the server-side files. The pre-rendered version already contains all the essential content and is ready for the web crawler to process without the need for further rendering.&lt;/p&gt;

&lt;p&gt;By providing a pre-rendered HTML version of the page to web crawlers, websites can ensure that search engines can quickly and accurately index their content. This can lead to better visibility in search engine results and an overall improved SEO (Search Engine Optimization) performance. It's a technique that benefits both the website owners and the search engines, as it allows for more efficient indexing and faster access to relevant content for users conducting searches.&lt;/p&gt;

&lt;h2&gt;
  
  
  How are we going to implement this?
&lt;/h2&gt;

&lt;p&gt;As we are clear now what is Prerendering, we shall conquer what solution we are going to use.  &lt;/p&gt;

&lt;p&gt;There are more than hand full of solutions that we can use in this case, in fact, you should be able to come up with your own solution using your favorite programming language. But here we use, &lt;a href="https://docs.prerender.io/" rel="noopener noreferrer"&gt;prerender.io&lt;/a&gt; as our solution.  &lt;/p&gt;

&lt;p&gt;You can host it on your own infrastructure if you wish, you can follow the document &lt;a href="https://github.com/prerender/prerender" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this article, I am going to use their cloud offering, which has a free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's see what happen under the hood
&lt;/h2&gt;

&lt;p&gt;First, before we dig deeper into the solution, let's clarify a couple of basics.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Request categories
&lt;/h3&gt;

&lt;p&gt;Based on our context, we can categories requests to the web site based on client,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser client:&lt;/strong&gt; This is a regular human user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crawler:&lt;/strong&gt; Web crawlers or bots who are visiting to website&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prerender crawlers:&lt;/strong&gt;  Crawlers from Prerender services&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  So how can we identify them?
&lt;/h3&gt;

&lt;p&gt;Browser clients and crawlers can be easily identified by looking at the &lt;code&gt;user-agent&lt;/code&gt; header. In terms of the Prerender service, Prerender service itself sends an &lt;code&gt;x-Prerender&lt;/code&gt; header along with the request, so we can use that header.&lt;/p&gt;

&lt;h3&gt;
  
  
  CloudFront event and lambada at edge integration
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbizkt.imgix.net%2Fposts%2Fedge-functions%2Fcloudfront-events-that-trigger-lambda-functions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbizkt.imgix.net%2Fposts%2Fedge-functions%2Fcloudfront-events-that-trigger-lambda-functions.png" alt="Lambda at edge functions" width="545" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In summary, by linking a CloudFront distribution with a Lambda@Edge function, CloudFront gains the ability to capture and handle requests and responses at its edge locations. This integration enables the execution of Lambda functions triggered by specific CloudFront events. These events encompass different stages in the request-response cycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Viewer request event:&lt;/strong&gt; This occurs when CloudFront receives a request from a viewer, meaning a user or a client attempting to access content through CloudFront.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin request event:&lt;/strong&gt; Before CloudFront forwards a request to the origin, this event takes place. The origin refers to the source server that holds the actual content being requested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Origin response" event:&lt;/strong&gt; CloudFront triggers this event when it receives a response from the origin server. The response contains the requested content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Viewer response event:&lt;/strong&gt; This event happens just before CloudFront sends the response back to the viewer, ensuring any required modifications or customizations can be applied.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Refer &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html" rel="noopener noreferrer"&gt;this&lt;/a&gt; for further information.&lt;/p&gt;

&lt;p&gt;Alright, back to the original solution discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ordinary Browser requests
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbizkt.imgix.net%2Fposts%2Fedge-functions%2Fprerender-browser-requests.drawio.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbizkt.imgix.net%2Fposts%2Fedge-functions%2Fprerender-browser-requests.drawio.png" alt="browser requests" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Crawlers and Prerender request
&lt;/h2&gt;

&lt;p&gt;Let's discuss what happens to the requests from crawlers.&lt;br&gt;
Let's assume that the particular request has never been cached in Prerender.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbizkt.imgix.net%2Fposts%2Fedge-functions%2Fprerender-crawler-requests.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbizkt.imgix.net%2Fposts%2Fedge-functions%2Fprerender-crawler-requests.png" alt="crawler requests" width="800" height="817"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Request from a crawler hit the CloudFront distribution

&lt;ul&gt;
&lt;li&gt;CloudFront verify that it is a crawler, and it is not from Prerender
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Then CloudFront talks to Prerender (&lt;em&gt;I have a request from a crawler, please pass me the static/rendered webpage&lt;/em&gt;)
&lt;/li&gt;
&lt;li&gt;Then Prerender, hold the current request from CloudFront, and send a brand-new request to CloudFront

&lt;ul&gt;
&lt;li&gt;Now, CloudFront validate that this request is from Prerender&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CloudFront would serve this request from Prerender as a normal user request&lt;/li&gt;
&lt;li&gt;Then Prerender render the content and respond to the CloudFront with static/rendered (HTML) web content&lt;/li&gt;
&lt;li&gt;Finally, CloudFront respond to the Crawler with the static content from Prerender&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Error Handling
&lt;/h3&gt;

&lt;p&gt;Let's take a brief look at error handling. Suppose a request is made for a page that isn't available. Web servers typically return a "not found" page with or without 404 HTTP status code. Prerender should not cache these 404 pages, nor let search engines to index them; in such a case, we should inform the crawler that the request isn't valid.&lt;/p&gt;

&lt;p&gt;To address this Prerender offers a very cool solution 💡. You can embed the error code into your HTML using meta tags, allowing Prerender to detect and relay it back. In other words, you can map a HTTP status code using HTML meta tags.&lt;/p&gt;

&lt;p&gt;For more information, see &lt;a href="https://docs.prerender.io/docs/status-codes" rel="noopener noreferrer"&gt;here.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will discuss this more with an example code later in this article.  &lt;/p&gt;
&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;Before diving deep into the code, I'd like to touch upon the setup. In the subsequent sections, I'll reference our &lt;a href="https://github.com/krishanthisera/aws-edge-functions" rel="noopener noreferrer"&gt;aws-edge-functions&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;This repository to be used as a Terraform sub-module with the AWS Static Hosting module mention &lt;a href="https://github.com/krishanthisera/aws-static-hosting" rel="noopener noreferrer"&gt;here&lt;/a&gt;. The primary focus of this module is to deploy our Lambda@Edge functions, facilitating Prerender integration with CloudFront.&lt;/p&gt;

&lt;p&gt;The repository can be divided into two sections,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Terraform IaC&lt;/strong&gt; dedicated to the edge function deployment&lt;/li&gt;
&lt;li&gt;A monorepo for the &lt;strong&gt;edge functions&lt;/strong&gt; and their development and build configuration&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Terraform IaC for edge function deployment
&lt;/h3&gt;

&lt;p&gt;Now, let's narrow our focus to the Terraform code, setting aside the Prerender and the theoretical aspects discussed earlier.&lt;/p&gt;

&lt;p&gt;Our objective is to deploy a series of AWS Lambda functions. In this particular context,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;first, we must build the code&lt;/li&gt;
&lt;li&gt;then, we can package and upload it as a Lambda function (AKA deploy).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Assume all build configurations have been preset for us. All we'd need to do is run a specific build command, which will compile (transpile to be exact) the code.&lt;/p&gt;

&lt;p&gt;The function below executes the build command each time we run the &lt;code&gt;terraform apply&lt;/code&gt; command. Here, we've defined two null resources with some local provisioners:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;To verify the presence of Node&lt;/li&gt;
&lt;li&gt;To install dependencies and build the code.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# build.tf&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"null_resource"&lt;/span&gt; &lt;span class="s2"&gt;"check_node_version"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="nx"&gt;triggers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;always_run&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"npx yarn version  --non-interactive"&lt;/span&gt;
    &lt;span class="nx"&gt;working_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/${var.edge_function_path}"&lt;/span&gt;

    &lt;span class="nx"&gt;interpreter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;on_failure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fail&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"null_resource"&lt;/span&gt; &lt;span class="s2"&gt;"build_edge_functions"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="nx"&gt;triggers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;always_run&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"npx yarn install  --non-interactive &amp;amp;&amp;amp; npx yarn build"&lt;/span&gt;
    &lt;span class="nx"&gt;working_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/${var.edge_function_path}"&lt;/span&gt;

    &lt;span class="nx"&gt;interpreter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;on_failure&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fail&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;null_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;check_node_version&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;A Quick Note:&lt;/strong&gt; For this setup, I'm leveraging a pipeline to deploy the infrastructure. I've chosen &lt;a href="https://spacelift.io/" rel="noopener noreferrer"&gt;Spacelift&lt;/a&gt; for this purpose. If you've been following along from the previous article, you might recall the &lt;a href="https://github.com/krishanthisera/aws-static-hosting" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; associated with Article 01. Within that repo, you'll find a &lt;a href="https://github.com/krishanthisera/aws-static-hosting/blob/main/Dockerfile" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt; 🤔.&lt;/p&gt;

&lt;p&gt;Why is this Dockerfile significant? Spacelift offers the capability to pair custom build environments with its runners. So, I've incorporated &lt;code&gt;Node.js&lt;/code&gt; and &lt;code&gt;npm&lt;/code&gt; into the runner's environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# https://github.com/krishanthisera/aws-static-hosting/blob/main/Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; public.ecr.aws/spacelift/runner-terraform:latest&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; root&lt;/span&gt;

&lt;span class="c"&gt;# Install node and npm&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--update&lt;/span&gt; &lt;span class="nt"&gt;--no-cache&lt;/span&gt; nodejs npm

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; spacelift&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we need to create a role and associate it with the Lambda function. This step enables us to utilize our Lambda functions as  Lambda@Edge functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# data.tf&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"lambda_edge_assume_role_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LambdaEdgeExecution"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"lambda.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"edgelambda.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# main.tf&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"lambda_edge_exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_edge_assume_role_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It's essential to specify both identifiers for Lambda@Edge functions. We will later associate this role with the function&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now, all that remains is to push the build artifacts to AWS. It's important to note that if we intend to associate these lambdas with CloudFront as Lambda@Edge functions, we must deploy them in the &lt;code&gt;us-east-1&lt;/code&gt; region.&lt;/p&gt;

&lt;p&gt;We'll discuss more about the build process later. For the time being, let's assume our build, or as I prefer to term it, our "packed" artifacts, are stored in a designated location. We can utilize Terraform locals to represent them in a more comprehensible format.&lt;/p&gt;

&lt;p&gt;We can utilize Terraform locals to display them in a more comprehensible manner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt; &lt;span class="c1"&gt;# main.tf&lt;/span&gt;
 &lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;edge_functions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prerender-proxy"&lt;/span&gt;
      &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.edge_function_path}/packages/prerender-proxy/build/index.js"&lt;/span&gt;
      &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"index.handler"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"filter-function"&lt;/span&gt;
      &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.edge_function_path}/packages/filter-function/build/index.js"&lt;/span&gt;
      &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"index.handler"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"response-handler"&lt;/span&gt;
      &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.edge_function_path}/packages/response-handler/build/index.js"&lt;/span&gt;
      &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"index.handler"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, I have defined functions by their names, specify their locations (build), and indicate the handler.&lt;/p&gt;

&lt;p&gt;We've defined three functions here. We'll dig deeper into the specifics of each function later. For now, our primary task is to package each of them into separate zip files for deployment.&lt;/p&gt;

&lt;p&gt;Now, we just need to define the Lambda configuration. We can employ Terraform's &lt;code&gt;count&lt;/code&gt; meta-argument to iterate over &lt;code&gt;locals&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"archive_file"&lt;/span&gt; &lt;span class="s2"&gt;"edge_function_archives"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge_functions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"zip"&lt;/span&gt;
  &lt;span class="nx"&gt;source_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/${local.edge_functions[count.index].path}"&lt;/span&gt;
  &lt;span class="nx"&gt;output_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/${var.edge_function_path}/function_archives/${local.edge_functions[count.index].name}.zip"&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;null_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;build_edge_functions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, for each function in &lt;code&gt;local.edge_functions&lt;/code&gt;, an archive file will be created.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_function"&lt;/span&gt; &lt;span class="s2"&gt;"edge_functions"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# If the file is not in the current working directory you will need to include a&lt;/span&gt;
  &lt;span class="c1"&gt;# path.module in the filename.&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge_functions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;filename&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/${var.edge_function_path}/function_archives/${local.edge_functions[count.index].name}.zip"&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${local.edge_functions[count.index].name}"&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge_functions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;
  &lt;span class="nx"&gt;publish&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;memory_size&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_edge_exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;source_code_hash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge_function_archives&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;output_base64sha256&lt;/span&gt;

  &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nodejs16.x"&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"lambda_edge_exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_edge_assume_role_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While each line of code is self-explanatory, it's worth noting that we're associating the IAM role defined earlier.&lt;/p&gt;

&lt;p&gt;Now, we need to think a couple of steps ahead, we can't solely rely on the lambda function names when associating them with CloudFront. We specifically need the ARN — more precisely, the ARN of a specific version. Since this will be a Terraform sub-module to be used in our static hosting module (as recalled from article 01 😉), accessing these ARNs programmatically is essential.&lt;/p&gt;

&lt;p&gt;Hence, the ARNs will be output as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# output.tf&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"function_arns"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge_functions&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qualified_arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Static Hosting Stack
&lt;/h4&gt;

&lt;p&gt;Let's discuss how we can associate our Lambda functions with CloudFront. For this segment, I'll be referencing the same repository as we see in the first article. You can find it &lt;a href="https://github.com/krishanthisera/aws-static-hosting" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First we need to import our lambda at edge module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# edge-functions.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"edge-functions"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github.com/krishanthisera/aws-edge-functions.git"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can associate our Lambda functions with our CloudFront distribution by simply referencing their names. Neat, right?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudfront_distribution"&lt;/span&gt; &lt;span class="s2"&gt;"blog_distribution"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="p"&gt;...&lt;/span&gt;

  &lt;span class="nx"&gt;default_cache_behavior&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_methods&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;cached_methods&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;target_origin_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S3-${var.bucket_name}"&lt;/span&gt;

    &lt;span class="nx"&gt;lambda_function_association&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;event_type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"origin-request"&lt;/span&gt;
      &lt;span class="nx"&gt;lambda_arn&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge-functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;function_arns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"prerender-proxy"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;lambda_function_association&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;event_type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"origin-response"&lt;/span&gt;
      &lt;span class="nx"&gt;lambda_arn&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge-functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;function_arns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"response-handler"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;lambda_function_association&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;event_type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"viewer-request"&lt;/span&gt;
      &lt;span class="nx"&gt;lambda_arn&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge-functions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;function_arns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"filter-function"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;forwarded_values&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;headers&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"x-request-prerender"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"x-prerender-host"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;query_string&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

      &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;forward&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

   &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, we've primarily addressed the Terraform and infrastructure components. Next, we'll discuss the specifics of the edge functions, focusing especially on the business logic and it's implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge functions
&lt;/h3&gt;

&lt;p&gt;From this point on in our discussion, I'll be referencing the content inside the &lt;a href="https://github.com/krishanthisera/aws-edge-functions/tree/master/edge-functions" rel="noopener noreferrer"&gt;edge-functions directory&lt;/a&gt; of our aws-edge-functions repo.&lt;/p&gt;

&lt;p&gt;This directory contains a monorepo for our edge functions. If you're unfamiliar with the term "monorepo", it stands for "monolithic repository". In a monorepo setup, multiple projects or components of a software application are stored within a single version control repository. So instead of managing distinct repositories for every project or component, everything is centralized.&lt;/p&gt;

&lt;p&gt;In our setup, all our Lambda@Edge functions are located in the packages directory. These functions are written in TypeScript. When we build them, the TypeScript code for each edge function is transpiled into a single JavaScript package.&lt;/p&gt;

&lt;p&gt;We manage dependencies using one main dependency/package management file (package.json) and have a single lock file (yarn.lock).&lt;/p&gt;

&lt;p&gt;I've used &lt;a href="https://esbuild.github.io/" rel="noopener noreferrer"&gt;esbuild&lt;/a&gt; for the build process. If you look inside each package, you'll find a file named &lt;code&gt;esbuild.js&lt;/code&gt;. This file outlines how the application is built. Additionally, the &lt;code&gt;package.json&lt;/code&gt; for each package contains the specified build script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;esbuild&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fg&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fast-glob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;


&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;define&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;// For optional ENVs&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`process.env.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buildNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/*.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node16&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;outdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./build&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourcemap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;logLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;buildNode&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've also used &lt;a href="https://turbo.build/" rel="noopener noreferrer"&gt;turbo-repo&lt;/a&gt; to aid in the build process.&lt;/p&gt;

&lt;p&gt;There are many tools out there to assist with this, but our main focus isn't to discuss the details of setting up a monorepo or how esbuild operates. We simply aim to transpile our TypeScript code to JavaScript so it can be used as a Lambda@Edge function.&lt;/p&gt;

&lt;h4&gt;
  
  
  Prerendering: The Business logic
&lt;/h4&gt;

&lt;p&gt;Let's dive into the most crucial part of our discussion.&lt;/p&gt;

&lt;p&gt;Imagine the scenario: our website has dynamic content, but search engines prefer static HTML for indexing. Our goal is to serve static HTML, rendered from our dynamic site, to these search engines.&lt;/p&gt;

&lt;p&gt;To visualize our workflow, refer to the previously mentioned diagram:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbizkt.imgix.net%2Fposts%2Fedge-functions%2Fedgefunction-naming.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbizkt.imgix.net%2Fposts%2Fedge-functions%2Fedgefunction-naming.png" alt="crawler requests" width="800" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The process kicks off when our CloudFront Distribution receives a request. We need to distinguish whether this request is from a search engine crawler or a regular user.&lt;/p&gt;

&lt;h5&gt;
  
  
  Step 1: Filtering Requests with Lambda@Edge | Filter Function
&lt;/h5&gt;

&lt;p&gt;We'll employ a Lambda@Edge function to introduce a header, allowing us to later distinguish and appropriately handle requests during the CloudFront request flow.&lt;/p&gt;

&lt;p&gt;Typescript handler implementation for this is not that complex 😄&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// edge-functions/packages/filter-function/src/index.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CloudFrontRequestEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CloudFrontRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;

  &lt;span class="c1"&gt;// Check if the request:&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Does not match any of the recognized file extensions&lt;/span&gt;
  &lt;span class="c1"&gt;// 2. Is from a recognized bot user agent&lt;/span&gt;
  &lt;span class="c1"&gt;// 3. Does not already have an x-prerender header&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;IS_FILE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;IS_BOT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-prerender&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Set x-request-prerender header to inform origin-request Lambda function&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-request-prerender&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-request-prerender&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;// Set x-prerender-host header to the host of the request&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-prerender-host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Prerender-Host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Step 2: Requesting the Prerender Service | Prerender Proxy
&lt;/h5&gt;

&lt;p&gt;Having filtered requests coming from crawlers, our next step is to ask the Prerender service to render the appropriate web page for us. This is a straightforward process: provide the webpage address and the Prerender-token from the &lt;code&gt;prerender.io&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CloudFrontRequestEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CloudFrontResponse&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;CloudFrontRequest&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;

  &lt;span class="c1"&gt;// If the request has the x-request-prerender header, it means the viewer-request function determined it should be prerendered&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-request-prerender&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// CloudFront alters requests for the root path to the default root object, /index.html.&lt;/span&gt;
    &lt;span class="c1"&gt;// However, when prerendering the homepage, this behavior is not desired.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PATH_PREFIX&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/index.html`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PATH_PREFIX&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Modify the request's origin to be the prerender service&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PRERENDER_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;readTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;keepaliveTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sslProtocols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TLSv1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TLSv1.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TLSv1.2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/https%3A%2F%2F&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-prerender-host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;customHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-prerender-token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-prerender-token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PRERENDER_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once invoked, the Lambda@Edge function requests the Prerender service to render our website. Then Prerender service, in turn, sends a new request to our CloudFront distribution, captures the webpage (with the dynamic content), render it, and returns it as a static HTML page.&lt;/p&gt;

&lt;h6&gt;
  
  
  Handling Errors
&lt;/h6&gt;

&lt;p&gt;What happens if the page isn't available? Typically, we would present a default 404 page (the HTTP status code could be 404 or 2XX). However, the Prerender service, allows us to specify a status code by embedding it within the error page's metadata:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;meta name="prerender-status-code" content="404"&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You can read more about this &lt;a href="https://docs.prerender.io/docs/11-best-practices" rel="noopener noreferrer"&gt;here&lt;/a&gt;.  &lt;/p&gt;

&lt;h5&gt;
  
  
  Step 3: Responding with Static HTML | Response Handler
&lt;/h5&gt;

&lt;p&gt;Once the Prerender service completes its task and sends back the static HTML, our third Lambda@Edge function formats the response.&lt;/p&gt;

&lt;p&gt;As part of this process, we also set cache control headers to dictate how the returned responses should be cached.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// edge-functions/packages/response-handler/src/index.ts&lt;/span&gt;

&lt;span class="c1"&gt;// Create an Axios client instance for HTTP requests.&lt;/span&gt;
&lt;span class="c1"&gt;// This instance is defined outside the Lambda function for reuse between calls.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Set request timeout&lt;/span&gt;
  &lt;span class="na"&gt;maxRedirects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Disable following redirects&lt;/span&gt;
  &lt;span class="na"&gt;validateStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Only consider HTTP 200 as a valid response&lt;/span&gt;
  &lt;span class="na"&gt;httpsAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;keepAlive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="c1"&gt;// Use a keep-alive HTTPS agent&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CloudFrontResponseEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CloudFrontResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;cf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;

  &lt;span class="c1"&gt;// If the x-Prerender-requestid header is present, set cache-control headers.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`max-age=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cacheMaxAge&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// If the response status isn't 200 (OK), fetch and set a custom error page.&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;200&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errorPageUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;
      &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="c1"&gt;// Remove any pre-existing content-length headers as they might contain values from the origin.&lt;/span&gt;
      &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-length&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// If fetching the custom error page fails, return the original response.&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's crucial to understand that regardless of the origin/source (S3 or Prerender), if the response isn't a 200 status code, here we provide our own custom error page. This page is fetched on-the-fly by Axios (the web-client), which sends a request to our website's 404 page. If you are following along with my previous article, I have get rid of the custom error page configuration from the CloudFront. See &lt;a href="https://github.com/krishanthisera/aws-static-hosting/commit/be22273ccbf8c3180e04108b705415d93a16d2fb?diff=unified" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping it UP
&lt;/h2&gt;

&lt;p&gt;We've discussed how to use AWS CloudFront edge functions and S3 for our static hosting needs.  Our main goal? Boosting our site's SEO prowess. We broke down how web crawlers work, comparing it to the usual browser requests. Digging deeper, we discuss the foundation of our solution. In short, this article offers a roadmap for those wanting to optimize their static sites using AWS tools.&lt;/p&gt;

</description>
      <category>statichosting</category>
      <category>edgefunctions</category>
      <category>devops</category>
      <category>aws</category>
    </item>
    <item>
      <title>AWS Static Hosting - Part 01: CloudFront, S3 and Terraform</title>
      <dc:creator>Krishan Thisera</dc:creator>
      <pubDate>Sun, 30 Jul 2023 03:22:02 +0000</pubDate>
      <link>https://dev.to/krishanthisera/host-a-static-website-in-aws-using-cloudfront-s3-and-terraform-hgg</link>
      <guid>https://dev.to/krishanthisera/host-a-static-website-in-aws-using-cloudfront-s3-and-terraform-hgg</guid>
      <description>&lt;p&gt;Depending on your requirement, There are many ways to host a website in a cloud environment. And tons of frameworks at you r disposal to get the website up and running. Here in this article, we will focus on how we can leverage AWS CloudFront and S3 to set up our static hosting infrastructure.  &lt;/p&gt;

&lt;p&gt;I am planning to discuss the scenario using two articles. In this article,  we will focus primarily on infrastructure setup, and the second article would be dedicated to enhancing SEO, using Lambda at Edge functions.&lt;/p&gt;

&lt;p&gt;The source code for this article is available &lt;a href="https://github.com/krishanthisera/aws-static-hosting/tree/aws-static-hosting-v1" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you begin
&lt;/h2&gt;

&lt;p&gt;I wanted to emphasize the following, If you come from an agile background (who ain't eh?) say these are our user stories&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We want the website to host our static content

&lt;ul&gt;
&lt;li&gt;The content should be securely stored in an S3 bucket (no public access to the bucket)&lt;/li&gt;
&lt;li&gt;The traffic to the site should be secured (HTTPS)&lt;/li&gt;
&lt;li&gt;Leverage CloudFront to deliver the static content from the bucket&lt;/li&gt;
&lt;li&gt;Leverage CloudFront edge functions to support multi-page routing&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The website has its own repository and a release cycle

&lt;ul&gt;
&lt;li&gt;Provision an IAM user to Deploy the content to the S3 bucket and invalidate the CloudFront cache&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu7opr47692to6uq8myiu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu7opr47692to6uq8myiu.png" alt="static hosting" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding CSR and SSR
&lt;/h3&gt;

&lt;p&gt;Before we jump into our Infrastructure setup, it's crucial to understand the two primary rendering methods for web applications:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client-Side Rendering (CSR)&lt;/li&gt;
&lt;li&gt;Server-Side Rendering (SSR).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Client-Side Rendering (CSR):&lt;/strong&gt; In CSR, the browser is responsible for rendering the content. When a user accesses the website, they receive a minimal HTML file. The browser then fetches the JavaScript, which, when executed, populates the page with content. This method is popular with Single Page Applications (SPAs) and offers a smooth user experience, especially for dynamic content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-Side Rendering (SSR):&lt;/strong&gt; With SSR, the server processes the request, renders the full page, and then sends the complete HTML content to the browser. This method is beneficial for SEO as search engines can crawl the content directly without executing JavaScript. It also provides a faster initial page load.&lt;/p&gt;

&lt;h4&gt;
  
  
  Factors to Consider When Choosing Between CSR and SSR
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SEO Needs:&lt;/strong&gt; If SEO is a priority, SSR might be more suitable as it ensures that search engines can easily index your content. &lt;em&gt;A teaser 💡: In our second article, we will discuss how we can address this very issue.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance:&lt;/strong&gt; CSR might introduce a slight delay before the user sees the content, as the browser needs to download and execute the JavaScript. On the other hand, SSR provides content instantly but might be resource-intensive on the server side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development Complexity:&lt;/strong&gt; CSR-based SPAs can be simpler to develop and deploy, especially when using modern frameworks. SSR might introduce additional complexities, especially when dealing with caching, state management, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User Experience:&lt;/strong&gt; For dynamic applications where content changes frequently based on user interactions, CSR can offer a more fluid experience.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Throughout this series, our primary focus is on applications that utilize Client-Side Rendering (CSR).&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting things up
&lt;/h2&gt;

&lt;p&gt;Before we start we shall set up our terraform environment. In Terraform it is vital to maintain your state file secure. I personally prefer Terraform Cloud as the configuration is straightforward and we don't need to worry about CI/CD (GitOps-like) setup.  &lt;/p&gt;

&lt;p&gt;First, you will need to a create Terraform cloud account &lt;a href="https://app.terraform.io/public/signup/account" rel="noopener noreferrer"&gt;here&lt;/a&gt; (its free). Then create a project and set up a workflow, at this point, you would need to have a GitHub account (or from any VCS provider) to maintain your infrastructure code. I am not going to show-how this, as this is very straightforward.&lt;/p&gt;

&lt;p&gt;If you have set up your Terraform workspace correctly, you should be able to see a webhook configured in your GitHub account. If you are using a VCS other than GitHub you would need to set this webhook manually.  &lt;/p&gt;

&lt;p&gt;You can follow &lt;a href="https://developer.hashicorp.com/terraform/language/settings/backends/remote#example-configurations" rel="noopener noreferrer"&gt;this&lt;/a&gt; to set up the backend.&lt;/p&gt;

&lt;p&gt;If you refer to the GitHub repo, &lt;a href="https://github.com/krishanthisera/aws-static-hosting/blob/main/config.remote.tfbackend" rel="noopener noreferrer"&gt;config.remote.tfbackend&lt;/a&gt; describe my remote backend configuration. In this case, I am using a CLI input to configure the backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init &lt;span class="nt"&gt;-backend-config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;config.remote.tfbackend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's have a quick peek into our provider's configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# provider.tf&lt;/span&gt;
&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.5.0"&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.7"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"remote"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we have set up our remote backend and conquered the provider configuration we can start writing our code.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3 Buckets
&lt;/h2&gt;

&lt;p&gt;For this section please refer to the &lt;a href="https://github.com/krishanthisera/aws-static-hosting/blob/main/s3.tf" rel="noopener noreferrer"&gt;s3.tf&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# S3 bucket for website.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"blog_assets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_name&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;common_tags&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# S3 Bucket Policy Association&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_policy"&lt;/span&gt; &lt;span class="s2"&gt;"assets_bucket_cloudfront_policy_association"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blog_assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_bucket_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# S3 bucket website configuration &lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_website_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"assets_bucket_website"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blog_assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;index_document&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;suffix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"index.html"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;error_document&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"404.html"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# S3 bucket ACL&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_acl"&lt;/span&gt; &lt;span class="s2"&gt;"assets_bucket_acl"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blog_assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;acl&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_bucket_ownership_controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets_bucket_acl_ownership&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# S3 bucket CORS configuration&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_cors_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"assets_bucket_cors"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blog_assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cors_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_headers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Content-Length"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_methods&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_origins&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://www.${var.domain_name}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"https://${var.domain_name}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;max_age_seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Set Bucket Object ownership&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_ownership_controls"&lt;/span&gt; &lt;span class="s2"&gt;"assets_bucket_acl_ownership"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blog_assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;object_ownership&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"BucketOwnerPreferred"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_bucket_public_access_block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets_bucket_public_access&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Block public access to the bucket&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_public_access_block"&lt;/span&gt; &lt;span class="s2"&gt;"assets_bucket_public_access"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blog_assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_acls&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_policy&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;ignore_public_acls&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;restrict_public_buckets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a couple of things to note here,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 bucket website configuration:&lt;/strong&gt; here we set the paths to our index document and error document. You can even pull this path out for and defined them as a variable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 bucket CORS configuration:&lt;/strong&gt; CORS configuration is a pretty generic one for our scenario.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block public access to the bucket:&lt;/strong&gt; we don't want our bucket objects to be publicly available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set Bucket Object ownership:&lt;/strong&gt; To avoid complications, we set the object ownership to bucket owner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 bucket policy:&lt;/strong&gt; here we are referring to the S3 bucket policy, which has been specified in data.tf.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's take a look at the policy document. See &lt;a href="https://github.com/krishanthisera/aws-static-hosting/blob/main/data.tf" rel="noopener noreferrer"&gt;data.tf&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# S3 Bucket Policy to Associate with the S3 Bucket&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"s3_bucket_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

  &lt;span class="c1"&gt;# Deployer User access to S3 bucket&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DeployerUser"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pipeline_deployment_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:s3:::${var.bucket_name}"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# CloudFront access to S3 bucket&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CloudFront"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cloudfront.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:SourceArn"&lt;/span&gt;

      &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"${aws_cloudfront_distribution.blog_distribution.arn}"&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:s3:::${var.bucket_name}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:s3:::${var.bucket_name}/*"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are specifying two different statements.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;To let the pipeline deployer user to list the bucket&lt;/li&gt;
&lt;li&gt;CloudFront service to read the S3 bucket content&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It is important to note that we will be using Origin Access Control (OAC) instead of legacy OAI (Origin Access Identity) to provide access to S3 bucket content to the CloudFront.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cloudfront.tf&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudfront_origin_access_control"&lt;/span&gt; &lt;span class="s2"&gt;"blog_distribution_origin_access"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blog_distribution_origin_access"&lt;/span&gt;
  &lt;span class="nx"&gt;origin_access_control_origin_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt;
  &lt;span class="nx"&gt;signing_behavior&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"always"&lt;/span&gt;
  &lt;span class="nx"&gt;signing_protocol&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sigv4"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://github.com/krishanthisera/aws-static-hosting/blob/main/cloudfront.tf" rel="noopener noreferrer"&gt;CloudFront&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;For this article, we shall keep the CloudFront configuration to a minimum. Let's discover more when we configure the Lambda at edge functions.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  aws_cloudfront_distribution
&lt;/h3&gt;

&lt;p&gt;Here we specify the origin configurations, as most of them are self-explanatory I am not planning to go and explain each one of them. But I think we might need an explanation for SSL certification configuration and function association.&lt;/p&gt;

&lt;h4&gt;
  
  
  SSL configuration
&lt;/h4&gt;

&lt;p&gt;If we refer to the repository, my certificate set up is sort of a "Bring your own certificate", but I have put together a configuration for Email or DNS challenge validation.&lt;/p&gt;

&lt;p&gt;Please refer to &lt;a href="https://github.com/krishanthisera/aws-static-hosting/blob/main/acm.tf" rel="noopener noreferrer"&gt;acm.tf&lt;/a&gt;. For Email validation use the following.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SSL Certificate&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_acm_certificate"&lt;/span&gt; &lt;span class="s2"&gt;"ssl_certificate"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acm_provider&lt;/span&gt;
  &lt;span class="nx"&gt;domain_name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain_name&lt;/span&gt;
  &lt;span class="nx"&gt;subject_alternative_names&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*.${var.domain_name}"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;validation_method&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EMAIL"&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;common_tags&lt;/span&gt;

  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;create_before_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Function association
&lt;/h4&gt;

&lt;p&gt;If you see &lt;a href="https://github.com/krishanthisera/aws-static-hosting/blob/main/src/astro.js" rel="noopener noreferrer"&gt;src/astro.js&lt;/a&gt;, I have a little edge function there and it is self-explanatory. A little confession, my blog is using the Astro framework, so I grab the code to the edge function from the documentation &lt;a href="https://docs.astro.build/en/guides/deploy/aws/#cloudfront-functions-setup" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/astro.js&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Check whether the URI is missing a file name.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Check whether the URI is missing a file extension.&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Edge Functions&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudfront_function"&lt;/span&gt; &lt;span class="s2"&gt;"astro_default_edge_function"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default_edge_function"&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cloudfront-js-1.0"&lt;/span&gt;
  &lt;span class="nx"&gt;comment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CloudFront Functions for Astro"&lt;/span&gt;
  &lt;span class="nx"&gt;publish&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;code&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"src/astro.js"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;a href="https://github.com/krishanthisera/aws-static-hosting/blob/main/iam.tf" rel="noopener noreferrer"&gt;IAM&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;During the deployment, the deployer user would need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Push build artifacts to the S3 bucket&lt;/li&gt;
&lt;li&gt;Do the CloudFront invalidation
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you skim through the file, you would get a good understanding of how this has been set up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# IAM Policy for put S3 objects&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"allow_s3_put_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow_aws_s3_put"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow Pipeline Deployment to put objects in S3"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow_aws_s3_put&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# IAM policy for cloudfront to invalidate cache&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"allow_cloudfront_invalidations_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow_cloudfront_invalidate"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow pipeline user to create CloudFront invalidation"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow_cloudfront_invalidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# IAM User group for Pipeline Deployment&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_group"&lt;/span&gt; &lt;span class="s2"&gt;"pipeline_deployment_group"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.domain_name}_deployment_group"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# IAM Policy attachment for Pipeline Deployment - S3 PUT&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_group_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"s3_put_group_policy_attachment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;group&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pipeline_deployment_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow_s3_put_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# IAM Policy attachment for Pipeline Deployment - CloudFront Invalidation&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_group_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"cloudfront_invalidation_group_policy_attachment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;group&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pipeline_deployment_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow_cloudfront_invalidations_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# IAM User for Pipeline Deployment&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_user"&lt;/span&gt; &lt;span class="s2"&gt;"pipeline_deployment_user"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.domain_name}_deployer"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# IAM User group membership for Pipeline Deployment&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_group_membership"&lt;/span&gt; &lt;span class="s2"&gt;"deployment_group_membership"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pipeline_deployment_group_membership"&lt;/span&gt;
  &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;aws_iam_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pipeline_deployment_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pipeline_deployment_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We shall have two policy documents for each use case mentioned earlier,&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;IAM policy for CloudFront to invalidate the cache
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# data.tf: IAM policy for CloudFront to invalidate cache&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"allow_cloudfront_invalidate"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowCloudFrontInvalidation"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"cloudfront:CreateInvalidation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"cloudfront:GetInvalidation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"cloudfront:ListInvalidations"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"${aws_cloudfront_distribution.blog_distribution.arn}"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;IAM Policy for managing S3 objects
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# data.tf: IAM Policy for put S3 objects&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"allow_aws_s3_put"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowS3Put"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;

    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:GetObject*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:GetBucket*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:List*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:DeleteObject*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:PutObjectLegalHold"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:PutObjectRetention"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:PutObjectTagging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:PutObjectVersionTagging"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:Abort*"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"arn:aws:s3:::${var.bucket_name}/*"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As they are pretty generic, I am not going dig deep, but &lt;a href="https://github.com/krishanthisera/aws-static-hosting/blob/main/iam.tf" rel="noopener noreferrer"&gt;here&lt;/a&gt; we are attaching those two policies, to the deployer group call &lt;code&gt;{var.domain_name}_deployment_group&lt;/code&gt; and then create a user called &lt;code&gt;${var.domain_name}_deployer&lt;/code&gt; and add it to the group.&lt;/p&gt;

&lt;h2&gt;
  
  
  Variables
&lt;/h2&gt;

&lt;p&gt;There are a couple of variables I have been using, In my case, I use &lt;a href="https://github.com/krishanthisera/aws-static-hosting/blob/main/terraform.tfvars" rel="noopener noreferrer"&gt;terraform.tfvars&lt;/a&gt; file to set the bucket name and the domain name. As I am not super comfortable with sharing my AWS account ID, I have copied the certificate ARN from a previously created certificate and save it against  &lt;code&gt;TF_VAR_ssl_certificate_arn&lt;/code&gt;.  You can configure these variables in Terraform Cloud.  &lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Don't forget to set your AWS access key pair.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6zxmemix78kdvhcr324n.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6zxmemix78kdvhcr324n.jpg" alt="Terraform Vars" width="798" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Depending on your configuration, you may either use CLI to apply the Terraform plan or, execute the plan using Terraform cloud.&lt;/p&gt;

&lt;p&gt;Once, you've deployed the environment, you may need to manually create the IAM key pair using AWS Console, and use it in your CI/CD pipeline&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article we discussed setting up static hosting on AWS using CloudFront, S3, and Terraform. We covered essential steps from configuring S3 buckets to setting up CloudFront distributions and managing IAM roles for security. When you set up AWS static hosting, using this systematic guide will help make your infrastructure reliable, safe, and adaptable.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>statichosting</category>
      <category>aws</category>
      <category>infrastructureascode</category>
    </item>
    <item>
      <title>Why should you consider to adopt Service Mesh</title>
      <dc:creator>Krishan Thisera</dc:creator>
      <pubDate>Sun, 30 Jul 2023 03:11:41 +0000</pubDate>
      <link>https://dev.to/krishanthisera/why-should-you-consider-to-adopt-service-mesh-125m</link>
      <guid>https://dev.to/krishanthisera/why-should-you-consider-to-adopt-service-mesh-125m</guid>
      <description>&lt;p&gt;This is a brief overview of why you should consider adopting Service Mesh with your existing Kubernetes cluster.&lt;br&gt;
I'm not trying to demystify the concepts regards to Service Mesh but here I am trying to pitch some basic considerations you may take into account.&lt;br&gt;
I should mention that if you are already an expert on the domain you can skip this. Especially, for this article, I will refer ISTIO.&lt;/p&gt;

&lt;p&gt;Service Mesh, in terms of Kubernetes, is a relatively new concept in which I found it as a set of tools that enhance the capabilities of the Kubernetes ecosystem.&lt;/p&gt;

&lt;p&gt;In this case, what are the enhancement that can be done to your existing Kubernetes architecture? Here I am not referring to your blog which is hosted in a Kubernetes&lt;br&gt;
 cluster with a couple of containers but how about an infrastructure where it runs dozens of containers encapsulated in dozens of Kubernetes PODs or let's call them microservices?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you manage the logs and traces?&lt;/li&gt;
&lt;li&gt;How do you manage the routes?&lt;/li&gt;
&lt;li&gt;How do you do the Load Balancing?&lt;/li&gt;
&lt;li&gt;How to make those microservices resilient?&lt;/li&gt;
&lt;li&gt;How do you handle the security and etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Likewise, I could list down more than a handful of concerns regards to the matter which means that Kubernetes does not solve all the problems. I am not trying to emphasise that,&lt;br&gt;
Service Mesh is the silver bullet to all problems but depending on the context your traditional Kubernetes implementation may no longer accommodate your requirement.&lt;/p&gt;

&lt;p&gt;Let me briefly go through the standard architecture of Service Mesh.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnitlchvsjcik4x93lc4l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnitlchvsjcik4x93lc4l.png" alt="mesh" width="741" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Please note that this is a very high-level diagram. The Control plane may contain more than one component.&lt;/p&gt;

&lt;p&gt;Generally, those sets of components in the service mesh can be categorized into control plane components and data plane components. As it is obvious, the control plane manages the service mesh while the data plane carries the actual workload of our microservices.&lt;/p&gt;

&lt;p&gt;It is important to note that, Service Mesh is a combination of technologies. For example, for ISTIO Service Mesh, the envoy from "Lyft" has been used as their native sidecar proxy and you may use Kiali as your Service Dashboard. Further, Prometheus is a dependency for Kiali and other tools, which are meant to achieve observability.&lt;/p&gt;

&lt;p&gt;In the context of the Service Mesh, microservices communicate to their subordinate microservices via a proxy. Especially, the proxy may run as a side container parallel to the original workload. The key point here is that, by having a sidecar proxy, Service Mesh addresses most of the concerns previously mentioned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telemetry
&lt;/h2&gt;

&lt;p&gt;One of the main reasons to associate Service Mesh with your Kubernetes cluster is to enhance observability.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitoring the services at a more concrete level&lt;/li&gt;
&lt;li&gt;Trace the traffic flows in a distributed manner&lt;/li&gt;
&lt;li&gt;Identify the services which cause the delays&lt;/li&gt;
&lt;li&gt;Oversees the service connectivity&lt;/li&gt;
&lt;li&gt;As mentioned, if you are using a service mesh, traffic between your microservice should flow through the respective sidecar proxy. Hence, it enables the control plane to collect the metrics regards to the traffic from the proxy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In terms of ISTIO, ISTIO uses Envoy as its native sidecar proxy. More importantly, the level of abstraction provided by ISTIO in which you are not required to directly configure those proxies.&lt;br&gt;
ISTIO provides a handy set of Customer Resource Definition(CRD) to configure them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kiali:&lt;/strong&gt;  can be used to inspect the connectivity between the services, and It's powerful that you can tune the traffic flow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc80ccf0y3rxeours0zhq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc80ccf0y3rxeours0zhq.png" alt="kiali" width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jaeger:&lt;/strong&gt; I would recall as my second favorite tool can be used to inspect the traces.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F883ftelalo70hpynaqud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F883ftelalo70hpynaqud.png" alt="jaeger" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prometheus:&lt;/strong&gt; One of the most popular metric scraper&lt;br&gt;
&lt;strong&gt;Grafana:&lt;/strong&gt; As your dashboard&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftcrcz0onq1j03zf6vg76.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftcrcz0onq1j03zf6vg76.png" alt="grafana" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing
&lt;/h2&gt;

&lt;p&gt;I will summarise a couple of routing features that I have been using in production,&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canaries:&lt;/strong&gt; Canary releases or weighted routings are one of the most popular routing scenarios in the context of microservices.&lt;br&gt;
Assume that you have a service with multiple versions, If you ought to split the traffic between those versions depending on the percentage, those are canaries.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Circuit Breakers:&lt;/strong&gt; Say that, you have service A, B, and C, where A depend on B and B, depending on C, in a scenario where service C take too much time to respond,&lt;br&gt;
 both A and service B will be affected. In this case, you may configure a Circuit breaker with a time-out that returns an acceptable response.&lt;br&gt;
 ISTIO uses Circuit Breaking and Outlier Detection interchangeably.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Hidden Release:&lt;/strong&gt; Have you ever tried testing in production? Say that you have a service with two versions. Version-1 is the production version and Version-2 is&lt;br&gt;
 the newer version which is in the testing phase. You may deploy both versions on your mesh and use custom HTTP headers to direct the traffic to Version-2.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;mTLS:&lt;/strong&gt; The bonus feature which comes with ISTIO and other service meshes. You are no longer required to maintain SSL connectivities at the source code level.&lt;br&gt;
Envoy proxy automatically does that for you. Say that you have 5-nodes Amazon EKS (Elastic Kubernetes Service). If you have multiple services (interconnect with each other),&lt;br&gt;
your services may not be in the same node and you will never know the underlying physical connectivity between the nodes. Sometimes the traffic may flow through&lt;br&gt;
different physical equipment (Switches. Routers, Servers) in the AWS datacentre.&lt;br&gt;
By using SSL connectivity between proxies, service mesh secure your traffic without giving you any burden.&lt;br&gt;
&lt;strong&gt;Fault Injection:&lt;/strong&gt;  If you are required to test your application with a scenario where a service got crashed or was poorly reachable,&lt;br&gt;
you can assign the service a delay response at the proxy level and observe the behaviour. This feature is much useful when you are executing chaos testing.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Traffic Mirroring:&lt;/strong&gt; You can easily mirror the traffic from a particular service to another service.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Create your own ISTIO Playground
&lt;/h2&gt;

&lt;p&gt;Lastly, for the time being, ISTIO document has some vague areas where those sections are not that clear. For example, ISTIO ingress with SSL certificates.&lt;br&gt;
The following GIT repository will walk you through ISTIO implementation with Cert-Manager.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/krishanthisera/istio-certman-poc" rel="noopener noreferrer"&gt;ISTIO with cert-manager&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that, this is a POC that I have designed and only describes the steps that you may follow to implement ISTIO with Cert-Manager.&lt;br&gt;
It is heavily recommended to follow the official documentation for both ISTIO and Cert-Manger for better understanding.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>istio</category>
      <category>servicemesh</category>
      <category>microservices</category>
    </item>
  </channel>
</rss>
