<?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>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="799" height="185"&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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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%2Cformat=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"&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="768" height="442"&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>
