<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[_CLOUD]]></title><description><![CDATA[Cloud Native Pilgrim | Kubernetes Enthusiast | Serverless Believer | Customer Experience Architect @ Pulumi | (he/him) | blogging my own opinions]]></description><link>https://blog.ediri.io</link><generator>RSS for Node</generator><lastBuildDate>Tue, 21 Apr 2026 03:42:56 GMT</lastBuildDate><atom:link href="https://blog.ediri.io/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Kubernetes GPU Sharing: NVIDIA MIG + DRA on Amazon EKS]]></title><description><![CDATA[GPU scheduling in Kubernetes has always felt like buying a mansion when you need a studio apartment. A small inference workload that needs 2GB of GPU memory gets scheduled on an entire 80GB A100, and there's nothing you can do about it. The device pl...]]></description><link>https://blog.ediri.io/kubernetes-gpu-sharing-nvidia-mig-dra-on-amazon-eks</link><guid isPermaLink="true">https://blog.ediri.io/kubernetes-gpu-sharing-nvidia-mig-dra-on-amazon-eks</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[GPU]]></category><category><![CDATA[AI]]></category><category><![CDATA[Machine Learning]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sun, 01 Feb 2026 15:37:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769959987486/2ced2c42-5017-4491-811e-1723e51c1d44.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>GPU scheduling in Kubernetes has always felt like buying a mansion when you need a studio apartment. A small inference workload that needs 2GB of GPU memory gets scheduled on an entire 80GB A100, and there's nothing you can do about it. The <a target="_blank" href="https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/">device plugin model</a> that Kubernetes introduced back in version 1.8 treats GPUs as indivisible units: one pod, one GPU, no exceptions. For years, platform teams have watched expensive hardware sit idle while data scientists wait in queue for their turn.</p>
<p>NVIDIA tried to address this with <a target="_blank" href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/">Multi-Instance GPU (MIG)</a> technology, which can physically partition an A100 or H100 into up to seven isolated instances. But the Kubernetes scheduler didn't understand MIG. You could partition the hardware, but getting workloads onto those partitions required awkward workarounds and custom tooling.</p>
<p>That changed with Kubernetes 1.34, where Dynamic Resource Allocation (DRA) went GA. DRA replaces the "give me one GPU" model with structured, attribute-based requests. You can now ask for "a 10GB MIG partition with at least 1/7th compute" and the scheduler knows what to do with that request. Device plugins just count GPUs. DRA actually knows what's inside them.</p>
<p>This post walks through building GPU infrastructure on Amazon EKS with Pulumi that puts DRA and MIG to work. We'll set up a cluster with GPU node groups that partition A100 GPUs into different-sized slices, then deploy mixed workloads (training jobs and inference services) that share a single GPU with full hardware isolation. Along the way, we'll cover real-world implementation challenges including GPU compatibility, Spot capacity constraints, AL2023 AMI configuration, and MIG strategy pitfalls.</p>
<h2 id="heading-how-dra-works">How DRA works</h2>
<h3 id="heading-the-problem-with-device-plugins">The problem with device plugins</h3>
<p>The device plugin API was added to Kubernetes in version 1.8 to handle hardware like GPUs. It works, but it has a fundamental limitation: devices are opaque integers. When you write this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">resources:</span>
  <span class="hljs-attr">limits:</span>
    <span class="hljs-attr">nvidia.com/gpu:</span> <span class="hljs-number">1</span>
</code></pre>
<p>Kubernetes sees "1" and nothing else. It doesn't know what kind of GPU, how much memory it has, whether it supports MIG, or what interconnects it has. The scheduler counts GPUs the same way it counts CPU millicores - as fungible units. This works fine when all your GPUs are identical and you always need a whole one. It falls apart when you have mixed GPU types, need partial GPUs, or want topology-aware placement.</p>
<p>Device plugins also can't express relationships between devices. If you need two GPUs connected via NVLink for a training job, there's no way to say that. You get two GPUs, and you hope they're connected.</p>
<h3 id="heading-how-dra-changes-the-model">How DRA changes the model</h3>
<p><a target="_blank" href="https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/">DRA</a> flips the model. Instead of counting GPUs like CPU cores, the scheduler now sees what each device actually is. A DRA driver publishes memory size, compute capability, MIG profiles, interconnect topology. The scheduler matches workloads to devices based on real requirements, not just "give me one."</p>
<p>Here's what the components look like:</p>
<p><strong>ResourceSlice</strong> - The DRA driver on each node publishes ResourceSlices to advertise what's available. Here's what one looks like from a running cluster with MIG-partitioned A100s:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">resource.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ResourceSlice</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">ip-10-0-170-169.ec2.internal-gpu.nvidia.com-qbgxv</span>
  <span class="hljs-attr">ownerReferences:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
    <span class="hljs-attr">kind:</span> <span class="hljs-string">Node</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">ip-10-0-170-169.ec2.internal</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">devices:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">gpu-0-mig-19-2-1</span>
    <span class="hljs-attr">attributes:</span>
      <span class="hljs-attr">architecture:</span>
        <span class="hljs-attr">string:</span> <span class="hljs-string">Ampere</span>
      <span class="hljs-attr">brand:</span>
        <span class="hljs-attr">string:</span> <span class="hljs-string">Nvidia</span>
      <span class="hljs-attr">cudaComputeCapability:</span>
        <span class="hljs-attr">version:</span> <span class="hljs-number">8.0</span><span class="hljs-number">.0</span>
      <span class="hljs-attr">cudaDriverVersion:</span>
        <span class="hljs-attr">version:</span> <span class="hljs-number">13.0</span><span class="hljs-number">.0</span>
      <span class="hljs-attr">driverVersion:</span>
        <span class="hljs-attr">version:</span> <span class="hljs-number">580.126</span><span class="hljs-number">.9</span>
      <span class="hljs-attr">productName:</span>
        <span class="hljs-attr">string:</span> <span class="hljs-string">NVIDIA</span> <span class="hljs-string">A100-SXM4-40GB</span>
      <span class="hljs-attr">profile:</span>
        <span class="hljs-attr">string:</span> <span class="hljs-string">1g.5gb</span>
      <span class="hljs-attr">type:</span>
        <span class="hljs-attr">string:</span> <span class="hljs-string">mig</span>
      <span class="hljs-attr">uuid:</span>
        <span class="hljs-attr">string:</span> <span class="hljs-string">MIG-53146931-c7ac-5a72-878e-15adb49f83ff</span>
    <span class="hljs-attr">capacity:</span>
      <span class="hljs-attr">memory:</span>
        <span class="hljs-attr">value:</span> <span class="hljs-string">4864Mi</span>
      <span class="hljs-attr">multiprocessors:</span>
        <span class="hljs-attr">value:</span> <span class="hljs-string">"14"</span>
</code></pre>
<p>The scheduler sees everything: it's an Ampere A100-SXM4-40GB, partitioned as a <code>1g.5gb</code> MIG instance with 4864Mi memory and 14 multiprocessors. The <code>profile</code> attribute is what you'll match in CEL expressions.</p>
<p><a target="_blank" href="https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/#deviceclass"><strong>DeviceClass</strong></a> - A DeviceClass is like a StorageClass but for devices. It defines a category of hardware that workloads can request. Cluster admins create DeviceClasses to expose different tiers of resources: "give me any GPU" vs "give me a small MIG partition" vs "give me a full A100 for training."</p>
<p>The NVIDIA DRA driver creates a <code>gpu.nvidia.com</code> DeviceClass automatically:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">resource.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">DeviceClass</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">gpu.nvidia.com</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selectors:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">cel:</span>
      <span class="hljs-attr">expression:</span> <span class="hljs-string">device.driver</span> <span class="hljs-string">==</span> <span class="hljs-string">'gpu.nvidia.com'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">device.attributes['gpu.nvidia.com'].type</span> <span class="hljs-string">==</span> <span class="hljs-string">'gpu'</span>
</code></pre>
<p>The <code>selectors</code> field uses CEL expressions to define which devices from ResourceSlices are available through this class. When a ResourceClaim references a DeviceClass, Kubernetes only considers devices that match the class's selectors. The claim can add more selectors to narrow things down further, but it can't escape the class's constraints.</p>
<p>This is where platform teams set guardrails. Create a <code>mig-small.nvidia.com</code> class that only matches <code>1g.5gb</code> profiles, and users requesting from that class can't accidentally (or intentionally) grab a full GPU.</p>
<p><strong>ResourceClaim</strong> - Workloads request devices through ResourceClaims. Unlike the device plugin model where you ask for a count, claims use <a target="_blank" href="https://kubernetes.io/docs/reference/using-api/cel/">CEL (Common Expression Language)</a> to express requirements:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">resource.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ResourceClaim</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">my-inference-gpu</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">devices:</span>
    <span class="hljs-attr">requests:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">gpu</span>
      <span class="hljs-attr">exactly:</span>
        <span class="hljs-attr">deviceClassName:</span> <span class="hljs-string">mig.nvidia.com</span>
        <span class="hljs-attr">count:</span> <span class="hljs-number">1</span>
        <span class="hljs-attr">selectors:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">cel:</span>
            <span class="hljs-attr">expression:</span> <span class="hljs-string">|
              device.attributes["gpu.nvidia.com"].type == "mig" &amp;&amp;
              device.attributes["gpu.nvidia.com"].profile == "1g.5gb"</span>
</code></pre>
<p>The <code>exactly</code> block specifies that the claim needs exactly 1 device matching the criteria. The CEL expression filters for MIG devices with the <code>1g.5gb</code> profile. You can write more complex requirements: "a GPU with Ampere architecture" or "two GPUs on the same PCIe root."</p>
<p>A ResourceClaim exists independently of any pod. You create it once, and multiple pods can reference the same claim to share access to the allocated device. Think shared inference servers where several client pods hit the same GPU. The lifecycle is manual: you create the claim, pods use it, you delete it when done.</p>
<p><strong>ResourceClaimTemplate</strong> - For Deployments and StatefulSets, you typically use templates so each pod gets its own claim:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">resource.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ResourceClaimTemplate</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">inference-gpu-template</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">spec:</span>
    <span class="hljs-attr">devices:</span>
      <span class="hljs-attr">requests:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">gpu</span>
        <span class="hljs-attr">exactly:</span>
          <span class="hljs-attr">deviceClassName:</span> <span class="hljs-string">mig.nvidia.com</span>
          <span class="hljs-attr">count:</span> <span class="hljs-number">1</span>
          <span class="hljs-attr">selectors:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">cel:</span>
              <span class="hljs-attr">expression:</span> <span class="hljs-string">|
                device.attributes["gpu.nvidia.com"].type == "mig" &amp;&amp;
                device.attributes["gpu.nvidia.com"].profile == "1g.5gb"</span>
</code></pre>
<p>The CEL expression matches the <code>type</code> and <code>profile</code> attributes from the ResourceSlice. Checking for <code>type == "mig"</code> ensures you get a MIG partition, not a full GPU.</p>
<p>Unlike a ResourceClaim, a ResourceClaimTemplate doesn't allocate anything by itself. When a pod references the template, Kubernetes generates a new ResourceClaim for that pod. Each replica in a Deployment gets its own claim, its own device. When the pod terminates, Kubernetes cleans up the generated claim.</p>
<p><strong>The short version</strong>: ResourceClaim = shared device, manual lifecycle. ResourceClaimTemplate = dedicated device per pod, automatic lifecycle.</p>
<h3 id="heading-the-allocation-flow">The allocation flow</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769958553290/dbce65a0-e5af-4222-9ce5-a5d06d7fbf28.png" alt="DRA vs Traditional Device Plugin" class="image--center mx-auto" /></p>
<p>When you deploy a pod that references a ResourceClaim, here's what happens:</p>
<ol>
<li><p><strong>Scheduling</strong> - The scheduler reads the claim's requirements and evaluates them against all ResourceSlices in the cluster. It finds nodes where matching devices are available. This is different from device plugins, where the scheduler just checks if <code>nvidia.com/gpu &gt;= 1</code>.</p>
</li>
<li><p><strong>Allocation</strong> - Once a node is selected, the scheduler marks specific devices as allocated in the ResourceClaim's status. The claim now has a binding to actual hardware.</p>
</li>
<li><p><strong>Node preparation</strong> - The kubelet on the selected node calls the DRA driver to prepare the device. For GPUs, this might mean configuring MIG partitions, setting up container device interface (CDI) specs, or configuring CUDA visible devices.</p>
</li>
<li><p><strong>Container startup</strong> - The container runs with the allocated device exposed according to the CDI spec. The workload sees exactly the GPU resources it requested.</p>
</li>
<li><p><strong>Cleanup</strong> - When the pod terminates, the driver releases the allocation. The device goes back into the pool for other workloads.</p>
</li>
</ol>
<p>Notice that allocation happens at scheduling time, not runtime. The scheduler knows exactly which device a pod will get before it even starts. No more race conditions when multiple pods compete for GPUs.</p>
<h2 id="heading-gpu-sharing-strategies">GPU sharing strategies</h2>
<p>Before diving into MIG, it helps to understand <a target="_blank" href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html">what else is out there</a>:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Strategy</td><td>Isolation</td><td>Use case</td><td>Overhead</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Time-slicing</strong></td><td>None - processes share the full GPU</td><td>Development, batch jobs</td><td>Low</td></tr>
<tr>
<td><strong>MPS (Multi-Process Service)</strong></td><td>Memory limits only</td><td>Training jobs with similar workloads</td><td>Medium</td></tr>
<tr>
<td><strong>MIG (Multi-Instance GPU)</strong></td><td>Full hardware isolation</td><td>Production inference, multi-tenant</td><td>Low</td></tr>
</tbody>
</table>
</div><p><strong>Time-slicing</strong> rapidly switches between processes on the same GPU. Good for dev environments, but there's no isolation. One workload can hog all GPU memory and crash everyone else.</p>
<p><strong>MPS</strong> is CUDA-level sharing. Better utilization than time-slicing, but memory isolation is soft limits only. Fine for training jobs that play nice together.</p>
<p><strong>MIG</strong> partitions the GPU at the hardware level. Each partition has its own compute units, memory, and memory bandwidth. If one partition crashes, the others keep running. This is what you want for production and multi-tenant setups.</p>
<h2 id="heading-mig-actual-hardware-partitioning">MIG: actual hardware partitioning</h2>
<p><a target="_blank" href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/">MIG (Multi-Instance GPU)</a> carves supported GPUs into up to seven isolated instances. The isolation runs deep: each instance gets its own processors with separate paths through the entire memory system, including dedicated crossbar ports, L2 cache banks, memory controllers, and DRAM address buses. One workload can't impact another's scheduling or performance. This hardware-level isolation is what makes MIG suitable for multi-tenant environments where you need guaranteed QoS.</p>
<h3 id="heading-how-mig-partitions-the-gpu">How MIG partitions the GPU</h3>
<p>MIG works by slicing the GPU into two types of resources that get combined to create isolated instances:</p>
<p><strong>GPU Instances (GI)</strong> combine memory slices with compute slices to create partitioned GPU resources. A GPU Instance includes a fraction of the GPU's memory, streaming multiprocessors (SMs), and other execution engines. Each GI provides memory Quality of Service with dedicated memory bandwidth and cache allocation.</p>
<p><strong>Compute Instances (CI)</strong> can further subdivide a GPU Instance's SM resources. A Compute Instance contains a subset of the parent GPU instance's SMs while sharing memory and engines. For most inference workloads, you'll use one CI per GI.</p>
<p>Take the A100-40GB: it divides into <strong>8 memory slices</strong> (5GB each) and <strong>7 SM slices</strong>. You combine these to create profiles, but there's a constraint: valid combinations can't overlap vertically in NVIDIA's profile layout diagram.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769958668940/8f31ee0d-22e6-4601-802a-ad058b469551.png" alt class="image--center mx-auto" /></p>
<p>The diagram above shows how the A100 hardware divides. Each vertical column represents a memory slice, and the horizontal bands represent SM slices. When you create a MIG profile, you're selecting which slices to group together.</p>
<h3 id="heading-understanding-mig-profile-names">Understanding MIG profile names</h3>
<p>MIG profiles follow a naming convention: <code>&lt;compute&gt;g.&lt;memory&gt;gb</code></p>
<ul>
<li><p>The first number indicates the fraction of streaming multiprocessors (SMs) allocated</p>
</li>
<li><p>The memory amount follows in gigabytes</p>
</li>
<li><p>For example, <code>1g.5gb</code> means 1/7th of the SMs and 5GB of memory</p>
</li>
</ul>
<p><strong>Profile suffixes</strong> modify capabilities:</p>
<ul>
<li><p><code>+me</code> profiles include at least one media engine (NVDEC, NVENC, NVJPG, or OFA)</p>
</li>
<li><p><code>+gfx</code> adds graphics API support (Blackwell architecture only)</p>
</li>
<li><p><code>+me.all</code> allocates all available media engines</p>
</li>
<li><p><code>-me</code> excludes media engines for compute-only workloads</p>
</li>
</ul>
<p>For inference workloads, you typically use the base profiles without suffixes since you need compute and memory but not video encoding.</p>
<h3 id="heading-available-mig-profiles">Available MIG profiles</h3>
<p>For an A100 40GB GPU (used in p4d.24xlarge instances), the available profiles are:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Profile</td><td>GPU Memory</td><td>Compute Units</td><td>Max instances</td><td>Use case</td></tr>
</thead>
<tbody>
<tr>
<td>1g.5gb</td><td>5 GB</td><td>1/7</td><td>7</td><td>Small inference models, development</td></tr>
<tr>
<td>2g.10gb</td><td>10 GB</td><td>2/7</td><td>3</td><td>Medium models, batch inference</td></tr>
<tr>
<td>3g.20gb</td><td>20 GB</td><td>3/7</td><td>2</td><td>Large models, mixed workloads</td></tr>
<tr>
<td>4g.20gb</td><td>20 GB</td><td>4/7</td><td>1</td><td>Compute-heavy with moderate memory</td></tr>
<tr>
<td>7g.40gb</td><td>40 GB</td><td>7/7</td><td>1</td><td>Full GPU for training or large models</td></tr>
</tbody>
</table>
</div><p>For an A100 80GB GPU, you get larger memory profiles:</p>
<ul>
<li><p><code>1g.10gb</code> - 7 instances available</p>
</li>
<li><p><code>2g.20gb</code> - 3 instances available</p>
</li>
<li><p><code>3g.40gb</code> - 2 instances available</p>
</li>
<li><p><code>4g.40gb</code> - 1 instance available</p>
</li>
<li><p><code>7g.80gb</code> - 1 instance available (full GPU)</p>
</li>
</ul>
<p>For inference, the <code>1g.5gb</code> profile (A100 40GB) or <code>1g.10gb</code> (A100 80GB) works well for small models. One A100 40GB can run seven inference services at once, each with hardware isolation.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769958828952/7fccfdec-c85f-470d-ae69-55f5bb8b039e.png" alt class="image--center mx-auto" /></p>
<p>The diagram shows all valid profile combinations. Notice how larger profiles (like <code>3g.20gb</code> and <code>4g.20gb</code>) can coexist because they don't vertically overlap, but some combinations aren't allowed due to how the memory and compute slices align.</p>
<h3 id="heading-mig-isolation-advantages">MIG isolation advantages</h3>
<p>MIG provides superior isolation compared to CUDA Streams and Multi-Process Service (MPS). Here's what you get that other GPU sharing methods can't provide:</p>
<p>Each partition has dedicated compute units that can't be preempted by other partitions. Memory boundaries are hardware-enforced, so one partition can't access another's memory. Dedicated memory controllers ensure predictable bandwidth per partition. And if one MIG instance crashes, the others keep running.</p>
<p>This is what makes MIG work for production multi-tenant environments where you need guaranteed QoS and security isolation between workloads.</p>
<h3 id="heading-gpu-architecture-support">GPU architecture support</h3>
<p>Here's the catch: <a target="_blank" href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-gpus.html">not all NVIDIA GPUs support MIG</a>. MIG requires compute capability 8.0 or higher, which means Ampere architecture and later. But even within Ampere, not every GPU has it.</p>
<p><strong>Ampere (different max instances):</strong></p>
<ul>
<li><p>A100-SXM4/PCIe (40GB or 80GB): up to 7 instances</p>
</li>
<li><p>A30 (24GB): up to 4 instances</p>
</li>
</ul>
<p><strong>Hopper:</strong></p>
<ul>
<li><p>H100-SXM5/PCIe (80GB or 94GB): up to 7 instances</p>
</li>
<li><p>H200-SXM5/NVL (141GB): up to 7 instances</p>
</li>
<li><p>H20 (96GB): up to 7 instances</p>
</li>
</ul>
<p><strong>Blackwell:</strong></p>
<ul>
<li><p>B200 (180GB): up to 7 instances</p>
</li>
<li><p>GB200 (186GB): up to 7 instances</p>
</li>
</ul>
<p>The A10G (g5 instances) is Ampere but doesn't support MIG. This trips people up. For MIG on AWS, you need p4d (A100) or p5 (H100) instances.</p>
<h2 id="heading-what-you-need">What you need</h2>
<p>The complete code for this example is available on GitHub:</p>
%[INVALID_URL]<p> </p>
<p><strong>Prerequisites:</strong></p>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html">EKS v1.34+</a> (DRA went GA in Kubernetes 1.34)</p>
</li>
<li><p><a target="_blank" href="/docs/iac/download-install/">Pulumi CLI</a> and AWS credentials</p>
</li>
<li><p><a target="_blank" href="/registry/packages/eks/">Pulumi EKS Provider v3+</a></p>
</li>
<li><p><a target="_blank" href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/">NVIDIA GPU Operator v25.10.0+</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/NVIDIA/k8s-dra-driver">NVIDIA DRA Driver v25.8.0+</a></p>
</li>
</ul>
<blockquote>
<p>[!WARNING]<br />p4d.24xlarge capacity is scarce in most AWS regions. Spot looks cheap ($2.48/hr vs $32/hr On-Demand in us-east-1a) but good luck actually getting it. On-Demand is more reliable but expensive.</p>
</blockquote>
<h2 id="heading-building-the-infrastructure">Building the infrastructure</h2>
<p>The stack has a VPC, an EKS cluster with GPU node groups, and the NVIDIA components for DRA. We're using <a target="_blank" href="/registry/packages/awsx/">Pulumi AWSX</a> for the VPC and the <a target="_blank" href="/registry/packages/eks/">Pulumi EKS provider</a> for the cluster. Let's walk through it.</p>
<h3 id="heading-vpc-and-cluster-setup">VPC and cluster setup</h3>
<p>First, create the VPC and EKS cluster:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> pulumi <span class="hljs-keyword">from</span> <span class="hljs-string">"@pulumi/pulumi"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> aws <span class="hljs-keyword">from</span> <span class="hljs-string">"@pulumi/aws"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> awsx <span class="hljs-keyword">from</span> <span class="hljs-string">"@pulumi/awsx"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> eks <span class="hljs-keyword">from</span> <span class="hljs-string">"@pulumi/eks"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> k8s <span class="hljs-keyword">from</span> <span class="hljs-string">"@pulumi/kubernetes"</span>;

<span class="hljs-keyword">const</span> config = <span class="hljs-keyword">new</span> pulumi.Config();
<span class="hljs-keyword">const</span> clusterName = config.get(<span class="hljs-string">"clusterName"</span>) || <span class="hljs-string">"gpu-dra-cluster"</span>;

<span class="hljs-comment">// VPC with required EKS subnet tags</span>
<span class="hljs-keyword">const</span> vpc = <span class="hljs-keyword">new</span> awsx.ec2.Vpc(<span class="hljs-string">"gpu-vpc"</span>, {
    enableDnsHostnames: <span class="hljs-literal">true</span>,
    cidrBlock: <span class="hljs-string">"10.0.0.0/16"</span>,
    subnetSpecs: [
        {
            <span class="hljs-keyword">type</span>: awsx.ec2.SubnetType.Public,
            tags: {
                [<span class="hljs-string">`kubernetes.io/cluster/<span class="hljs-subst">${clusterName}</span>`</span>]: <span class="hljs-string">"shared"</span>,
                <span class="hljs-string">"kubernetes.io/role/elb"</span>: <span class="hljs-string">"1"</span>,
            },
        },
        {
            <span class="hljs-keyword">type</span>: awsx.ec2.SubnetType.Private,
            tags: {
                [<span class="hljs-string">`kubernetes.io/cluster/<span class="hljs-subst">${clusterName}</span>`</span>]: <span class="hljs-string">"shared"</span>,
                <span class="hljs-string">"kubernetes.io/role/internal-elb"</span>: <span class="hljs-string">"1"</span>,
            },
        },
    ],
    subnetStrategy: <span class="hljs-string">"Auto"</span>,
});

<span class="hljs-comment">// EKS cluster without Auto Mode</span>
<span class="hljs-comment">// We'll use managed node groups for system workloads and GPU workloads</span>
<span class="hljs-keyword">const</span> cluster = <span class="hljs-keyword">new</span> eks.Cluster(<span class="hljs-string">"gpu-cluster"</span>, {
    name: clusterName,
    vpcId: vpc.vpcId,
    publicSubnetIds: vpc.publicSubnetIds,
    privateSubnetIds: vpc.privateSubnetIds,
    authenticationMode: eks.AuthenticationMode.Api,
    skipDefaultNodeGroup: <span class="hljs-literal">true</span>,
    version: <span class="hljs-string">"1.34"</span>,
});

<span class="hljs-comment">// Kubernetes provider for deploying resources</span>
<span class="hljs-keyword">const</span> k8sProvider = <span class="hljs-keyword">new</span> k8s.Provider(<span class="hljs-string">"k8s-provider"</span>, {
    kubeconfig: cluster.kubeconfigJson,
    enableServerSideApply: <span class="hljs-literal">true</span>,
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> kubeconfig = pulumi.secret(cluster.kubeconfig);
</code></pre>
<h3 id="heading-creating-node-groups">Creating node groups</h3>
<p>We'll create two node groups: one for system workloads and one for GPU workloads.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// IAM role for managed node groups</span>
<span class="hljs-keyword">const</span> nodeRole = <span class="hljs-keyword">new</span> aws.iam.Role(<span class="hljs-string">"system-node-role"</span>, {
    assumeRolePolicy: <span class="hljs-built_in">JSON</span>.stringify({
        Version: <span class="hljs-string">"2012-10-17"</span>,
        Statement: [{
            Action: <span class="hljs-string">"sts:AssumeRole"</span>,
            Effect: <span class="hljs-string">"Allow"</span>,
            Principal: { Service: <span class="hljs-string">"ec2.amazonaws.com"</span> },
        }],
    }),
});

<span class="hljs-comment">// Attach required policies for EKS worker nodes</span>
<span class="hljs-keyword">const</span> nodeRolePolicies = [
    <span class="hljs-string">"arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"</span>,
    <span class="hljs-string">"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"</span>,
    <span class="hljs-string">"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"</span>,
];

nodeRolePolicies.forEach(<span class="hljs-function">(<span class="hljs-params">policyArn, index</span>) =&gt;</span> {
    <span class="hljs-keyword">new</span> aws.iam.RolePolicyAttachment(<span class="hljs-string">`system-node-policy-<span class="hljs-subst">${index}</span>`</span>, {
        role: nodeRole.name,
        policyArn: policyArn,
    });
});

<span class="hljs-comment">// System node group for control plane workloads</span>
<span class="hljs-keyword">const</span> systemNodeGroup = <span class="hljs-keyword">new</span> eks.ManagedNodeGroup(<span class="hljs-string">"system-nodes"</span>, {
    cluster: cluster,
    nodeGroupName: <span class="hljs-string">"system-nodes"</span>,
    nodeRole: nodeRole,
    instanceTypes: [<span class="hljs-string">"m6i.large"</span>],
    capacityType: <span class="hljs-string">"ON_DEMAND"</span>,
    scalingConfig: {
        desiredSize: <span class="hljs-number">2</span>,
        minSize: <span class="hljs-number">1</span>,
        maxSize: <span class="hljs-number">4</span>,
    },
    labels: { <span class="hljs-string">"node-role"</span>: <span class="hljs-string">"system"</span> },
    taints: [],
}, { dependsOn: [cluster] });

<span class="hljs-comment">// GPU node group with p4d.24xlarge (A100 40GB GPUs)</span>
<span class="hljs-comment">// Note: SPOT capacity for p4d.24xlarge is extremely limited and often unavailable</span>
<span class="hljs-comment">// Using ON_DEMAND for more reliable (though expensive) capacity</span>
<span class="hljs-keyword">const</span> gpuNodeGroup = <span class="hljs-keyword">new</span> eks.ManagedNodeGroup(<span class="hljs-string">"gpu-nodes"</span>, {
    cluster: cluster,
    nodeGroupName: <span class="hljs-string">"gpu-nodes"</span>,
    nodeRole: nodeRole,
    instanceTypes: [<span class="hljs-string">"p4d.24xlarge"</span>],
    capacityType: <span class="hljs-string">"ON_DEMAND"</span>,  <span class="hljs-comment">// SPOT often fails with UnfulfillableCapacity</span>
    scalingConfig: {
        desiredSize: <span class="hljs-number">1</span>,
        minSize: <span class="hljs-number">0</span>,
        maxSize: <span class="hljs-number">2</span>,
    },
    diskSize: <span class="hljs-number">100</span>,
    amiType: <span class="hljs-string">"AL2023_x86_64_NVIDIA"</span>,  <span class="hljs-comment">// EKS-optimized Amazon Linux 2023 with NVIDIA drivers</span>
    labels: {
        <span class="hljs-string">"node-role"</span>: <span class="hljs-string">"gpu"</span>,
        <span class="hljs-string">"nvidia.com/gpu.present"</span>: <span class="hljs-string">"true"</span>,
        <span class="hljs-string">"nvidia.com/mig.config"</span>: <span class="hljs-string">"all-balanced"</span>,  <span class="hljs-comment">// Configure all GPUs with balanced MIG profiles</span>
    },
    taints: [{
        key: <span class="hljs-string">"nvidia.com/gpu"</span>,
        value: <span class="hljs-string">"true"</span>,
        effect: <span class="hljs-string">"NO_SCHEDULE"</span>,  <span class="hljs-comment">// Prevent non-GPU workloads from scheduling</span>
    }],
}, { dependsOn: [cluster] });
</code></pre>
<p>The GPU node group uses the <code>AL2023_x86_64_NVIDIA</code> AMI type, which includes pre-installed NVIDIA drivers and Container Toolkit. The <code>nvidia.com/mig.config</code> label tells the MIG Manager how to partition the GPUs when they come online.</p>
<p>A note on capacity: p4d.24xlarge instances are in high demand. Spot looks attractive on paper (92% savings) but frequently fails with <code>UnfulfillableCapacity</code> errors. On-Demand is more reliable but runs $32/hr per instance in us-east-1. For production, capacity reservations help. Checking multiple regions helps more.</p>
<h3 id="heading-installing-the-nvidia-gpu-operator">Installing the NVIDIA GPU Operator</h3>
<p>The <a target="_blank" href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/">GPU Operator</a> manages NVIDIA software components on your cluster. When using the AL2023_x86_64_NVIDIA AMI, you must disable driver and toolkit installation since they're pre-installed:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// NVIDIA GPU Operator with DRA configuration</span>
<span class="hljs-keyword">const</span> gpuOperatorNamespace = <span class="hljs-keyword">new</span> k8s.core.v1.Namespace(<span class="hljs-string">"gpu-operator-ns"</span>, {
    metadata: { name: <span class="hljs-string">"gpu-operator"</span> },
}, { provider: k8sProvider });

<span class="hljs-keyword">const</span> gpuOperator = <span class="hljs-keyword">new</span> k8s.helm.v3.Release(<span class="hljs-string">"gpu-operator"</span>, {
    chart: <span class="hljs-string">"gpu-operator"</span>,
    <span class="hljs-keyword">namespace</span>: gpuOperatorNamespace.metadata.name,
    repositoryOpts: {
        repo: <span class="hljs-string">"https://helm.ngc.nvidia.com/nvidia"</span>,
    },
    version: <span class="hljs-string">"v25.10.1"</span>,
    skipAwait: <span class="hljs-literal">true</span>,  <span class="hljs-comment">// GPU Operator takes time to roll out across nodes</span>
    values: {
        <span class="hljs-comment">// AL2023_x86_64_NVIDIA AMI has pre-installed drivers</span>
        driver: { enabled: <span class="hljs-literal">false</span> },
        <span class="hljs-comment">// AL2023_x86_64_NVIDIA AMI has pre-installed toolkit</span>
        toolkit: { enabled: <span class="hljs-literal">false</span> },
        <span class="hljs-comment">// Disable traditional device plugin - DRA handles allocation</span>
        devicePlugin: { enabled: <span class="hljs-literal">false</span> },
        <span class="hljs-comment">// Set containerd as the runtime</span>
        operator: {
            defaultRuntime: <span class="hljs-string">"containerd"</span>,
        },
        <span class="hljs-comment">// Enable Node Feature Discovery for GPU detection</span>
        nfd: { enabled: <span class="hljs-literal">true</span> },
        <span class="hljs-comment">// Configure MIG strategy for heterogeneous profiles</span>
        mig: {
            strategy: <span class="hljs-string">"mixed"</span>,
        },
        <span class="hljs-comment">// Enable MIG Manager to apply MIG configurations</span>
        migManager: {
            enabled: <span class="hljs-literal">true</span>,
            env: [{
                name: <span class="hljs-string">"WITH_REBOOT"</span>,
                value: <span class="hljs-string">"true"</span>,  <span class="hljs-comment">// Allow node reboots if needed for MIG changes</span>
            }],
        },
        <span class="hljs-comment">// Enable DCGM Exporter for GPU metrics</span>
        dcgmExporter: {
            enabled: <span class="hljs-literal">true</span>,
            serviceMonitor: { enabled: <span class="hljs-literal">true</span> },
        },
    },
}, { provider: k8sProvider, dependsOn: [gpuNodeGroup] });
</code></pre>
<p>A few things to watch here: you must set <code>driver.enabled: false</code> and <code>toolkit.enabled: false</code> when using the AL2023_x86_64_NVIDIA AMI, or you'll get conflicts with the pre-installed drivers. The <code>mig.strategy: "mixed"</code> is required because <code>all-balanced</code> creates heterogeneous MIG slice sizes. And <code>operator.defaultRuntime: "containerd"</code> is required for AL2023's cgroup v2 setup.</p>
<h3 id="heading-understanding-mig-configuration">Understanding MIG configuration</h3>
<p>That <code>nvidia.com/mig.config: "all-balanced"</code> label on line 435 does more than you might think. MIG Manager runs as a controller in your cluster, watching for this label on GPU nodes. When it spots it, the reconfiguration happens automatically: GPU workloads stop, <code>nvidia-mig-parted</code> applies the new geometry, the node reboots if necessary, then everything comes back up with the new partitioning.</p>
<p>The label value has to match a profile name in the <code>default-mig-parted-config</code> ConfigMap. The GPU Operator creates this ConfigMap with pre-defined configurations for different GPU models:</p>
<pre><code class="lang-bash">kubectl get configmap -n gpu-operator default-mig-parted-config -o yaml
</code></pre>
<blockquote>
<p><strong>Note:</strong> If you're using <a target="_blank" href="/docs/esc/">Pulumi ESC</a> for cluster authentication, you can run this via <code>pulumi env run &lt;your-env&gt; -- kubectl get configmap ...</code> to inject credentials automatically.</p>
</blockquote>
<p>For A100-40GB GPUs (p4d.24xlarge), you get these options:</p>
<ul>
<li><p><code>all-1g.5gb</code> creates 7 identical 1g.5gb slices per GPU (56 total across 8 GPUs)</p>
</li>
<li><p><code>all-2g.10gb</code> creates 3 identical 2g.10gb slices per GPU</p>
</li>
<li><p><code>all-3g.20gb</code> creates 2 identical 3g.20gb slices per GPU</p>
</li>
<li><p><code>all-balanced</code> creates a mix: 2× 1g.5gb + 1× 2g.10gb + 1× 3g.20gb per GPU</p>
</li>
</ul>
<p>The <code>all-balanced</code> profile is more useful than uniform slicing. You get small slices for inference, medium slices for training, and larger slices for bigger models, all on the same GPU with hardware isolation. Real workloads aren't all the same size, so why partition like they are?</p>
<h3 id="heading-understanding-mig-strategy-single-vs-mixed">Understanding MIG strategy: single vs mixed</h3>
<p>The <code>mig.strategy</code> setting controls how GPU Feature Discovery (GFD) advertises MIG devices to Kubernetes. Get this wrong and your GPU nodes show <code>nvidia.com/gpu.product=NVIDIA-A100-SXM4-40GB-MIG-INVALID</code> with zero resources available. We spent an hour debugging this one.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Strategy</td><td>When to use</td><td>What it does</td></tr>
</thead>
<tbody>
<tr>
<td><code>single</code></td><td>Uniform profiles (<code>all-1g.5gb</code>, <code>all-2g.10gb</code>)</td><td>Expects all MIG devices to be the same type. Advertises as <code>nvidia.com/gpu</code>.</td></tr>
<tr>
<td><code>mixed</code></td><td>Heterogeneous profiles (<code>all-balanced</code>)</td><td>Supports multiple MIG device types. Advertises as <code>nvidia.com/mig-1g.5gb</code>, <code>nvidia.com/mig-2g.10gb</code>, etc.</td></tr>
</tbody>
</table>
</div><p>Here's the thing that tripped us up: <code>all-balanced</code> creates different MIG slice sizes (1g.5gb, 2g.10gb, 3g.20gb) on the same GPU. If you use <code>strategy: "single"</code> with <code>all-balanced</code>, GFD sees multiple MIG types and marks everything invalid:</p>
<pre><code class="lang-text">Invalid configuration detected for mig-strategy=single: more than one MIG device type present on node
</code></pre>
<p>Use <code>mixed</code> whenever your MIG configuration creates different slice sizes. Use <code>single</code> only with uniform profiles like <code>all-1g.5gb</code> where every slice is identical.</p>
<p>One clarification: <code>single</code> doesn't mean all GPUs must have the same profile. It means all MIG slices across the node must be the same type. GPU 0 and GPU 1 both running <code>all-1g.5gb</code>? That's fine with <code>single</code> because all slices are 1g.5gb. But <code>all-balanced</code> creates 1g.5gb, 2g.10gb, and 3g.20gb slices on every GPU, so you need <code>mixed</code>.</p>
<p>For the test workload we're building, <code>all-balanced</code> demonstrates the actual value of MIG with DRA: different workload sizes sharing the same GPU.</p>
<h3 id="heading-installing-the-nvidia-dra-driver">Installing the NVIDIA DRA Driver</h3>
<p>The <a target="_blank" href="https://github.com/NVIDIA/k8s-dra-driver">DRA driver</a> publishes GPU resources to Kubernetes and handles the allocation lifecycle:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// NVIDIA DRA Driver for GPU allocation</span>
<span class="hljs-keyword">const</span> draDriverNamespace = <span class="hljs-keyword">new</span> k8s.core.v1.Namespace(<span class="hljs-string">"dra-driver-ns"</span>, {
    metadata: { name: <span class="hljs-string">"nvidia-dra-driver"</span> },
}, { provider: k8sProvider });

<span class="hljs-keyword">const</span> draDriver = <span class="hljs-keyword">new</span> k8s.helm.v3.Release(<span class="hljs-string">"nvidia-dra-driver"</span>, {
    chart: <span class="hljs-string">"nvidia-dra-driver-gpu"</span>,
    <span class="hljs-keyword">namespace</span>: draDriverNamespace.metadata.name,
    repositoryOpts: {
        repo: <span class="hljs-string">"https://helm.ngc.nvidia.com/nvidia"</span>,
    },
    version: <span class="hljs-string">"v25.8.1"</span>,
    skipAwait: <span class="hljs-literal">true</span>,
    values: {
        <span class="hljs-comment">// Set driver root for host-installed drivers (AL2023 AMI)</span>
        nvidiaDriverRoot: <span class="hljs-string">"/"</span>,
        <span class="hljs-comment">// Enable GPU resources override for proper DRA functionality</span>
        gpuResourcesEnabledOverride: <span class="hljs-literal">true</span>,
        resources: {
            gpus: { enabled: <span class="hljs-literal">true</span> },
            computeDomains: { enabled: <span class="hljs-literal">false</span> },
        },
        <span class="hljs-comment">// Configure kubelet plugin to tolerate GPU node taints</span>
        kubeletPlugin: {
            tolerations: [{
                key: <span class="hljs-string">"nvidia.com/gpu"</span>,
                operator: <span class="hljs-string">"Exists"</span>,
                effect: <span class="hljs-string">"NoSchedule"</span>,
            }],
        },
        <span class="hljs-comment">// Schedule DRA controller on system nodes (not GPU nodes)</span>
        controller: {
            affinity: {
                nodeAffinity: {
                    requiredDuringSchedulingIgnoredDuringExecution: {
                        nodeSelectorTerms: [{
                            matchExpressions: [{
                                key: <span class="hljs-string">"node-role"</span>,
                                operator: <span class="hljs-string">"In"</span>,
                                values: [<span class="hljs-string">"system"</span>],
                            }],
                        }],
                    },
                },
            },
        },
    },
}, { provider: k8sProvider, dependsOn: [gpuOperator] });
</code></pre>
<p>Put the DRA controller on system nodes, not GPU nodes. You don't want resource allocation conflicts. The kubelet plugin still runs on GPU nodes as a DaemonSet to handle device preparation.</p>
<p>The DRA driver creates two DeviceClasses automatically when <code>resources.gpus.enabled: true</code> is set:</p>
<ul>
<li><p><code>gpu.nvidia.com</code> for full GPU allocations</p>
</li>
<li><p><code>mig.nvidia.com</code> for MIG partition allocations</p>
</li>
</ul>
<h2 id="heading-understanding-resourceclaimtemplates">Understanding ResourceClaimTemplates</h2>
<p>ResourceClaimTemplates define how pods request specific GPU resources. The CEL expression in the selector determines which devices match. For a workload that needs a small MIG partition:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">resource.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ResourceClaimTemplate</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">mig-small-template</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">spec:</span>
    <span class="hljs-attr">devices:</span>
      <span class="hljs-attr">requests:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">mig-gpu</span>
        <span class="hljs-attr">exactly:</span>
          <span class="hljs-attr">deviceClassName:</span> <span class="hljs-string">mig.nvidia.com</span>
          <span class="hljs-attr">count:</span> <span class="hljs-number">1</span>
          <span class="hljs-attr">selectors:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">cel:</span>
              <span class="hljs-attr">expression:</span> <span class="hljs-string">|
                device.attributes["gpu.nvidia.com"].type == "mig" &amp;&amp;
                device.attributes["gpu.nvidia.com"].profile == "1g.5gb"</span>
</code></pre>
<p>The CEL expression filters for MIG devices with the <code>1g.5gb</code> profile. For different profiles, change the profile value to <code>2g.10gb</code>, <code>3g.20gb</code>, etc.</p>
<h2 id="heading-real-world-workload-demonstration-fashion-mnist-training-and-inference">Real-world workload demonstration: Fashion-MNIST training and inference</h2>
<p>To demonstrate MIG running different workload types on the same GPU, we deployed three PyTorch workloads using the Fashion-MNIST dataset: a large training job, a medium training job, and a small inference service, all sharing a single A100 GPU.</p>
<p><a target="_blank" href="https://github.com/zalandoresearch/fashion-mnist">Fashion-MNIST</a> is a dataset of 70,000 grayscale images (60,000 training, 10,000 test) showing clothing items across 10 categories: t-shirts, trousers, pullovers, dresses, coats, sandals, shirts, sneakers, bags, and ankle boots. Created by Zalando Research as a drop-in replacement for the classic MNIST handwritten digits dataset, it provides a more challenging classification task while maintaining the same 28x28 pixel format. It's commonly used for benchmarking ML frameworks and testing GPU workloads because it's large enough to stress hardware but small enough to iterate quickly.</p>
<h3 id="heading-the-workloads">The workloads</h3>
<p>With the <code>all-balanced</code> MIG configuration, each A100 40GB GPU provides:</p>
<ul>
<li><p>2× <code>1g.5gb</code> partitions (small inference workloads)</p>
</li>
<li><p>1× <code>2g.10gb</code> partition (medium training)</p>
</li>
<li><p>1× <code>3g.20gb</code> partition (large training)</p>
</li>
</ul>
<p>Here's what we deployed:</p>
<p><strong>Large training workload (3g.20gb)</strong>: ResNet-18 training on Fashion-MNIST with 256-batch size, using ~11GB GPU memory.</p>
<p><strong>Medium training workload (2g.10gb)</strong>: Custom CNN training with 128-batch size, using ~5GB GPU memory.</p>
<p><strong>Small inference workload (1g.5gb)</strong>: Continuous inference loop using ~0.01GB memory.</p>
<h3 id="heading-configmap-with-training-scripts">ConfigMap with training scripts</h3>
<p>First, create a ConfigMap containing the Python scripts for each workload:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> fashionMnistScripts = <span class="hljs-keyword">new</span> k8s.core.v1.ConfigMap(<span class="hljs-string">"fashion-mnist-scripts"</span>, {
    metadata: {
        name: <span class="hljs-string">"fashion-mnist-scripts"</span>,
        <span class="hljs-keyword">namespace</span>: migTestNamespace.metadata.name,
    },
    data: {
        <span class="hljs-string">"large-training-script.py"</span>: <span class="hljs-string">`import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms, models

print("Starting Large Training Workload (3g.20gb MIG)")
print(f"PyTorch version: {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"CUDA device: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'N/A'}")

transform = transforms.Compose([
    transforms.Resize(224),
    transforms.Grayscale(3),
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

train_dataset = datasets.FashionMNIST('./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=256, shuffle=True)

model = models.resnet18(pretrained=False, num_classes=10).cuda()
optimizer = optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()

print("Starting training loop...")
for epoch in range(20):
    model.train()
    total_loss = 0
    correct = 0
    total = 0

    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.cuda(), target.cuda()
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
        pred = output.argmax(dim=1)
        correct += pred.eq(target).sum().item()
        total += target.size(0)

    accuracy = 100. * correct / total
    avg_loss = total_loss / len(train_loader)
    print(f"Epoch {epoch+1}/20 | Loss: {avg_loss:.4f} | Accuracy: {accuracy:.2f}% | GPU Mem: {torch.cuda.memory_allocated()/1e9:.2f}GB")

print("Training complete!")`</span>,

        <span class="hljs-string">"medium-training-script.py"</span>: <span class="hljs-string">`import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms

print("Starting Medium Training Workload (2g.10gb MIG)")
print(f"PyTorch version: {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"CUDA device: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'N/A'}")

transform = transforms.Compose([
    transforms.Resize(32),
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

train_dataset = datasets.FashionMNIST('./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True)

# Custom CNN with 4 conv layers + 2 FC
model = nn.Sequential(
    nn.Conv2d(1, 64, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2),
    nn.Conv2d(64, 128, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2),
    nn.Conv2d(128, 256, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2),
    nn.Conv2d(256, 512, kernel_size=3, padding=1),
    nn.ReLU(),
    nn.AdaptiveAvgPool2d(1),
    nn.Flatten(),
    nn.Linear(512, 256),
    nn.ReLU(),
    nn.Dropout(0.5),
    nn.Linear(256, 10)
).cuda()

optimizer = optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()

print("Starting training loop...")
for epoch in range(15):
    model.train()
    total_loss = 0
    correct = 0
    total = 0

    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.cuda(), target.cuda()
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
        pred = output.argmax(dim=1)
        correct += pred.eq(target).sum().item()
        total += target.size(0)

    accuracy = 100. * correct / total
    avg_loss = total_loss / len(train_loader)
    print(f"Epoch {epoch+1}/15 | Loss: {avg_loss:.4f} | Accuracy: {accuracy:.2f}% | GPU Mem: {torch.cuda.memory_allocated()/1e9:.2f}GB")

print("Training complete!")`</span>,

        <span class="hljs-string">"small-inference-script.py"</span>: <span class="hljs-string">`import torch
import torch.nn as nn
from torchvision import datasets, transforms

print("Starting Small Inference Workload (1g.5gb MIG)")
print(f"PyTorch version: {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"CUDA device: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'N/A'}")

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

test_dataset = datasets.FashionMNIST('./data', train=False, download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=32, shuffle=False)

model = nn.Sequential(
    nn.Flatten(),
    nn.Linear(28*28, 512),
    nn.ReLU(),
    nn.Linear(512, 256),
    nn.ReLU(),
    nn.Linear(256, 10)
).cuda()

model.eval()

print("Starting continuous inference loop...")
iteration = 0
while True:
    for data, target in test_loader:
        data, target = data.cuda(), target.cuda()
        with torch.no_grad():
            output = model(data)
            pred = output.argmax(dim=1)

        if iteration % 50 == 0:
            print(f"Iteration {iteration} | GPU Mem: {torch.cuda.memory_allocated()/1e9:.2f}GB")

        iteration += 1`</span>,
    },
}, { provider: k8sProvider });
</code></pre>
<h3 id="heading-resourceclaimtemplates-for-different-mig-profiles">ResourceClaimTemplates for different MIG profiles</h3>
<p>Create three ResourceClaimTemplates, one for each MIG profile:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Large training: 3g.20gb MIG profile</span>
<span class="hljs-keyword">const</span> migLargeClaimTemplate = <span class="hljs-keyword">new</span> k8s.apiextensions.CustomResource(<span class="hljs-string">"mig-large-template"</span>, {
    apiVersion: <span class="hljs-string">"resource.k8s.io/v1"</span>,
    kind: <span class="hljs-string">"ResourceClaimTemplate"</span>,
    metadata: {
        name: <span class="hljs-string">"mig-large-template"</span>,
        <span class="hljs-keyword">namespace</span>: migTestNamespace.metadata.name,
    },
    spec: {
        spec: {
            devices: {
                requests: [{
                    name: <span class="hljs-string">"mig-large"</span>,
                    exactly: {
                        deviceClassName: <span class="hljs-string">"mig.nvidia.com"</span>,
                        count: <span class="hljs-number">1</span>,
                        selectors: [{
                            cel: {
                                expression: <span class="hljs-string">'device.attributes["gpu.nvidia.com"].type == "mig" &amp;&amp; device.attributes["gpu.nvidia.com"].profile == "3g.20gb"'</span>,
                            },
                        }],
                    },
                }],
            },
        },
    },
}, { provider: k8sProvider, dependsOn: [draDriver] });

<span class="hljs-comment">// Medium training: 2g.10gb MIG profile</span>
<span class="hljs-keyword">const</span> migMediumClaimTemplate = <span class="hljs-keyword">new</span> k8s.apiextensions.CustomResource(<span class="hljs-string">"mig-medium-template"</span>, {
    apiVersion: <span class="hljs-string">"resource.k8s.io/v1"</span>,
    kind: <span class="hljs-string">"ResourceClaimTemplate"</span>,
    metadata: {
        name: <span class="hljs-string">"mig-medium-template"</span>,
        <span class="hljs-keyword">namespace</span>: migTestNamespace.metadata.name,
    },
    spec: {
        spec: {
            devices: {
                requests: [{
                    name: <span class="hljs-string">"mig-medium"</span>,
                    exactly: {
                        deviceClassName: <span class="hljs-string">"mig.nvidia.com"</span>,
                        count: <span class="hljs-number">1</span>,
                        selectors: [{
                            cel: {
                                expression: <span class="hljs-string">'device.attributes["gpu.nvidia.com"].type == "mig" &amp;&amp; device.attributes["gpu.nvidia.com"].profile == "2g.10gb"'</span>,
                            },
                        }],
                    },
                }],
            },
        },
    },
}, { provider: k8sProvider, dependsOn: [draDriver] });

<span class="hljs-comment">// Small inference: 1g.5gb MIG profile</span>
<span class="hljs-keyword">const</span> migSmallClaimTemplate = <span class="hljs-keyword">new</span> k8s.apiextensions.CustomResource(<span class="hljs-string">"mig-small-template"</span>, {
    apiVersion: <span class="hljs-string">"resource.k8s.io/v1"</span>,
    kind: <span class="hljs-string">"ResourceClaimTemplate"</span>,
    metadata: {
        name: <span class="hljs-string">"mig-small-template"</span>,
        <span class="hljs-keyword">namespace</span>: migTestNamespace.metadata.name,
    },
    spec: {
        spec: {
            devices: {
                requests: [{
                    name: <span class="hljs-string">"mig-small"</span>,
                    exactly: {
                        deviceClassName: <span class="hljs-string">"mig.nvidia.com"</span>,
                        count: <span class="hljs-number">1</span>,
                        selectors: [{
                            cel: {
                                expression: <span class="hljs-string">'device.attributes["gpu.nvidia.com"].type == "mig" &amp;&amp; device.attributes["gpu.nvidia.com"].profile == "1g.5gb"'</span>,
                            },
                        }],
                    },
                }],
            },
        },
    },
}, { provider: k8sProvider, dependsOn: [draDriver] });
</code></pre>
<h3 id="heading-deploying-the-workload-pods">Deploying the workload pods</h3>
<p>Finally, create three pods that run the workloads:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Large training pod (3g.20gb)</span>
<span class="hljs-keyword">const</span> migLargeTrainingPod = <span class="hljs-keyword">new</span> k8s.core.v1.Pod(<span class="hljs-string">"mig-large-training"</span>, {
    metadata: {
        name: <span class="hljs-string">"mig-large-training-pod"</span>,
        <span class="hljs-keyword">namespace</span>: migTestNamespace.metadata.name,
    },
    spec: {
        restartPolicy: <span class="hljs-string">"Never"</span>,
        tolerations: [{
            key: <span class="hljs-string">"nvidia.com/gpu"</span>,
            operator: <span class="hljs-string">"Exists"</span>,
            effect: <span class="hljs-string">"NoSchedule"</span>,
        }],
        nodeSelector: {
            <span class="hljs-string">"node-role"</span>: <span class="hljs-string">"gpu"</span>,
            <span class="hljs-string">"nvidia.com/gpu.present"</span>: <span class="hljs-string">"true"</span>,
        },
        containers: [{
            name: <span class="hljs-string">"training"</span>,
            image: <span class="hljs-string">"nvcr.io/nvidia/pytorch:25.12-py3"</span>,
            command: [<span class="hljs-string">"python"</span>, <span class="hljs-string">"/scripts/large-training-script.py"</span>],
            volumeMounts: [{
                name: <span class="hljs-string">"scripts"</span>,
                mountPath: <span class="hljs-string">"/scripts"</span>,
                readOnly: <span class="hljs-literal">true</span>,
            }],
            resources: {
                claims: [{ name: <span class="hljs-string">"mig-large"</span> }],
            },
        }],
        resourceClaims: [{
            name: <span class="hljs-string">"mig-large"</span>,
            resourceClaimTemplateName: <span class="hljs-string">"mig-large-template"</span>,
        }],
        volumes: [{
            name: <span class="hljs-string">"scripts"</span>,
            configMap: {
                name: fashionMnistScripts.metadata.name,
            },
        }],
    },
}, { provider: k8sProvider, dependsOn: [migLargeClaimTemplate, fashionMnistScripts] });

<span class="hljs-comment">// Medium training pod (2g.10gb)</span>
<span class="hljs-keyword">const</span> migMediumTrainingPod = <span class="hljs-keyword">new</span> k8s.core.v1.Pod(<span class="hljs-string">"mig-medium-training"</span>, {
    metadata: {
        name: <span class="hljs-string">"mig-medium-training-pod"</span>,
        <span class="hljs-keyword">namespace</span>: migTestNamespace.metadata.name,
    },
    spec: {
        restartPolicy: <span class="hljs-string">"Never"</span>,
        tolerations: [{
            key: <span class="hljs-string">"nvidia.com/gpu"</span>,
            operator: <span class="hljs-string">"Exists"</span>,
            effect: <span class="hljs-string">"NoSchedule"</span>,
        }],
        nodeSelector: {
            <span class="hljs-string">"node-role"</span>: <span class="hljs-string">"gpu"</span>,
            <span class="hljs-string">"nvidia.com/gpu.present"</span>: <span class="hljs-string">"true"</span>,
        },
        containers: [{
            name: <span class="hljs-string">"training"</span>,
            image: <span class="hljs-string">"nvcr.io/nvidia/pytorch:25.12-py3"</span>,
            command: [<span class="hljs-string">"python"</span>, <span class="hljs-string">"/scripts/medium-training-script.py"</span>],
            volumeMounts: [{
                name: <span class="hljs-string">"scripts"</span>,
                mountPath: <span class="hljs-string">"/scripts"</span>,
                readOnly: <span class="hljs-literal">true</span>,
            }],
            resources: {
                claims: [{ name: <span class="hljs-string">"mig-medium"</span> }],
            },
        }],
        resourceClaims: [{
            name: <span class="hljs-string">"mig-medium"</span>,
            resourceClaimTemplateName: <span class="hljs-string">"mig-medium-template"</span>,
        }],
        volumes: [{
            name: <span class="hljs-string">"scripts"</span>,
            configMap: {
                name: fashionMnistScripts.metadata.name,
            },
        }],
    },
}, { provider: k8sProvider, dependsOn: [migMediumClaimTemplate, fashionMnistScripts] });

<span class="hljs-comment">// Small inference pod (1g.5gb)</span>
<span class="hljs-keyword">const</span> migSmallInferencePod = <span class="hljs-keyword">new</span> k8s.core.v1.Pod(<span class="hljs-string">"mig-small-inference"</span>, {
    metadata: {
        name: <span class="hljs-string">"mig-small-inference-pod"</span>,
        <span class="hljs-keyword">namespace</span>: migTestNamespace.metadata.name,
    },
    spec: {
        restartPolicy: <span class="hljs-string">"Never"</span>,
        tolerations: [{
            key: <span class="hljs-string">"nvidia.com/gpu"</span>,
            operator: <span class="hljs-string">"Exists"</span>,
            effect: <span class="hljs-string">"NoSchedule"</span>,
        }],
        nodeSelector: {
            <span class="hljs-string">"node-role"</span>: <span class="hljs-string">"gpu"</span>,
            <span class="hljs-string">"nvidia.com/gpu.present"</span>: <span class="hljs-string">"true"</span>,
        },
        containers: [{
            name: <span class="hljs-string">"inference"</span>,
            image: <span class="hljs-string">"nvcr.io/nvidia/pytorch:25.12-py3"</span>,
            command: [<span class="hljs-string">"python"</span>, <span class="hljs-string">"/scripts/small-inference-script.py"</span>],
            volumeMounts: [{
                name: <span class="hljs-string">"scripts"</span>,
                mountPath: <span class="hljs-string">"/scripts"</span>,
                readOnly: <span class="hljs-literal">true</span>,
            }],
            resources: {
                claims: [{ name: <span class="hljs-string">"mig-small"</span> }],
            },
        }],
        resourceClaims: [{
            name: <span class="hljs-string">"mig-small"</span>,
            resourceClaimTemplateName: <span class="hljs-string">"mig-small-template"</span>,
        }],
        volumes: [{
            name: <span class="hljs-string">"scripts"</span>,
            configMap: {
                name: fashionMnistScripts.metadata.name,
            },
        }],
    },
}, { provider: k8sProvider, dependsOn: [migSmallClaimTemplate, fashionMnistScripts] });
</code></pre>
<h3 id="heading-deployment-results">Deployment results</h3>
<p>After deploying with <code>pulumi up</code>, the three workloads scheduled onto the same physical GPU:</p>
<pre><code class="lang-text">NAME                      READY   STATUS      RESTARTS   AGE
mig-large-training-pod    1/1     Running     0          21m
mig-medium-training-pod   0/1     Completed   0          21m
mig-small-inference-pod   1/1     Running     0          20m
</code></pre>
<p>The logs confirm each workload got its allocated MIG partition:</p>
<p><strong>Medium training</strong> (completed 15 epochs):</p>
<pre><code class="lang-text">Starting Medium Training Workload (2g.10gb MIG)
CUDA device: NVIDIA A100-SXM4-40GB MIG 2g.10gb
Epoch 1/15 | Loss: 0.7649 | Accuracy: 71.21% | GPU Mem: 0.05GB
...
Epoch 15/15 | Loss: 0.0827 | Accuracy: 96.95% | GPU Mem: 0.05GB
Training complete!
</code></pre>
<p><strong>Large training</strong> (20 epochs with ResNet-18):</p>
<pre><code class="lang-text">Starting Large Training Workload (3g.20gb MIG)
CUDA device: NVIDIA A100-SXM4-40GB MIG 3g.20gb
Epoch 1/20 | Loss: 0.4476 | Accuracy: 83.35% | GPU Mem: 0.26GB
...
Epoch 20/20 | Loss: 0.0183 | Accuracy: 99.36% | GPU Mem: 0.26GB
Training complete!
</code></pre>
<p><strong>Small inference</strong> (continuous operation):</p>
<pre><code class="lang-text">Starting Small Inference Workload (1g.5gb MIG)
CUDA device: NVIDIA A100-SXM4-40GB MIG 1g.5gb
Iteration 260000 | GPU Mem: 0.01GB
</code></pre>
<p>The medium training finished in about 21 minutes with 96.95% accuracy. The large training job completed 20 epochs, reaching 99.36% accuracy. The inference pod continues running, now past 576,000 iterations.</p>
<h2 id="heading-monitoring-mig-with-grafana">Monitoring MIG with Grafana</h2>
<p>With <a target="_blank" href="https://github.com/NVIDIA/dcgm-exporter">DCGM Exporter</a> enabled in the GPU Operator, you can visualize MIG partition metrics in Grafana. The screenshot below shows an NVIDIA MIG dashboard displaying metrics for multiple MIG instances on an A100 GPU:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769958879399/5bba335f-dac0-4c1d-814e-6e8196eccf5f.png" alt class="image--center mx-auto" /></p>
<p>The dashboard breaks down metrics by MIG partition: GPU utilization, memory consumption, SM clock speeds, and temperature. Useful for spotting underutilized partitions or memory pressure before they become problems.</p>
<h2 id="heading-why-platform-engineers-should-care">Why platform engineers should care</h2>
<p>If you're running a shared GPU cluster, you've probably dealt with the politics of GPU allocation. Team A wants dedicated hardware. Team B complains their jobs keep getting killed. Nobody trusts the "fair share" scheduler. MIG with DRA sidesteps a lot of this because the isolation is in the hardware, not a software policy that can be argued with.</p>
<p>The cost math is also hard to ignore. Seven <code>1g.5gb</code> inference services on one A100 versus seven separate GPU instances? That's not a small difference on the AWS bill.</p>
<p>And there's the self-service angle. Once you've set up DeviceClasses and ResourceClaimTemplates, ML teams can request what they need without filing tickets. They pick a claim template, and the cluster figures out where to put the workload. You define the guardrails once; they stay out of your queue.</p>
<p>If you want to go further, <a target="_blank" href="/docs/iac/using-pulumi/crossguard/">Pulumi CrossGuard</a> can enforce policies on GPU allocation. Here's a policy that blocks full GPU profiles while allowing the balanced MIG partitions:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> policy <span class="hljs-keyword">from</span> <span class="hljs-string">"@pulumi/policy"</span>;

<span class="hljs-keyword">const</span> allowedMigProfiles = [<span class="hljs-string">"1g.5gb"</span>, <span class="hljs-string">"2g.10gb"</span>, <span class="hljs-string">"3g.20gb"</span>];
<span class="hljs-keyword">const</span> blockedMigProfiles = [<span class="hljs-string">"4g.20gb"</span>, <span class="hljs-string">"7g.40gb"</span>, <span class="hljs-string">"7g.80gb"</span>];

<span class="hljs-keyword">new</span> policy.PolicyPack(<span class="hljs-string">"mig-policy"</span>, {
    policies: [
        {
            name: <span class="hljs-string">"enforce-balanced-mig-profiles"</span>,
            description: <span class="hljs-string">"Prevents full GPU allocations while allowing balanced MIG profiles (1g.5gb, 2g.10gb, 3g.20gb)"</span>,
            enforcementLevel: <span class="hljs-string">"mandatory"</span>,
            validateResource: <span class="hljs-function">(<span class="hljs-params">args, reportViolation</span>) =&gt;</span> {
                <span class="hljs-keyword">const</span> resource = args.props <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>;

                <span class="hljs-keyword">if</span> (args.type !== <span class="hljs-string">"kubernetes:resource.k8s.io/v1:ResourceClaimTemplate"</span>) {
                    <span class="hljs-keyword">return</span>;
                }

                <span class="hljs-keyword">const</span> spec = resource.spec;
                <span class="hljs-keyword">if</span> (!spec?.spec?.devices?.requests) <span class="hljs-keyword">return</span>;

                <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> request <span class="hljs-keyword">of</span> spec.spec.devices.requests) {
                    <span class="hljs-keyword">const</span> selectors = request.exactly?.selectors || [];
                    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> selector <span class="hljs-keyword">of</span> selectors) {
                        <span class="hljs-keyword">const</span> expression = selector.cel?.expression;
                        <span class="hljs-keyword">if</span> (!expression) <span class="hljs-keyword">continue</span>;

                        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> blocked <span class="hljs-keyword">of</span> blockedMigProfiles) {
                            <span class="hljs-keyword">if</span> (expression.includes(<span class="hljs-string">`"<span class="hljs-subst">${blocked}</span>"`</span>)) {
                                reportViolation(
                                    <span class="hljs-string">`ResourceClaimTemplate '<span class="hljs-subst">${resource.metadata?.name}</span>' requests `</span> +
                                    <span class="hljs-string">`blocked profile '<span class="hljs-subst">${blocked}</span>'. Allowed: <span class="hljs-subst">${allowedMigProfiles.join(<span class="hljs-string">", "</span>)}</span>`</span>
                                );
                            }
                        }
                    }
                }
            },
        },
    ],
});
</code></pre>
<p>Run it with <code>pulumi preview --policy-pack ./mig-policy</code>. If someone tries to request a <code>7g.80gb</code> profile (the full GPU), the preview fails:</p>
<pre><code class="lang-bash">Policies:
    ❌ mig-policy@v1.0.0 (<span class="hljs-built_in">local</span>: mig-policy)
        - [mandatory]  enforce-small-mig-profiles  (kubernetes:resource.k8s.io/v1:ResourceClaimTemplate: mig-invalid-template)
          Prevents using large MIG profiles (4g.20gb and above) to maximize GPU utilization <span class="hljs-keyword">for</span> inference workloads
          ResourceClaimTemplate <span class="hljs-string">'mig-invalid-template'</span> requests blocked MIG profile <span class="hljs-string">'7g.80gb'</span>. Only small profiles are allowed: 1g.5gb, 2g.10gb, 3g.20gb. Large profiles waste GPU resources <span class="hljs-keyword">for</span> inference workloads.
</code></pre>
<p>The usual policy-as-code workflow, but applied to hardware allocation.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/pulumi/examples/tree/master/aws-ts-eks-gpu-dra">https://github.com/pulumi/examples/tree/master/aws-ts-eks-gpu-dra</a></div>
<p> </p>
<h2 id="heading-capacity-planning">Capacity planning</h2>
<p>The cluster is running. Someone asks: "Can I get three more MIG slices for my inference service?" You should be able to answer that without guessing.</p>
<h3 id="heading-checking-capacity-with-kubectl">Checking capacity with kubectl</h3>
<p>The quickest way is <code>kubectl</code> against ResourceSlices and ResourceClaims:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># See all available MIG devices across the cluster</span>
kubectl get resourceslices -o jsonpath=<span class="hljs-string">'{range .items[*]}{.metadata.name}{"\n"}{range .spec.devices[*]}  {.name}: {.attributes.profile.string}{"\n"}{end}{end}'</span>

<span class="hljs-comment"># Count total vs allocated MIG slices</span>
kubectl get resourceslices -o json | jq <span class="hljs-string">'[.items[].spec.devices[]] | length'</span>
kubectl get resourceclaims -A -o json | jq <span class="hljs-string">'[.items[] | select(.status.allocation != null)] | length'</span>

<span class="hljs-comment"># See which claims are pending (waiting for capacity)</span>
kubectl get resourceclaims -A -o json | jq <span class="hljs-string">'.items[] | select(.status.allocation == null) | .metadata.name'</span>
</code></pre>
<p>On our test cluster with one p4d.24xlarge (8x A100 40GB GPUs, each with the <code>all-balanced</code> profile creating 4 slices: 2× 1g.5gb, 1× 2g.10gb, 1× 3g.20gb), the output shows:</p>
<pre><code class="lang-text">Total MIG slices: 32
Allocated: 3
Available: 29
</code></pre>
<h3 id="heading-monitoring-with-prometheus-and-grafana">Monitoring with Prometheus and Grafana</h3>
<p>DCGM Exporter (enabled in our GPU Operator config) pushes metrics to Prometheus. Some queries worth having:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Available GPU memory per MIG instance</span>
DCGM_FI_DEV_FB_FREE{gpu=~<span class="hljs-string">".*MIG.*"</span>}

<span class="hljs-comment"># GPU utilization per MIG partition</span>
DCGM_FI_DEV_GPU_UTIL{gpu=~<span class="hljs-string">".*MIG.*"</span>}

<span class="hljs-comment"># Count of active MIG devices</span>
count(DCGM_FI_DEV_GPU_UTIL{gpu=~<span class="hljs-string">".*MIG.*"</span>})
</code></pre>
<p>Wire these into a Grafana dashboard. You'll want to know when you're running low before someone's deployment gets stuck pending.</p>
<h3 id="heading-direct-node-inspection">Direct node inspection</h3>
<p>When kubectl and Prometheus disagree, SSH into the GPU node and check with <code>nvidia-smi</code>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># List all GPU instances and their status</span>
nvidia-smi mig -lgi

<span class="hljs-comment"># Show compute instances within each GPU instance</span>
nvidia-smi mig -lci

<span class="hljs-comment"># Detailed view with memory and compute info</span>
nvidia-smi -L
</code></pre>
<p>This is the ground truth. If the hardware says seven slices but Kubernetes only sees five, you know where to start debugging.</p>
<h2 id="heading-what-we-learned-the-hard-way">What we learned the hard way</h2>
<p>Here's what bit us during implementation:</p>
<h3 id="heading-gpu-compatibility-this-one-got-us">GPU compatibility (this one got us)</h3>
<p>Not all NVIDIA GPUs support MIG. We initially tried g5 instances with A10G GPUs. They're Ampere architecture, so MIG should work, right? Wrong. MIG requires compute capability 8.0+, but that's necessary, not sufficient. The A10G doesn't have the hardware partitioning silicon. Check the <a target="_blank" href="https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-gpus.html">supported GPUs list</a> before picking instance types. On AWS, p4d (A100) and p5 (H100) work. g5 (A10G) doesn't.</p>
<h3 id="heading-spot-capacity-is-a-mirage">Spot capacity is a mirage</h3>
<p>On paper, Spot pricing for p4d.24xlarge looks amazing: $2.48/hr versus $32/hr for On-Demand. That's 92% savings. In practice? We spent hours watching <code>UnfulfillableCapacity</code> errors scroll by.</p>
<p>The p4d.24xlarge packs 8x A100 40GB GPUs, 1.1TB RAM, and 400Gbps networking. Everyone wants these. Spot pools are perpetually exhausted.</p>
<p>What actually works: start with On-Demand (yes, it's expensive), check capacity across regions (us-west-2, eu-west-1, and ap-southeast-1 tend to have better availability), and use <a target="_blank" href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-capacity-reservations.html">capacity reservations</a> for anything critical. Don't plan to scale from zero to ten GPU nodes quickly. The capacity probably isn't there.</p>
<p>If you do get Spot capacity, spread instances across availability zones, implement graceful shutdown handlers for the <a target="_blank" href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html">2-minute warning</a>, and keep a small On-Demand node group as fallback. <a target="_blank" href="https://docs.aws.amazon.com/eventbridge/latest/userguide/aws-events.html">EventBridge</a> can give you advance notice of interruptions.</p>
<h3 id="heading-al2023-ami-quirks">AL2023 AMI quirks</h3>
<p>The <code>AL2023_x86_64_NVIDIA</code> AMI comes with NVIDIA drivers and Container Toolkit pre-installed. This is convenient until you realize the GPU Operator will try to install them again. Disable both (<code>driver.enabled: false</code>, <code>toolkit.enabled: false</code>) or watch things break mysteriously. Also set <code>operator.defaultRuntime: "containerd"</code> for cgroup v2 support.</p>
<h3 id="heading-mig-strategy-mismatch">MIG strategy mismatch</h3>
<p>This one had us scratching our heads. The GPU node came up, MIG Manager reported success, but the node label showed <code>NVIDIA-A100-SXM4-40GB-MIG-INVALID</code> and Kubernetes saw zero GPU resources. The culprit: we had <code>mig.strategy: "single"</code> but were using <code>all-balanced</code>, which creates multiple slice types. GFD expects uniform slices with <code>single</code> and marks anything else as invalid. Switch to <code>strategy: "mixed"</code> for heterogeneous profiles. Check with <code>kubectl logs -n gpu-operator -l app=gpu-feature-discovery</code> if you hit this.</p>
<h3 id="heading-dra-api-changes-from-beta">DRA API changes from beta</h3>
<p>Kubernetes 1.34 promoted DRA to GA, which means breaking changes from v1beta1. ResourceClaimTemplate now requires an <code>exactly</code> block with a <code>count</code> field. DeviceClass and ResourceClaim moved from <code>v1beta1</code> to <code>v1</code>. If you're migrating from an earlier setup, your manifests need updating.</p>
<h2 id="heading-where-to-go-from-here">Where to go from here</h2>
<p>DRA with MIG solves a problem that's been frustrating platform teams for years: how do you share expensive GPU hardware without losing isolation? The answer turns out to be hardware partitioning plus a scheduler that actually understands what it's allocating.</p>
<p>The setup isn't trivial. Between GPU compatibility gotchas, Spot capacity constraints, AL2023 AMI configuration, and MIG strategy mismatches, there's real complexity here. But once it's running, your ML teams can request GPU resources without filing tickets, and you can stop watching $32/hr hardware sit 90% idle.</p>
<p>The code in this post is available to adapt for your own infrastructure. For more details, see the <a target="_blank" href="https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/">Kubernetes DRA documentation</a>, the <a target="_blank" href="https://awslabs.github.io/ai-on-eks/docs/guidance/dynamic-resource-allocation">AWS Labs DRA guide</a>, and the <a target="_blank" href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/">NVIDIA GPU Operator docs</a>. The <a target="_blank" href="https://github.com/NVIDIA/k8s-dra-driver">NVIDIA DRA Driver repo</a> has more CEL expression examples.</p>
]]></content:encoded></item><item><title><![CDATA[Argo CD: Enable Helm Support in Kustomize]]></title><description><![CDATA[TL;DR: The code
https://github.com/dirien/quick-bites
 
Nothing is more controversial in the Kubernetes community than whether to use Helm or Kustomize.
I always advocate the philosophy of using the right tool for the right job. It avoids the problem...]]></description><link>https://blog.ediri.io/argo-cd-enable-helm-support-in-kustomize</link><guid isPermaLink="true">https://blog.ediri.io/argo-cd-enable-helm-support-in-kustomize</guid><category><![CDATA[ArgoCD]]></category><category><![CDATA[gitops]]></category><category><![CDATA[Helm]]></category><category><![CDATA[Kustomize]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sun, 24 Nov 2024 17:27:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732468886226/d0860fba-905b-49bb-847e-07107551b4e9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-the-code">TL;DR: The code</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites">https://github.com/dirien/quick-bites</a></div>
<p> </p>
<p>Nothing is more controversial in the Kubernetes community than whether to use <a target="_blank" href="https://helm.sh/">Helm</a> or <a target="_blank" href="https://kustomize.io/">Kustomize</a>.</p>
<p>I always advocate the philosophy of <em>using the right tool for the right job</em>. It avoids the problem of <em>everything looks like a nail when you have a hammer</em>.</p>
<p>Yet, sometimes you need to combine similar tools to get the best results. Helm and Kustomize are two tools, when combined smartly, they can boost productivity and flexibility.</p>
<p>But, before we jump into the details, let's first understand what Helm and Kustomize are and what they do.</p>
<h2 id="heading-helm">Helm</h2>
<p>Helm is the de facto default package manager for Kubernetes applications. It provides an efficient way to package, share, and deploy your Kubernetes applications. It can be used via the CLI or through first-class support in CD tools like <a target="_blank" href="https://argoproj.github.io/cd/">Argo CD</a> or <a target="_blank" href="https://fluxcd.io/">Flux</a>. One of the key features I like about Helm is its powerful templating engine. It lets you create reusable templates for your Kubernetes resources. This keeps your YAML files <a target="_blank" href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a> (Don't Repeat Yourself). It makes it easier to manage and maintain your Kubernetes resources.</p>
<p>Example of the template in Helm I took from the <a target="_blank" href="https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/templates/prometheus/servicemonitor.yaml">kube-prometheus-stack</a></p>
<pre><code class="lang-yaml">{{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">and</span> <span class="hljs-string">.Values.prometheus.enabled</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.selfMonitor</span>}}
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">monitoring.coreos.com/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ServiceMonitor</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> {{<span class="hljs-string">template</span> <span class="hljs-string">"kube-prometheus-stack.fullname"</span> <span class="hljs-string">.</span>}}<span class="hljs-string">-prometheus</span>
  <span class="hljs-attr">namespace:</span> {{<span class="hljs-string">template</span> <span class="hljs-string">"kube-prometheus-stack.namespace"</span> <span class="hljs-string">.</span>}}
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> {{<span class="hljs-string">template</span> <span class="hljs-string">"kube-prometheus-stack.name"</span> <span class="hljs-string">.</span>}}<span class="hljs-string">-prometheus</span>
  {{<span class="hljs-string">include</span> <span class="hljs-string">"kube-prometheus-stack.labels"</span> <span class="hljs-string">.</span> <span class="hljs-string">|</span> <span class="hljs-string">indent</span> <span class="hljs-number">4</span>}}
  {{<span class="hljs-bullet">-</span> <span class="hljs-string">with</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.additionalLabels</span>}}
  {{<span class="hljs-bullet">-</span> <span class="hljs-string">toYaml</span> <span class="hljs-string">.</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">4</span>}}
  {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
<span class="hljs-attr">spec:</span> {{<span class="hljs-bullet">-</span> <span class="hljs-string">include</span> <span class="hljs-string">"servicemonitor.scrapeLimits"</span> <span class="hljs-string">.Values.prometheus.serviceMonitor</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">2</span>}}
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> {{<span class="hljs-string">template</span> <span class="hljs-string">"kube-prometheus-stack.name"</span> <span class="hljs-string">.</span>}}<span class="hljs-string">-prometheus</span>
      <span class="hljs-attr">release:</span> {{<span class="hljs-string">$.Release.Name</span> <span class="hljs-string">|</span> <span class="hljs-string">quote</span>}}
      <span class="hljs-attr">self-monitor:</span> <span class="hljs-string">"true"</span>
  <span class="hljs-attr">namespaceSelector:</span>
    <span class="hljs-attr">matchNames:</span>
    <span class="hljs-bullet">-</span> {{<span class="hljs-string">printf</span> <span class="hljs-string">"%s"</span> <span class="hljs-string">(include</span> <span class="hljs-string">"kube-prometheus-stack.namespace"</span> <span class="hljs-string">.)</span> <span class="hljs-string">|</span> <span class="hljs-string">quote</span>}}
  <span class="hljs-attr">endpoints:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> {{<span class="hljs-string">.Values.prometheus.prometheusSpec.portName</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.interval</span>}}
    <span class="hljs-attr">interval:</span> {{<span class="hljs-string">.Values.prometheus.serviceMonitor.interval</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.scheme</span>}}
    <span class="hljs-attr">scheme:</span> {{<span class="hljs-string">.Values.prometheus.serviceMonitor.scheme</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.tlsConfig</span>}}
    <span class="hljs-attr">tlsConfig:</span> {{<span class="hljs-bullet">-</span> <span class="hljs-string">toYaml</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.tlsConfig</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">6</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.bearerTokenFile</span>}}
    <span class="hljs-attr">bearerTokenFile:</span> {{<span class="hljs-string">.Values.prometheus.serviceMonitor.bearerTokenFile</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    <span class="hljs-attr">path:</span> <span class="hljs-string">"<span class="hljs-template-variable">{{ trimSuffix "/" .Values.prometheus.prometheusSpec.routePrefix }}</span>/metrics"</span>
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.metricRelabelings</span>}}
    <span class="hljs-attr">metricRelabelings:</span> {{<span class="hljs-bullet">-</span> <span class="hljs-string">tpl</span> <span class="hljs-string">(toYaml</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.metricRelabelings</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">6</span><span class="hljs-string">)</span> <span class="hljs-string">.</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.relabelings</span>}}
    <span class="hljs-attr">relabelings:</span> {{<span class="hljs-bullet">-</span> <span class="hljs-string">toYaml</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.relabelings</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">6</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
  <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-string">reloader-web</span>
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.interval</span>}}
    <span class="hljs-attr">interval:</span> {{<span class="hljs-string">.Values.prometheus.serviceMonitor.interval</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.scheme</span>}}
    <span class="hljs-attr">scheme:</span> {{<span class="hljs-string">.Values.prometheus.serviceMonitor.scheme</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.tlsConfig</span>}}
    <span class="hljs-attr">tlsConfig:</span> {{<span class="hljs-bullet">-</span> <span class="hljs-string">toYaml</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.tlsConfig</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">6</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    <span class="hljs-attr">path:</span> <span class="hljs-string">"/metrics"</span>
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.metricRelabelings</span>}}
    <span class="hljs-attr">metricRelabelings:</span> {{<span class="hljs-bullet">-</span> <span class="hljs-string">tpl</span> <span class="hljs-string">(toYaml</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.metricRelabelings</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">6</span><span class="hljs-string">)</span> <span class="hljs-string">.</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.relabelings</span>}}
    <span class="hljs-attr">relabelings:</span> {{<span class="hljs-bullet">-</span> <span class="hljs-string">toYaml</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.relabelings</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">6</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
  {{<span class="hljs-bullet">-</span> <span class="hljs-string">range</span> <span class="hljs-string">.Values.prometheus.serviceMonitor.additionalEndpoints</span>}}
  <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> {{<span class="hljs-string">.port</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">or</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.interval</span> <span class="hljs-string">.interval</span>}}
    <span class="hljs-attr">interval:</span> {{<span class="hljs-string">default</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.interval</span> <span class="hljs-string">.interval</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">or</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.proxyUrl</span> <span class="hljs-string">.proxyUrl</span>}}
    <span class="hljs-attr">proxyUrl:</span> {{<span class="hljs-string">default</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.proxyUrl</span> <span class="hljs-string">.proxyUrl</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">or</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.scheme</span> <span class="hljs-string">.scheme</span>}}
    <span class="hljs-attr">scheme:</span> {{<span class="hljs-string">default</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.scheme</span> <span class="hljs-string">.scheme</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">or</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.bearerTokenFile</span> <span class="hljs-string">.bearerTokenFile</span>}}
    <span class="hljs-attr">bearerTokenFile:</span> {{<span class="hljs-string">default</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.bearerTokenFile</span> <span class="hljs-string">.bearerTokenFile</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">or</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.tlsConfig</span> <span class="hljs-string">.tlsConfig</span>}}
    <span class="hljs-attr">tlsConfig:</span> {{<span class="hljs-bullet">-</span> <span class="hljs-string">default</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.tlsConfig</span> <span class="hljs-string">.tlsConfig</span> <span class="hljs-string">|</span> <span class="hljs-string">toYaml</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">6</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    <span class="hljs-attr">path:</span> {{<span class="hljs-string">.path</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">or</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.metricRelabelings</span> <span class="hljs-string">.metricRelabelings</span>}}
    <span class="hljs-attr">metricRelabelings:</span> {{<span class="hljs-bullet">-</span> <span class="hljs-string">tpl</span> <span class="hljs-string">(default</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.metricRelabelings</span> <span class="hljs-string">.metricRelabelings</span> <span class="hljs-string">|</span> <span class="hljs-string">toYaml</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">6</span><span class="hljs-string">)</span> <span class="hljs-string">.</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">if</span> <span class="hljs-string">or</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.relabelings</span> <span class="hljs-string">.relabelings</span>}}
    <span class="hljs-attr">relabelings:</span> {{<span class="hljs-bullet">-</span> <span class="hljs-string">default</span> <span class="hljs-string">$.Values.prometheus.serviceMonitor.relabelings</span> <span class="hljs-string">.relabelings</span> <span class="hljs-string">|</span> <span class="hljs-string">toYaml</span> <span class="hljs-string">|</span> <span class="hljs-string">nindent</span> <span class="hljs-number">6</span>}}
    {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
  {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
  {{<span class="hljs-bullet">-</span> <span class="hljs-string">end</span>}}
</code></pre>
<p>As powerful as Helm is, it has its limitations. One of them is the customisation of third-party Helm charts. And this is where Kustomize comes in. But more on that later.</p>
<h2 id="heading-kustomize">Kustomize</h2>
<p>Kustomize lets you, as the name suggests, customise your raw, template-free YAML files. It does not alter the original YAML files but instead applies patches to them. Like Helm, Kustomize has a stand-alone CLI tool. It also has first-class integrations in <a target="_blank" href="https://kubernetes.io/docs/reference/kubectl/">kubectl</a> and CD tools like Argo CD and Flux. There are different ways to use Kustomize. The most common variation is to use Kustomize with a base and overlay structure. The base has the raw, template-free YAML files. The overlay has the patches to apply to the base.</p>
<p>Example file structure:</p>
<pre><code class="lang-bash">~/someApp
├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    ├── development
    │   ├── cpu_count.yaml
    │   ├── kustomization.yaml
    │   └── replica_count.yaml
    └── production
        ├── cpu_count.yaml
        ├── kustomization.yaml
        └── replica_count.yaml
</code></pre>
<p>Keep in mind, that you need to have the <code>kustomization.yaml</code> file in each directory to tell Kustomize which resources to apply and which patches to use.</p>
<p>Example of a <code>kustomization.yaml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">kustomize.config.k8s.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Kustomization</span>

<span class="hljs-attr">labels:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">pairs:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">someApp</span>

<span class="hljs-attr">resources:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">deployment.yaml</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">service.yaml</span>
</code></pre>
<p>This example will apply the <code>deployment.yaml</code> and <code>service.yaml</code> files in the base directory and add the label <code>app: someApp</code> to each resource.</p>
<h2 id="heading-combining-helm-and-kustomize">Combining Helm and Kustomize</h2>
<p>Now that we know the basics of Helm and Kustomize, let's combine them to get the best of both worlds. Before we use Argo CD to deploy our application, let's create a demo use case.</p>
<h3 id="heading-use-case">Use Case</h3>
<p>Think about a scenario where you would like to use the new <code>Gateway API</code> from Kubernetes. Unfortunately, most of the Helm charts do not support the <code>Gateway API</code> yet.</p>
<p>Now this is where Kustomize comes in. As an example of this use case, I am going to deploy a <code>Minecraft</code> server.</p>
<h3 id="heading-create-the-kustomize-project">Create the Kustomize Project</h3>
<p>First, let's create a new Kustomize project. We will use the <code>kustomize create</code> command to create a new project.</p>
<pre><code class="lang-bash">kustomize init
</code></pre>
<p>Now open the <code>kustomization.yaml</code> file and add the following content:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">kustomize.config.k8s.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Kustomization</span>

<span class="hljs-attr">resources:</span>
<span class="hljs-bullet">-</span> <span class="hljs-string">base/gateway.yaml</span>

<span class="hljs-attr">helmCharts:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">minecraft</span>
  <span class="hljs-attr">releaseName:</span> <span class="hljs-string">my</span>
  <span class="hljs-attr">repo:</span> <span class="hljs-string">https://itzg.github.io/minecraft-server-charts/</span>
  <span class="hljs-attr">version:</span> <span class="hljs-number">4.23</span><span class="hljs-number">.2</span>
  <span class="hljs-attr">valuesInline:</span>
    <span class="hljs-attr">minecraftServer:</span>
      <span class="hljs-attr">eula:</span> <span class="hljs-string">"true"</span>
</code></pre>
<p>Create the <code>base</code> directory and add the <code>gateway.yaml</code> file with the following content:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">kind:</span> <span class="hljs-string">GatewayClass</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">gateway.networking.k8s.io/v1</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">eg</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">controllerName:</span> <span class="hljs-string">gateway.envoyproxy.io/gatewayclass-controller</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">gateway.networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Gateway</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">minecraft-gateway</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">gatewayClassName:</span> <span class="hljs-string">eg</span>
  <span class="hljs-attr">listeners:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">minecraft</span>
    <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
    <span class="hljs-attr">port:</span> <span class="hljs-number">8088</span>
    <span class="hljs-attr">allowedRoutes:</span>
      <span class="hljs-attr">kinds:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">kind:</span> <span class="hljs-string">TCPRoute</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">gateway.networking.k8s.io/v1alpha2</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">TCPRoute</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">minecraft-route</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">parentRefs:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">minecraft-gateway</span>
    <span class="hljs-attr">sectionName:</span> <span class="hljs-string">minecraft</span>
  <span class="hljs-attr">rules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">backendRefs:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">my-minecraft</span>
      <span class="hljs-attr">port:</span> <span class="hljs-number">25565</span>
</code></pre>
<p>Now let's apply the Kustomize project:</p>
<pre><code class="lang-bash">kustomize build --enable-helm | kubectl apply -f -
</code></pre>
<p>Important to note is the <code>--enable-helm</code> flag. This flag tells Kustomize to enable Helm support.</p>
<p>So far, so good. We have deployed a Minecraft server using Helm. We added the missing <code>Gateway API</code> support using Kustomize.</p>
<p>The question now is, how can we deploy this application using Argo CD?</p>
<h2 id="heading-deploy-the-application-using-argo-cd">Deploy the Application using Argo CD</h2>
<p>First we need to deploy Argo CD using Helm and following content in the <code>values.yaml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">configs:</span>
  <span class="hljs-attr">cm:</span>
    <span class="hljs-attr">kustomize.buildOptions:</span> <span class="hljs-string">"--enable-helm"</span>
  <span class="hljs-comment"># the rest of the values are only for demonstration purposes</span>
  <span class="hljs-attr">secret:</span>
    <span class="hljs-attr">argocdServerAdminPassword:</span> <span class="hljs-string">"$2a$10$RjjTokiJSaTQt8jAMOUTK.O0VIZ3.0AEs3/JxtaFKGZir93yFPEOG"</span>
    <span class="hljs-attr">argocdServerAdminPasswordMtime:</span> <span class="hljs-string">"2023-11-13T09:23:16Z"</span>
  <span class="hljs-attr">params:</span>
    <span class="hljs-attr">"server.insecure":</span> <span class="hljs-literal">true</span>
</code></pre>
<p>The most important part is the <code>kustomize.buildOptions: "--enable-helm"</code> configuration. This tells Argo CD to enable Helm support in Kustomize.</p>
<p>Now we can deploy Argo CD and the <a target="_blank" href="https://www.envoyproxy.io/">Envoy Gateway</a> using Helm:</p>
<pre><code class="lang-bash">helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm upgrade -i my-argo-cd argo/argo-cd -f values.yaml --namespace argocd --create-namespace
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.2.1 -n envoy-gateway-system --create-namespace
</code></pre>
<p>After the deployment is finished we can create a new Argo CD application using the following content:</p>
<pre><code class="lang-bash">kubectl apply -f minecraft-application.yaml
</code></pre>
<p>With the following content in the <code>minecraft-application.yaml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">argoproj.io/v1alpha1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Application</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">minecraft-application</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">argocd</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">destination:</span>
    <span class="hljs-attr">namespace:</span> <span class="hljs-string">minecraft</span>
    <span class="hljs-attr">server:</span> <span class="hljs-string">https://kubernetes.default.svc</span>
  <span class="hljs-attr">project:</span> <span class="hljs-string">default</span>
  <span class="hljs-attr">source:</span>
    <span class="hljs-attr">repoURL:</span> <span class="hljs-string">https://github.com/dirien/quick-bites.git</span>
    <span class="hljs-attr">targetRevision:</span> <span class="hljs-string">main</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">argocd-kustomize-helms-support</span>
  <span class="hljs-attr">syncPolicy:</span>
    <span class="hljs-attr">automated:</span>
      <span class="hljs-attr">prune:</span> <span class="hljs-literal">true</span>
      <span class="hljs-attr">selfHeal:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">syncOptions:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">ServerSideApply=true</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">CreateNamespace=true</span>
</code></pre>
<p>With this setup, we have successfully deployed a Minecraft server using Helm and added the missing <code>Gateway API</code> support. You can check the <code>TCPRoute</code> and <code>Gateway</code> resources by running the following command:</p>
<pre><code class="lang-bash">kubectl get gateway,tcproute -n minecraft

NAME                                                  CLASS   ADDRESS   PROGRAMMED   AGE
gateway.gateway.networking.k8s.io/minecraft-gateway   eg                False        5m19s

NAME                                                 AGE
tcproute.gateway.networking.k8s.io/minecraft-route   5m19s
</code></pre>
<p>Start yor Minecraft client and connect to the server using the IP address of the <code>minecraft-gateway</code> resource and the port. In this case it would be <code>8088</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732464726592/3cd38ce9-3528-4f62-b68a-4d45a0e777a6.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732464732997/6638aa6f-11f7-494d-903c-1371ac966cad.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>Note: Enabling Helm support in every app has a slight performance cost. If you want more control over which Applications to use, create an Argo CD <code>ConfigurationManagementPlugin</code>.</p>
</blockquote>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Combining Helm and Kustomize can be a powerful way to deploy your app. Use it when you need to customize third-party Helm charts. Or, if you prefer Kustomize and don't want to write Helm charts for your own deployments.</p>
]]></content:encoded></item><item><title><![CDATA[Advanced Secret Management on Kubernetes with Pulumi: External Secrets Operator]]></title><description><![CDATA[TL;DR Le code
https://github.com/dirien/quick-bites
 
Introduction
This article is part three of my series on secret management on Kubernetes with the help of Pulumi. In my first article, we talked about the Sealed Secrets controller. The second arti...]]></description><link>https://blog.ediri.io/advanced-secret-management-on-kubernetes-with-pulumi-external-secrets-operator</link><guid isPermaLink="true">https://blog.ediri.io/advanced-secret-management-on-kubernetes-with-pulumi-external-secrets-operator</guid><category><![CDATA[Pulumi]]></category><category><![CDATA[gitops]]></category><category><![CDATA[Security]]></category><category><![CDATA[secrets management]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sun, 06 Oct 2024 12:07:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728216208290/fa3a6a8c-c6a7-4a13-8e79-0e975d51ec5e.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-le-code">TL;DR Le code</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites">https://github.com/dirien/quick-bites</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>This article is part three of my series on secret management on Kubernetes with the help of <a target="_blank" href="https://www.pulumi.com/">Pulumi</a>. In my first article, we talked about the <mark>Sealed Secrets</mark> controller. The second article, we talked about the <mark>Secrets Store CSI Driver</mark> and how it compares to the <mark>Sealed Secrets</mark> controller.</p>
<p>If you haven't read those articles yet, I recommend you do so before continuing with this one:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.ediri.io/advanced-secret-management-on-kubernetes-with-pulumi-and-gitops-sealed-secrets-controller">https://blog.ediri.io/advanced-secret-management-on-kubernetes-with-pulumi-and-gitops-sealed-secrets-controller</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.ediri.io/advanced-secret-management-on-kubernetes-with-pulumi-secrets-store-csi-driver">https://blog.ediri.io/advanced-secret-management-on-kubernetes-with-pulumi-secrets-store-csi-driver</a></div>
<p> </p>
<p>And as we saw in the previous articles, managing secrets in <a target="_blank" href="https://kubernetes.io/">Kubernetes</a> can be a pain in the neck. But we all agree. Proper secret management is vital for our apps and infrastructure security. Especially, and now comes the important part, if we store our secrets in a Git repository. This is also an essential part of the <a target="_blank" href="https://opengitops.dev/">GitOps</a> workflow. We saw that the <mark>Sealed Secrets</mark> controller could encrypt our secrets and store them in Git. Yet, we soon recognized its flaws. Think about the nightmare of managing keys for many projects, clusters, and secrets used by various teams. And, rotating them.</p>
<p>The second solution we saw was the <mark>Secrets Store CSI Driver</mark>. This solution is great. But you must build your app with the <mark>Secrets Store CSI Driver</mark> in mind to retrieve the secrets from a file. You could argue this is manageable.</p>
<p>But what about external tools that don't support this driver?</p>
<h3 id="heading-what-is-the-external-secrets-operator">What is the External Secrets Operator?</h3>
<p>Here comes the <mark>External Secrets Operator (ESO)</mark> to the rescue. The ESO is build following the <a target="_blank" href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/">Kubernetes operator pattern</a> and manages secrets in a highly secure and scalable way. ESO syncs secrets in external systems, like Pulumi ESC, HashiCorp Vault, AWS Secrets Manager, and Azure Key Vault, into Kubernetes secrets. This lets us tame secret sprawl. It centralizes secret management in one place. It also provides a secure, controlled way to access them.</p>
<p>ESO offers us the following benefits:</p>
<ul>
<li><p>By leveraging external secret management systems, ESO acts as a bridge between Kubernetes and external secret management.</p>
</li>
<li><p>With ESO, we don't have any manual intervention to manage secrets in Kubernetes. Everything is automated and managed by ESO.</p>
</li>
<li><p>ESO providers support secret rotation when the external secret management system supports it.</p>
</li>
<li><p>Through the unified interface ESO provides, handling cross-cluster and cross-cloud secret management becomes straightforward and independent of the underlying infrastructure.</p>
</li>
</ul>
<h3 id="heading-the-architecture-of-the-external-secrets-operator">The Architecture of the External Secrets Operator</h3>
<p>ESO is built on top of the Kubernetes operator pattern and extends the Kubernetes API with several different custom resource definitions (CRDs) to manage secrets. ESO is watching for these CRDs, and when secrets at the external secret management system match the defined CRDs, ESO will synchronize the secrets into Kubernetes secrets. Now we have the secret as part of the Kubernetes reconciliation loop. This means that the secret is always up to date and in sync with the external secret management system.</p>
<p><img src="https://external-secrets.io/latest/pictures/diagrams-high-level-simple.png" alt="high-level" /></p>
<p>Custom Resource Definitions (CRDs) of the External Secrets Operator</p>
<p>Following CRDs, you should know when working with ESO:</p>
<ul>
<li><p><code>(Cluster)SecretStore</code>: This CRD defines the external secret management system that ESO should use to manage secrets. It contains the configuration to connect to the external secret management system. <code>SecretStore</code> is namespaced, while the <code>ClusterSecretStore</code> is cluster-scoped, which means that it can be used across many namespaces.</p>
</li>
<li><p><code>(Cluster)ExternalSecret</code>: This CRD defines the secret that should be synchronized into Kubernetes. It contains the reference to the <code>SecretStore</code> and the path to the secret in the external secret management system. <code>ExternalSecret</code> is namespaced, while the <code>ClusterExternalSecret</code> is cluster-scoped, which means that it can be used across many namespaces.</p>
</li>
</ul>
<p><img src="https://external-secrets.io/latest/pictures/diagrams-component-overview.png" alt="Component Overview" /></p>
<h2 id="heading-setting-up-the-external-secrets-operator">Setting Up the External Secrets Operator</h2>
<p>Like the other two articles, we will use <mark>Pulumi</mark> to deploy the External Secrets Operator to our Kubernetes cluster. This time, I am going to use <mark>DigitalOcean</mark> as the cloud provider and <mark>Pulumi ESC</mark> as our external secret management system. But you can use any other cloud provider and external secret management system you like.</p>
<h3 id="heading-what-is-pulumi-esc">What is Pulumi ESC?</h3>
<p>Pulumi ESC provides a comprehensive solution for managing secrets, environments, and configurations. While it integrates seamlessly with Pulumi IaC projects through pulumiConfig, exposing stored values to your Pulumi stacks, its functionality extends beyond this use case.</p>
<p>As a standalone service, Pulumi ESC supports diverse applications with dedicated SDKs for various programming languages. Additionally, it offers a CLI for command-line management of secrets and configurations, enabling context creation for CLI tools like Terraform.</p>
<p>This new secrets management and orchestration service can be employed both within and outside of Pulumi's Infrastructure as Code ecosystem. To explore Pulumi ESC's capabilities further, consult the official <a target="_blank" href="https://www.pulumi.com/docs/esc/">documentation</a>.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>To follow this article, you will need the following:</p>
<ul>
<li><p><a target="_blank" href="https://www.pulumi.com/docs/get-started/install/">Pulumi CLI</a> is installed.</p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">kubectl</a> installed.</p>
</li>
<li><p>optional <a target="_blank" href="https://k9scli.io/topics/install/">K9s</a>, if you want to quickly interact with your cluster.</p>
</li>
<li><p><a target="_blank" href="https://try.digitalocean.com/cloud/">DigitalOcean</a> account.</p>
</li>
</ul>
<p>I will skip the whole Pulumi installation and config part. I explained this in my previous posts.</p>
<h3 id="heading-create-a-new-pulumi-project">Create a new Pulumi project</h3>
<p>In the previous articles, I used <code>TypeScript</code> and <code>Go</code>, but to show you that you can use a lot of different languages with Pulumi, it's time for <mark>Python</mark>.</p>
<p>We start by creating a new Pulumi project:</p>
<pre><code class="lang-shell">mkdir pulumi-external-secret-operator
cd pulumi-external-secret-operator
pulumi new python --force
</code></pre>
<p>Leave the default values and at the end you should see something like this:</p>
<pre><code class="lang-shell">pulumi new digitalocean-python --force
This command will walk you through creating a new Pulumi project.

Enter a value or leave blank to accept the (default), and press &lt;ENTER&gt;.
Press ^C at any time to quit.

Project name (pulumi-external-secret-operator):
Project description (A minimal DigitalOcean Python Pulumi program):
Created project 'pulumi-external-secret-operator'

Please enter your desired stack name.
To create a stack in an organization, use the format &lt;org-name&gt;/&lt;stack-name&gt; (e.g. `acmecorp/dev`).
Stack name (dev):
Created stack 'dev'

The toolchain to use for installing dependencies and running the program pip
Installing dependencies...

Creating virtual environment...
Finished creating virtual environment
Updating pip, setuptools, and wheel in virtual environment...
Requirement already satisfied: pip in ./venv/lib/python3.12/site-packages (24.2)
Collecting setuptools
  Using cached setuptools-75.1.0-py3-none-any.whl.metadata (6.9 kB)
Collecting wheel
  Using cached wheel-0.44.0-py3-none-any.whl.metadata (2.3 kB)
Using cached setuptools-75.1.0-py3-none-any.whl (1.2 MB)
Using cached wheel-0.44.0-py3-none-any.whl (67 kB)
Installing collected packages: wheel, setuptools
Successfully installed setuptools-75.1.0 wheel-0.44.0
Finished updating
Installing dependencies in virtual environment...
Collecting pulumi&lt;4.0.0,&gt;=3.0.0 (from -r requirements.txt (line 1))
Downloading pulumi-3.135.1-py3-none-any.whl (273 kB)
Downloading pulumi_digitalocean-4.33.0-py3-none-any.whl (377 kB)
Installing collected packages: arpeggio, six, semver, pyyaml, protobuf, grpcio, dill, debugpy, attrs, pulumi, parver, pulumi-digitalocean
Successfully installed arpeggio-2.0.2 attrs-24.2.0 debugpy-1.8.6 dill-0.3.9 grpcio-1.60.2 parver-0.5 protobuf-4.25.5 pulumi-3.135.1 pulumi-digitalocean-4.33.0 pyyaml-6.0.2 semver-2.13.0 six-1.16.0
Finished installing dependencies
Finished installing dependencies

Your new project is ready to go! ✨

To perform an initial deployment, run `pulumi up`
</code></pre>
<h3 id="heading-writing-the-pulumi-code-to-deploy-the-external-secrets-operator">Writing the Pulumi code to deploy the External Secrets Operator</h3>
<p>After the project is created, we can start writing our code. You should see the following files in your project:</p>
<pre><code class="lang-shell">tree -L 1
.
├── Pulumi.yaml
├── __main__.py
├── requirements.txt
└── venv

2 directories, 3 files
</code></pre>
<p>Add the <code>pulumi-kubernetes</code> dependencies to your <code>requirements.txt</code> file:</p>
<pre><code class="lang-shell">pulumi&gt;=3.0.0,&lt;4.0.0
pulumi-digitalocean&gt;=4.0.0,&lt;5.0.0
pulumi-kubernetes==4.18.1
</code></pre>
<p>Install the dependencies:</p>
<pre><code class="lang-shell">./venv/bin/pip install -r requirements.txt
</code></pre>
<p>Replace the content of the <code>__main__.py</code> file with the following code:</p>
<pre><code class="lang-python"><span class="hljs-string">"""A DigitalOcean Python Pulumi program"""</span>

<span class="hljs-keyword">import</span> pulumi
<span class="hljs-keyword">import</span> pulumi_digitalocean <span class="hljs-keyword">as</span> digitalocean
<span class="hljs-keyword">import</span> pulumi_kubernetes <span class="hljs-keyword">as</span> kubernetes

do_cluster = digitalocean.KubernetesCluster(
    <span class="hljs-string">"do_cluster"</span>,
    name=<span class="hljs-string">"esc-cluster"</span>,
    region=<span class="hljs-string">"nyc1"</span>,
    version=<span class="hljs-string">"1.31.1-do.1"</span>,
    destroy_all_associated_resources=<span class="hljs-literal">True</span>,
    node_pool=digitalocean.KubernetesClusterNodePoolArgs(
        name=<span class="hljs-string">"default"</span>, size=<span class="hljs-string">"s-2vcpu-2gb"</span>, node_count=<span class="hljs-number">1</span>
    ),
)

do_k8s_provider = kubernetes.Provider(
    <span class="hljs-string">"do_k8s_provider"</span>,
    enable_server_side_apply=<span class="hljs-literal">True</span>,
    kubeconfig=do_cluster.kube_configs[<span class="hljs-number">0</span>].apply(<span class="hljs-keyword">lambda</span> config: config.raw_config),
)

namespace = kubernetes.core.v1.Namespace(
    <span class="hljs-string">"external-secrets"</span>,
    metadata={
        <span class="hljs-string">"name"</span>: <span class="hljs-string">"external-secrets"</span>,
    },
    opts=pulumi.ResourceOptions(provider=do_k8s_provider),
)

<span class="hljs-comment"># Deploy a Helm release into the namespace</span>
external_secrets = kubernetes.helm.v3.Release(
    <span class="hljs-string">"external-secrets"</span>,
    chart=<span class="hljs-string">"external-secrets"</span>,
    version=<span class="hljs-string">"0.10.4"</span>,  <span class="hljs-comment"># Specify the version of the chart</span>
    namespace=namespace.metadata[<span class="hljs-string">"name"</span>],
    repository_opts={
        <span class="hljs-string">"repo"</span>: <span class="hljs-string">"https://charts.external-secrets.io"</span>,
    },
    opts=pulumi.ResourceOptions(provider=do_k8s_provider),
)

<span class="hljs-comment"># Deploy a secret into the namespace</span>
pulumi_access_token = pulumi.Config().require(<span class="hljs-string">"pulumi-pat"</span>)

my_secret = kubernetes.core.v1.Secret(
    <span class="hljs-string">"my-secret"</span>,
    metadata={
        <span class="hljs-string">"namespace"</span>: namespace.metadata[<span class="hljs-string">"name"</span>],
        <span class="hljs-string">"name"</span>: <span class="hljs-string">"pulumi-access-token"</span>,
    },
    string_data={
        <span class="hljs-string">"PULUMI_ACCESS_TOKEN"</span>: pulumi_access_token,
    },
    type=<span class="hljs-string">"Opaque"</span>,
    opts=pulumi.ResourceOptions(provider=do_k8s_provider),
)

pulumi.export(<span class="hljs-string">"kubeconfig"</span>, do_k8s_provider.kubeconfig)
</code></pre>
<p>This code creates a new DigitalOcean Kubernetes cluster. It has one node and a size of s-2vcpu-2gb. We also create a dedicated Kubernetes provider for this cluster as we need it later to deploy the External Secrets Operator, the ClusterSecretStore, and ExternalSecret.</p>
<p>Before we can deploy and create the cluster, we need to provide the DigitalOcean token. There are different ways to achieve this. You can set the token as an environment variable or use the Pulumi configuration.</p>
<p>I am going to use the most secure way and use Pulumi ESC to store the token and provide it to the Pulumi stack. To do this, you need to create a new Pulumi ESC environment. You can do this by running the following command:</p>
<pre><code class="lang-shell">pulumi env init &lt;your-org&gt;/eso-do-cluster/eso-dev
</code></pre>
<p>We define the <code>DigitalOcean</code> token inside the <code>Pulumi ESC</code> environment using following sytnax:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">values:</span>
  <span class="hljs-attr">pulumiConfig:</span>
    <span class="hljs-attr">digitalocean:token:</span>
      <span class="hljs-attr">fn::secret:</span> <span class="hljs-string">&lt;your-do-token&gt;</span>
    <span class="hljs-attr">pulumi-pat:</span>
      <span class="hljs-attr">fn::secret:</span> <span class="hljs-string">&lt;your-pulumi-pat&gt;</span>
</code></pre>
<p>Replace &lt;your-do-pat&gt; with your DigitalOcean token. You can find the token in your DigitalOcean account settings. And replace &lt;your-pulumi-pat&gt; with your <a target="_blank" href="https://www.pulumi.com/docs/pulumi-cloud/access-management/access-tokens/">Pulumi Personal Access Token</a>.</p>
<p>To apply the configuration, run the pulumi env edit command and copy the above YAML into the editor:</p>
<pre><code class="lang-bash">pulumi env edit &lt;your-org&gt;/eso-do-cluster/eso-dev
</code></pre>
<p>Last step is to link the <code>Pulumi ESC</code> environment to the Pulumi stack by creating a new <code>Pulumi.dev.yaml</code> file:</p>
<pre><code class="lang-shell">cat &lt;&lt;EOF &gt; Pulumi.dev.yaml
environment:
- eso-do-cluster/eso-dev
EOF
</code></pre>
<p>Now we can deploy the stack:</p>
<pre><code class="lang-shell">pulumi up
</code></pre>
<p>You can check that everything is running by running the following command:</p>
<pre><code class="lang-shell">pulumi stack output kubeconfig --show-secrets &gt; kubeconfig.yaml
kubectl --kubeconfig kubeconfig.yaml get secret -n external-secrets pulumi-access-token -o jsonpath='{.data.PULUMI_ACCESS_TOKEN}' | base64 -d
</code></pre>
<p>You should see your Pulumi Personal Access Token printed to the console.</p>
<h3 id="heading-create-an-external-secret-and-use-it-in-a-deployment">Create an external secret and use it in a deployment.</h3>
<p>Now that the External Secrets Operator is running, we can create an ExternalSecret and use it in a deployment.</p>
<p>For this, we are going to create a new ESC project called eso-to-esc-app and a development environment:</p>
<pre><code class="lang-shell">pulumi env init &lt;your-org&gt;/eso-to-esc-app/dev
</code></pre>
<p>Add the following values to the <code>Pulumi ESC</code> environment:</p>
<pre><code class="lang-bash">values:
  app:
    hello: world
    hello-secret:
      fn::secret: world
</code></pre>
<p>Then run this command:</p>
<pre><code class="lang-shell">pulumi env edit &lt;your-org&gt;/eso-to-esc-app/dev
</code></pre>
<p>Now we can create the <code>ClusterSecretStore</code>:</p>
<pre><code class="lang-python"><span class="hljs-string">"""A DigitalOcean Python Pulumi program"""</span>

<span class="hljs-keyword">import</span> pulumi
<span class="hljs-keyword">import</span> pulumi_digitalocean <span class="hljs-keyword">as</span> digitalocean
<span class="hljs-keyword">import</span> pulumi_kubernetes <span class="hljs-keyword">as</span> kubernetes

<span class="hljs-comment"># Cut for brevity</span>

cluster_secret_store = kubernetes.apiextensions.CustomResource(
    <span class="hljs-string">"cluster-secret-store"</span>,
    api_version=<span class="hljs-string">"external-secrets.io/v1beta1"</span>,
    kind=<span class="hljs-string">"ClusterSecretStore"</span>,
    metadata=kubernetes.meta.v1.ObjectMetaArgs(
        name=<span class="hljs-string">"secret-store"</span>,
    ),
    spec={
        <span class="hljs-string">"provider"</span>: {
            <span class="hljs-string">"pulumi"</span>: {
                <span class="hljs-string">"organization"</span>: pulumi.get_organization(),
                <span class="hljs-string">"project"</span>: <span class="hljs-string">"eso-to-esc-app"</span>,
                <span class="hljs-string">"environment"</span>: <span class="hljs-string">"dev"</span>,
                <span class="hljs-string">"accessToken"</span>: {
                    <span class="hljs-string">"secretRef"</span>: {
                        <span class="hljs-string">"name"</span>: my_secret.metadata.name,
                        <span class="hljs-string">"key"</span>: <span class="hljs-string">"PULUMI_ACCESS_TOKEN"</span>,
                        <span class="hljs-string">"namespace"</span>: my_secret.metadata.namespace,
                    },
                },
            },
        },
    },
    opts=pulumi.ResourceOptions(provider=do_k8s_provider),
)
</code></pre>
<p>Finally we can create the <code>ExternalSecret</code> and wire it up to our demo application:</p>
<pre><code class="lang-python"><span class="hljs-string">"""A DigitalOcean Python Pulumi program"""</span>

<span class="hljs-keyword">import</span> pulumi
<span class="hljs-keyword">import</span> pulumi_digitalocean <span class="hljs-keyword">as</span> digitalocean
<span class="hljs-keyword">import</span> pulumi_kubernetes <span class="hljs-keyword">as</span> kubernetes

<span class="hljs-comment"># Cut for brevity</span>

external_secret = kubernetes.apiextensions.CustomResource(
    <span class="hljs-string">"external-secret"</span>,
    api_version=<span class="hljs-string">"external-secrets.io/v1beta1"</span>,
    kind=<span class="hljs-string">"ExternalSecret"</span>,
    metadata=kubernetes.meta.v1.ObjectMetaArgs(
        name=<span class="hljs-string">"esc-secret-store"</span>,
    ),
    spec={
        <span class="hljs-string">"dataFrom"</span>: [
            {
                <span class="hljs-string">"extract"</span>: {
                    <span class="hljs-string">"conversionStrategy"</span>: <span class="hljs-string">"Default"</span>,
                    <span class="hljs-string">"key"</span>: <span class="hljs-string">"app"</span>,
                }
            }
        ],
        <span class="hljs-string">"refreshInterval"</span>: <span class="hljs-string">"10s"</span>,
        <span class="hljs-string">"secretStoreRef"</span>: {
            <span class="hljs-string">"kind"</span>: cluster_secret_store.kind,
            <span class="hljs-string">"name"</span>: cluster_secret_store.metadata[<span class="hljs-string">"name"</span>],
        },
    },
    opts=pulumi.ResourceOptions(provider=do_k8s_provider),
)

hello_server_deployment = kubernetes.apps.v1.Deployment(
    <span class="hljs-string">"hello-server-deployment"</span>,
    metadata=kubernetes.meta.v1.ObjectMetaArgs(
        name=<span class="hljs-string">"hello"</span>,
        labels={<span class="hljs-string">"app"</span>: <span class="hljs-string">"hello"</span>},
    ),
    spec=kubernetes.apps.v1.DeploymentSpecArgs(
        replicas=<span class="hljs-number">1</span>,
        selector=kubernetes.meta.v1.LabelSelectorArgs(
            match_labels={<span class="hljs-string">"app"</span>: <span class="hljs-string">"hello"</span>},
        ),
        template=kubernetes.core.v1.PodTemplateSpecArgs(
            metadata=kubernetes.meta.v1.ObjectMetaArgs(
                labels={<span class="hljs-string">"app"</span>: <span class="hljs-string">"hello"</span>},
            ),
            spec=kubernetes.core.v1.PodSpecArgs(
                containers=[
                    kubernetes.core.v1.ContainerArgs(
                        name=<span class="hljs-string">"hello-server"</span>,
                        image=<span class="hljs-string">"ghcr.io/dirien/hello-server/hello-server:latest"</span>,
                        env_from=[
                            kubernetes.core.v1.EnvFromSourceArgs(
                                secret_ref=kubernetes.core.v1.SecretEnvSourceArgs(
                                    name=external_secret.metadata[<span class="hljs-string">"name"</span>]
                                )
                            )
                        ],
                        ports=[
                            kubernetes.core.v1.ContainerPortArgs(
                                container_port=<span class="hljs-number">8080</span>,
                            )
                        ],
                        resources=kubernetes.core.v1.ResourceRequirementsArgs(
                            limits=<span class="hljs-literal">None</span>,
                            requests=<span class="hljs-literal">None</span>,
                        ),
                    )
                ],
            ),
        ),
    ),
    opts=pulumi.ResourceOptions(provider=do_k8s_provider),
)
</code></pre>
<p>Deploy all the changes by running:</p>
<pre><code class="lang-shell">pulumi up
</code></pre>
<p>After the deployment is finished, you can check the logs of the <code>hello-server</code> pod by running:</p>
<pre><code class="lang-shell">pod_name=$(kubectl --kubeconfig kubeconfig.yaml get pods -l app=hello -o jsonpath='{.items[0].metadata.name}')
kubectl port-forward --kubeconfig kubeconfig.yaml $pod_name 8080:8080
</code></pre>
<p>Now you can access the <code>hello-server</code> by running:</p>
<pre><code class="lang-shell">curl http://localhost:8080/env/hello
curl http://localhost:8080/env/hello-secret

hello=world
hello-secret=world%
</code></pre>
<p>Bingo! We successfully retrieved the secret from the <code>External Secrets Operator</code> and used it in our application.</p>
<h3 id="heading-housekeeping">Housekeeping</h3>
<p>Don't forget to clean up your resources by running:</p>
<pre><code class="lang-shell">pulumi destroy
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The External Secrets Operator is a great tool to manage secrets in Kubernetes. It provides a secure and efficient way to manage secrets in a cloud-native environment, improving security, efficiency, and compliance when consuming secrets in your Kubernetes cluster. All this is done by not adding any more complexity to your applications and operations. Especially from an operational perspective, the External Secrets Operator is great as it follows the Kubernetes way of providing a declarative API to manage secrets and how it plays well with the GitOps workflow.</p>
<p>All in all, I highly recommend you give the External Secrets Operator a try and see how it can help your organization manage secrets in a secure and efficient manner.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://external-secrets.io/latest/">External Secrets Operator</a></p>
</li>
<li><p><a target="_blank" href="https://www.pulumi.com/docs/esc/">Pulumi ESC</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Rust in the Cloud: Running Rust Based Functions in AWS]]></title><description><![CDATA[TL;DR: Le Code:
https://github.com/dirien/quick-bites/tree/main/rust-in-the-cloud-aws
 
Introduction
This blog post is all about running Rust-based functions on AWS Lambda. Will see, how much effort is required to get started and what the pros and co...]]></description><link>https://blog.ediri.io/rust-in-the-cloud-running-rust-based-functions-in-aws</link><guid isPermaLink="true">https://blog.ediri.io/rust-in-the-cloud-running-rust-based-functions-in-aws</guid><category><![CDATA[Rust]]></category><category><![CDATA[AWS]]></category><category><![CDATA[lambda]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[Developer]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Thu, 02 Nov 2023 19:51:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1698955268650/f1960f51-4f38-495b-884d-00c815d24fc9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-le-code">TL;DR: Le Code:</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/rust-in-the-cloud-aws">https://github.com/dirien/quick-bites/tree/main/rust-in-the-cloud-aws</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>This blog post is all about running Rust-based functions on AWS Lambda. Will see, how much effort is required to get started and what the pros and cons are. If you are already familiar with AWS Lambdas, you may know that AWS Lambdas offers a variety of runtimes. A runtime provides a language-specific environment to run your function in. You can use this AWS provided runtimes, or you can build your own.</p>
<p>Now comes the interesting part for us: Because Rust is compiled to native code, we do not need a dedicated runtime to run our Rust code. We will use instead the <a target="_blank" href="https://github.com/awslabs/aws-lambda-rust-runtime">Rust runtime client</a> to build locally (or in your CI system) and then upload the binary using the AWS CLI by defining the AWS Lambda function. As runtime, we will use <code>provided.al2</code> which is a runtime that does not include any language-specific dependencies.</p>
<p>The demo code? A simple function that returns as AI-generated <mark>Dad Joke</mark>. The function will give us some, hopefully, funny dad jokes.</p>
<p>Additionally, we cross-compile our lambda function to <code>arm64</code> architecture to profit from the <a target="_blank" href="https://docs.aws.amazon.com/whitepapers/latest/aws-graviton-performance-testing/what-is-aws-graviton.html">AWS Graviton</a> processor. Graviton-based instances offer the best bang for your buck and are up to 40% cheaper than Intel-based solutions.</p>
<p>You are new to Rust and stumbled over this blog article? I have a whole series of blog posts about rust, if you are interested in more, check out my Rust 🦀 series:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.ediri.io/series/learning-rust">https://blog.ediri.io/series/learning-rust</a></div>
<p> </p>
<h3 id="heading-why-aws-cli">Why AWS CLI?</h3>
<p>You may ask yourself: <em>Why we are not using an infrastructure as code tool like to define and deploy our AWS Lambda function?</em></p>
<p>The reason is simple: I want to keep the blog post as simple as possible. We will use <a target="_blank" href="https://www.pulumi.com/">Pulumi</a> later in some more sophisticated blog posts. For now, we will use the AWS CLI to define our AWS Lambda function. Stay tuned for more to come.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow this blog post, you should have a basic understanding of Rust and the Cargo build tool. If you are new to Rust check out my blog post:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.ediri.io/learn-rust-in-under-10-mins">https://blog.ediri.io/learn-rust-in-under-10-mins</a></div>
<p> </p>
<p>Before we start, we need to make sure we have the following tools installed:</p>
<ul>
<li><p><a target="_blank" href="https://www.rust-lang.org">Rust</a></p>
</li>
<li><p>An IDE or text editor of your choice</p>
</li>
<li><p>AWS account (free tier is enough)</p>
</li>
<li><p>AWS CLI</p>
</li>
<li><p>ChatGPT API key (No need for ChatGPT Plus)</p>
</li>
</ul>
<h2 id="heading-create-a-new-rust-project">Create a new Rust project</h2>
<p>Before we can start with the rust code, we need to install the <code>cargo-lambda</code> Cargo plugin. As I am using a Mac, I will use <code>brew</code> to install the plugin.</p>
<p>If you are using a different OS, check out the <a target="_blank" href="https://github.com/awslabs/aws-lambda-rust-runtime#getting-started">Getting started section</a> of the official documentation.</p>
<pre><code class="lang-bash">brew tap cargo-lambda/cargo-lambda
brew install cargo-lambda
</code></pre>
<p>Now we can create our function by running the <code>cargo-lambda</code> with the subcommand <code>new</code>.</p>
<pre><code class="lang-bash">cargo lambda new dadjoke
</code></pre>
<p>You will be asked a few questions. I have answered them as follows:</p>
<pre><code class="lang-bash">cargo lambda new dadjoke   
&gt; Is this <span class="hljs-keyword">function</span> an HTTP <span class="hljs-keyword">function</span>? Yes
</code></pre>
<p>This will generate a new Rust project inside the folder named <code>dadjoke</code>.</p>
<p>Change into the newly created folder and add our <code>chatgpt_rs</code> crate using the <code>cargo add</code> command.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> dadjoke
cargo add chatgpt_rs
</code></pre>
<p>Open the <code>src/main.rs</code> file and replace the existing content with the following code, as we want to access the <code>ChatGPT</code> API to generate AI-generated Dad Jokes for us.</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> chatgpt::prelude::ChatGPT;
<span class="hljs-keyword">use</span> chatgpt::types::CompletionResponse;
<span class="hljs-keyword">use</span> lambda_http::{run, service_fn, Error, Request, Response};


<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">function_handler</span></span>(_: Request) -&gt; <span class="hljs-built_in">Result</span>&lt;Response&lt;<span class="hljs-built_in">String</span>&gt;, Error&gt; {
    <span class="hljs-keyword">let</span> chatgpt_api_key = std::env::var(<span class="hljs-string">"CHATGPT_API_KEY"</span>).expect(<span class="hljs-string">"env variable `CHATGPT_API_KEY` should be set"</span>);

    <span class="hljs-keyword">let</span> client = ChatGPT::new(chatgpt_api_key)?;

    <span class="hljs-keyword">let</span> response: CompletionResponse = client
        .send_message(<span class="hljs-string">"Imagine you are a dad and tell a good dad joke, tell only the joke without any other text, all in one line without line breaks:"</span>)
        .<span class="hljs-keyword">await</span>.unwrap();

    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> reponse_string = response.message().content.to_string();
    reponse_string = reponse_string.replace(<span class="hljs-string">"\n"</span>, <span class="hljs-string">""</span>);

    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"{}"</span>, reponse_string);

    <span class="hljs-keyword">let</span> resp = Response::builder()
        .status(<span class="hljs-number">200</span>)
        .header(<span class="hljs-string">"content-type"</span>, <span class="hljs-string">"text/html"</span>)
        .body(reponse_string)
        .map_err(<span class="hljs-built_in">Box</span>::new)?;
    <span class="hljs-literal">Ok</span>(resp)
}

<span class="hljs-meta">#[tokio::main]</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() -&gt; <span class="hljs-built_in">Result</span>&lt;(), Error&gt; {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .with_target(<span class="hljs-literal">false</span>)
        .without_time()
        .init();

    run(service_fn(function_handler)).<span class="hljs-keyword">await</span>
}
</code></pre>
<p>The main action happens in the <code>function_handler</code> function. The API key is provided via the environment variable <code>CHATGPT_API_KEY</code>. We will set this variable later when we deploy our function to AWS Lambda. We then make a non-streaming call to ChatGPT to give us a dad joke back. I use the following phrase to give the AI the right context from the start:</p>
<blockquote>
<p>Imagine you are a dad and tell a good dad joke, tell only the joke without any other text, all in one line without line breaks:</p>
</blockquote>
<h2 id="heading-build-and-test-our-function-locally">Build and Test Our Function Locally</h2>
<p>Before we deploy our function to AWS, we want to test it locally to be sure it works as expected. To do so, the <code>cargo lambda</code> subcommand <code>watch</code> offers us a very easy way to build our function and start a local HTTP server on the port <code>9000</code>.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> CHATGPT_API_KEY=&lt;YOUR_CHATGPT_API_KEY&gt;
cargo lambda watch -a 0.0.0.0 -p 9000
</code></pre>
<p>Now we can invoke our function using <code>cargo lambda invoke</code>. This will send an HTTP request to our local server and print the response with handling all the event parameters for us.</p>
<pre><code class="lang-bash">cargo lambda invoke dadjoke --data-ascii <span class="hljs-string">'{}'</span> -a 0.0.0.0
</code></pre>
<p>You should see something like this:</p>
<pre><code class="lang-bash">{<span class="hljs-string">"statusCode"</span>:200,<span class="hljs-string">"headers"</span>:{<span class="hljs-string">"content-type"</span>:<span class="hljs-string">"text/html"</span>},<span class="hljs-string">"multiValueHeaders"</span>:{<span class="hljs-string">"content-type"</span>:[<span class="hljs-string">"text/html"</span>]},<span class="hljs-string">"body"</span>:<span class="hljs-string">"Response: Why don't skeletons fight each other? They don't have the guts!"</span>,<span class="hljs-string">"isBase64Encoded"</span>:<span class="hljs-literal">false</span>}
</code></pre>
<h2 id="heading-deploy-our-function-to-aws-lambda">Deploy our function to AWS Lambda</h2>
<p>We are ready to deploy our function to AWS!</p>
<p>First, we need to build and bundle our function. We can do this, again, by using the <code>cargo lambda</code> with the subcommand <code>build</code>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🤓</div>
<div data-node-type="callout-text">As we want to use Graviton-based instances, we need to cross-compile our function to <code>arm64</code>. We can do this by adding <code>--arm64</code> to the <code>cargo lambda build</code> command.</div>
</div>

<pre><code class="lang-bash">cargo lambda build --release --arm64 --output-format zip
</code></pre>
<p>This will create a zip file inside the <code>target/lambda/release</code> folder.</p>
<p>Now we can use the following AWS CLI command to create the execution role for our function and then create the function itself.</p>
<pre><code class="lang-bash">aws iam create-role --role-name rust-in-the-cloud-role --assume-role-policy-document <span class="hljs-string">'{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'</span>
</code></pre>
<p>After the role is created, we need to attach the <code>AWSLambdaBasicExecutionRole</code> policy to the role.</p>
<pre><code class="lang-bash">aws iam attach-role-policy --role-name rust-in-the-cloud-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
</code></pre>
<p>With the role in place, we can now create our function. We will use the <code>aws lambda create-function</code> command to do so.</p>
<pre><code class="lang-bash">aws lambda create-function --function-name rust-in-the-cloud \
--handler bootstrap \
--zip-file fileb://./target/lambda/dadjoke/bootstrap.zip \
--runtime provided.al2 \
--role arn:aws:iam::&lt;YOUR_AWS_ACCOUNT_ID&gt;:role/rust-in-the-cloud-role \
--environment Variables={CHATGPT_API_KEY=&lt;YOUR_CHATGPT_API_KEY&gt;} \
--tracing-config Mode=Active
</code></pre>
<p>To get the role arn, you can use the <code>aws iam list-roles</code> command.</p>
<pre><code class="lang-bash">aws iam list-roles | grep rust-in-the-cloud-role
</code></pre>
<p>Now we can use the <code>aws lambda invoke</code> command to synchronously invoke our function.</p>
<pre><code class="lang-bash">aws lambda invoke --cli-binary-format raw-in-base64-out  \
--function-name rust-in-the-cloud \
--payload <span class="hljs-string">'{}'</span> response.json
</code></pre>
<p>Now we can use the <code>jq</code> command to extract the response from the <code>response.json</code> file.</p>
<pre><code class="lang-bash">cat response.json | jq <span class="hljs-string">'.body'</span>
</code></pre>
<p>You should see a dad joke like this:</p>
<pre><code class="lang-bash"><span class="hljs-string">"Why don't eggs tell jokes? Because they might crack up!"</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>We have seen how easy it is to run Rust-based functions in AWS Lambda. With the help of the <code>cargo-lambda</code> plugin, we can build and bundle our function and then use for example the AWS CLI to deploy it to AWS Lambda.</p>
<p>Let's have a look at the pros and cons of this approach:</p>
<h3 id="heading-pros">Pros</h3>
<ul>
<li><p>Easy to get started</p>
</li>
<li><p>Lightning-fast cold start times and low memory footprint</p>
</li>
<li><p>No need to build a custom runtime</p>
</li>
</ul>
<p>But there are also some cons:</p>
<h3 id="heading-cons">Cons</h3>
<ul>
<li><p>I have the feeling that AWS is not supporting Rust as a first-class citizen.</p>
</li>
<li><p>The documentation is not that great, and you need to figure some stuff out by yourself.</p>
</li>
</ul>
<p>Overall, I am still very happy using Rust in AWS Lambda and I could see adding Rust-based functions to a polyglot microservice architecture in the future.</p>
<p>If you want to learn more, check out the official documentation <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-rust.html">here</a>.</p>
<h2 id="heading-housekeeping">Housekeeping</h2>
<p>To clean up, we can delete the function and the role.</p>
<pre><code class="lang-bash">aws lambda delete-function --function-name rust-in-the-cloud
aws iam detach-role-policy --role-name rust-in-the-cloud-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name rust-in-the-cloud-role
</code></pre>
]]></content:encoded></item><item><title><![CDATA[A Step-by-Step Guide to Using Velero on AWS EKS Clusters via Pulumi]]></title><description><![CDATA[TL;DR: THE CODE!
https://github.com/dirien/quick-bites/tree/main/pulumi-eks-velero
 
Introduction
My motivation to write this article comes from the recent blog post of Lily Cohen who had a severe data loss on her Kubernetes cluster.
During a routine...]]></description><link>https://blog.ediri.io/a-step-by-step-guide-to-using-velero-on-aws-eks-clusters-via-pulumi</link><guid isPermaLink="true">https://blog.ediri.io/a-step-by-step-guide-to-using-velero-on-aws-eks-clusters-via-pulumi</guid><category><![CDATA[AWS]]></category><category><![CDATA[EKS]]></category><category><![CDATA[velero]]></category><category><![CDATA[Pulumi]]></category><category><![CDATA[Infrastructure as code]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sun, 03 Sep 2023 18:29:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693765615341/75ad2b51-4fe2-4a60-b3fe-dbd82aa99e31.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-the-code">TL;DR: THE CODE!</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/pulumi-eks-velero">https://github.com/dirien/quick-bites/tree/main/pulumi-eks-velero</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>My motivation to write this article comes from the recent blog post of <a target="_blank" href="https://firefish.social/@lily">Lily Cohen</a> who had a severe data loss on her Kubernetes cluster.</p>
<p>During a routine GitOps cleanup, YAML manifests responsible for creating Kubernetes namespaces were moved to a directory not tracked by <a target="_blank" href="https://argoproj.github.io/cd/">ArgoCD</a>, leading to unintentional data deletion. Although backups were taken using Velero every six hours, the <a target="_blank" href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/">Persistent Volume Claim (PVC)</a> data was not included due to a missing Restic flag, rendering the backups incomplete and the data irretrievable.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://firefish.social/notes/9iqefgi8rzfksnqc">https://firefish.social/notes/9iqefgi8rzfksnqc</a></div>
<p> </p>
<p>Let's have a look at <a target="_blank" href="https://velero.io/">Velero</a> and <a target="_blank" href="https://www.pulumi.com">Pulumi</a> to back up and restore an EKS cluster including the persistent volumes.</p>
<h3 id="heading-what-is-velero">What is Velero?</h3>
<p><a target="_blank" href="https://velero.io/">Velero</a> is an open-source tool to backup and restore, perform disaster recovery, and migrate Kubernetes cluster resources and persistent volumes.</p>
<p>Velero gives us the following benefits:</p>
<ul>
<li><p><mark>Disaster Recovery:</mark> Reduces time to recovery in case of infrastructure loss, data corruption, and/or service outages</p>
</li>
<li><p><mark>Data Migration:</mark> Enables cluster portability by easily migrating Kubernetes resources from one cluster to another and integrates with DevOps workflows to create ephemeral clones of Kubernetes namespaces</p>
</li>
<li><p><mark>Ephemeral Clusters:</mark> Provides a reliable tool to unlock new approaches to cluster lifecycle management treating clusters as "cattle"</p>
</li>
</ul>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/8skHGzUBZ-Q">https://youtu.be/8skHGzUBZ-Q</a></div>
<p> </p>
<h3 id="heading-using-velero-with-managed-kubernetes-services">Using Velero With Managed Kubernetes Services</h3>
<p>Managed Kubernetes services like EKS, AKS, GKE, etc. are great as they take away the burden of managing the control plane. The <code>etcd</code> key-value store is managed by the cloud provider and therefore only accessible through the Kubernetes API Server. Here comes Velero into play.</p>
<p>Velero retrieves <code>etcd</code> data via the Kubernetes API Server, offering significant flexibility in backup options. You can filter which resources to back up based on criteria like namespace or label, and even choose to exclude certain resources from the backup.</p>
<p>Velero manages backup and restore tasks using Custom Resources (CRs) within the Kubernetes cluster. The Velero controller monitors these CRs to execute backup and restore procedures.</p>
<p>This Kubernetes-native methodology offers excellent opportunities to harness the broader Kubernetes ecosystem, including GitOps for delivery or admission controllers for implementing policy-as-code to avoid misconfigurations. Additionally, Velero's CLI can be integrated into existing CI/CD systems, allowing pipelines to trigger backup and restore actions on demand.</p>
<h3 id="heading-velero-workflow-in-a-nutshell">Velero Workflow in a Nutshell</h3>
<p>Velero has two main components:</p>
<ul>
<li><p>A server that runs in your Kubernetes cluster</p>
</li>
<li><p>A command-line client that runs locally</p>
</li>
</ul>
<p>With the command-line client, we can create backups and restore them by creating CRs in the Kubernetes cluster.</p>
<h4 id="heading-backup">Backup</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693761202595/ec1054c6-52e6-42d8-9bf3-9603b8efc10c.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>The user initiates a call to the Kubernetes API server using the Velero CLI.</p>
</li>
<li><p>The API server generates a Custom Resource (CR) of kind <code>backups.velero.io</code>.</p>
</li>
<li><p>The Velero Controller then takes the following steps:</p>
<ul>
<li><p>Verifies the presence of the CR objects.</p>
</li>
<li><p>Requests the relevant resources from the API server.</p>
</li>
<li><p>Compresses these resources and stores them in an S3 bucket.</p>
</li>
</ul>
</li>
</ol>
<h4 id="heading-restore">Restore</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693761775113/bf2dc15a-6f64-41c0-b796-bdb2bdf55f4a.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>The user initiates a restore request to the Kubernetes API server via the Velero CLI.</p>
</li>
<li><p>In response, the API server creates a Custom Resource (CR) for the restore operation, of kind <code>restores.velero.io.</code></p>
</li>
<li><p>The Velero Controller then performs the following actions:</p>
<ul>
<li><p>Checks for the existence of the specified Restore CR objects.</p>
</li>
<li><p>Fetches the corresponding compressed resources from the S3 bucket.</p>
</li>
<li><p>Decompress these resources and calls the API server to restore them into the Kubernetes cluster.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Having covered the theoretical aspects of Velero, let's now dive into the practical portion of this blog article.</p>
<ul>
<li><p><a target="_blank" href="https://www.pulumi.com/docs/get-started/install/">Pulumi</a></p>
</li>
<li><p><a target="_blank" href="https://app.pulumi.com/signup">Pulumi Account</a> - this is optional but convenient for handling the state of stack.</p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/">AWS Account</a></p>
</li>
<li><p><code>kubectl</code> - Required to interact with the Kubernetes cluster. You can install it by following the instructions <a target="_blank" href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">here</a>.</p>
</li>
<li><p><a target="_blank" href="https://golang.org/doc/install">Go</a></p>
</li>
<li><p><a target="_blank" href="https://velero.io/docs/v1.12/basic-install/">Velero CLI</a></p>
</li>
</ul>
<p>I will use Golang as my programming language of choice. You can use of course any other language supported by Pulumi.</p>
<p>To create a Pulumi project, run the following commands:</p>
<pre><code class="lang-bash">mkdir pulumi-eks-velero
<span class="hljs-built_in">cd</span> pulumi-eks-velero
pulumi new go --force
</code></pre>
<blockquote>
<p>I am using the <code>--force</code> flag as I already created the directory beforehand.</p>
</blockquote>
<h2 id="heading-define-the-eks-cluster">Define the EKS Cluster</h2>
<p>To deploy an EKS cluster, we will use the Pulumi EKS package. The package is available on the Pulumi Registry and is called <a target="_blank" href="https://www.pulumi.com/registry/packages/eks/">pulumi-eks</a>.</p>
<p>I am not going into much detail here on how to deploy an EKS cluster with Pulumi. Check the demo code for more details. The only thing I want to mention is that we need to activate the OpenID Connect (OIDC) provider for the cluster and install the AWS EBS CSI driver to support the dynamic provisioning of EBS volumes.</p>
<p>You can do this by setting the <code>CreateOidcProvider</code> flag to <code>true</code> when creating the cluster. We need this as we're going to use IRSA (IAM Roles for Service Accounts) to grant Velero access to the S3 bucket.</p>
<p>The EBS CSI driver installation consists of several steps:</p>
<ul>
<li><p>Create the IAM assume role for the EBS CSI driver</p>
</li>
<li><p>Create the EBS IAM policy</p>
</li>
<li><p>Deploy the EBS CSI driver via Helm</p>
</li>
</ul>
<p>Here is the code for the EBS CSI driver installation:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted code for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitted code for brevity</span>
        ebsRole, err := iam.NewRole(ctx, <span class="hljs-string">"velero-ebs-role"</span>, &amp;iam.RoleArgs{
            AssumeRolePolicy: pulumi.All(cluster.Core.OidcProvider().Arn(), cluster.Core.OidcProvider().Url()).ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args []<span class="hljs-keyword">interface</span>{})</span> <span class="hljs-title">string</span></span> {
                arn := args[<span class="hljs-number">0</span>].(<span class="hljs-keyword">string</span>)
                url := args[<span class="hljs-number">1</span>].(<span class="hljs-keyword">string</span>)
                assumeRolePolicy, _ := iam.GetPolicyDocument(ctx, &amp;iam.GetPolicyDocumentArgs{
                    Statements: []iam.GetPolicyDocumentStatement{
                        {
                            Effect: pulumi.StringRef(<span class="hljs-string">"Allow"</span>),
                            Actions: []<span class="hljs-keyword">string</span>{
                                <span class="hljs-string">"sts:AssumeRoleWithWebIdentity"</span>,
                            },
                            Principals: []iam.GetPolicyDocumentStatementPrincipal{
                                {
                                    Type: <span class="hljs-string">"Federated"</span>,
                                    Identifiers: []<span class="hljs-keyword">string</span>{
                                        arn,
                                    },
                                },
                            },
                            Conditions: []iam.GetPolicyDocumentStatementCondition{
                                {
                                    Test: <span class="hljs-string">"StringEquals"</span>,
                                    Values: []<span class="hljs-keyword">string</span>{
                                        ebsServiceAccount,
                                    },
                                    Variable: fmt.Sprintf(<span class="hljs-string">"%s:sub"</span>, url),
                                },
                            },
                        },
                    },
                })
                <span class="hljs-keyword">return</span> assumeRolePolicy.Json
            }).(pulumi.StringOutput),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        ebsPolicyFile, _ := os.ReadFile(<span class="hljs-string">"./policies/ebs-iam-policy.json"</span>)

        ebsIAMPolicy, err := iam.NewPolicy(ctx, <span class="hljs-string">"velero-ebs-policy"</span>, &amp;iam.PolicyArgs{
            Policy: pulumi.String(ebsPolicyFile),
        }, pulumi.DependsOn([]pulumi.Resource{ebsRole}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        ebsRolePolicyAttachment, err := iam.NewRolePolicyAttachment(ctx, <span class="hljs-string">"velero-esb-role-attachment"</span>, &amp;iam.RolePolicyAttachmentArgs{
            PolicyArn: ebsIAMPolicy.Arn,
            Role:      ebsRole.Name,
        }, pulumi.DependsOn([]pulumi.Resource{ebsIAMPolicy}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        _, err = helm.NewRelease(ctx, <span class="hljs-string">"aws-ebs-csi-driver"</span>, &amp;helm.ReleaseArgs{
            Chart:       pulumi.String(<span class="hljs-string">"aws-ebs-csi-driver"</span>),
            Version:     pulumi.String(<span class="hljs-string">"2.22.0"</span>),
            Namespace:   pulumi.String(<span class="hljs-string">"kube-system"</span>),
            ForceUpdate: pulumi.Bool(<span class="hljs-literal">true</span>),
            RepositoryOpts: helm.RepositoryOptsArgs{
                Repo: pulumi.String(<span class="hljs-string">"https://kubernetes-sigs.github.io/aws-ebs-csi-driver"</span>),
            },
            Values: pulumi.Map{
                <span class="hljs-string">"controller"</span>: pulumi.Map{
                    <span class="hljs-string">"serviceAccount"</span>: pulumi.Map{
                        <span class="hljs-string">"annotations"</span>: pulumi.Map{
                            <span class="hljs-string">"eks.amazonaws.com/role-arn"</span>: ebsRole.Arn,
                        },
                    },
                },
                <span class="hljs-string">"storageClasses"</span>: pulumi.Array{
                    pulumi.Map{
                        <span class="hljs-string">"name"</span>:              pulumi.String(<span class="hljs-string">"ebs-sc"</span>),
                        <span class="hljs-string">"volumeBindingMode"</span>: pulumi.String(<span class="hljs-string">"WaitForFirstConsumer"</span>),
                    },
                },
                <span class="hljs-string">"volumeSnapshotClasses"</span>: pulumi.Array{
                    pulumi.Map{
                        <span class="hljs-string">"name"</span>:           pulumi.String(<span class="hljs-string">"ebs-vsc"</span>),
                        <span class="hljs-string">"deletionPolicy"</span>: pulumi.String(<span class="hljs-string">"Delete"</span>),
                    },
                },
            },
        }, pulumi.Provider(provider), pulumi.DependsOn([]pulumi.Resource{ebsRolePolicyAttachment}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>Important to note here that we create the service account for the EBS CSI driver with the <code>eks.amazonaws.com/role-arn</code> annotation. This annotation is required by the EBS CSI driver to assume the IAM role we created before. Additionally, we create the storage class and volume snapshot class for the EBS CSI driver as well. You can decide if you want to keep this or deploy the storage class and volume snapshot class as separate Kubernetes resources.</p>
<h2 id="heading-define-the-s3-bucket">Define the S3 Bucket</h2>
<p>Next, we need to create the S3 bucket where we will store the backups. We will use the Pulumi AWS package to create the S3 bucket and the corresponding bucket public access block.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted code for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitted code for brevity</span>
        bucket, err := s3.NewBucket(ctx, <span class="hljs-string">"velero-bucket"</span>, &amp;s3.BucketArgs{
            Bucket: pulumi.String(<span class="hljs-string">"velero-eks-bucket-dirien"</span>),
            Tags: pulumi.StringMap{
                <span class="hljs-string">"Name"</span>: pulumi.String(<span class="hljs-string">"velero-eks-bucket-dirien"</span>),
            },
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        _, err = s3.NewBucketPublicAccessBlock(ctx, <span class="hljs-string">"velero-bucket-public-access-block"</span>, &amp;s3.BucketPublicAccessBlockArgs{
            Bucket:                bucket.ID(),
            BlockPublicAcls:       pulumi.Bool(<span class="hljs-literal">true</span>),
            BlockPublicPolicy:     pulumi.Bool(<span class="hljs-literal">true</span>),
            IgnorePublicAcls:      pulumi.Bool(<span class="hljs-literal">true</span>),
            RestrictPublicBuckets: pulumi.Bool(<span class="hljs-literal">true</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>One thing to note here is that we set the <code>BlockPublicAcls</code>, <code>BlockPublicPolicy</code>, <code>IgnorePublicAcls</code>, and the <code>RestrictPublicBuckets</code> flags to <code>true</code>. This is a best practice to prevent the S3 bucket from being publicly accessible.</p>
<h2 id="heading-define-the-velero-installation">Define the Velero Installation</h2>
<p>At last, we're ready to deploy Velero. The first step is setting up the IRSA to give Velero the required permissions to access the S3 bucket. This process is similar to installing the EBS CSI driver, albeit with different policies. Once that's done, we'll proceed to deploy Velero using its Helm chart.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted code for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitted code for brevity</span>
        saRole, err := iam.NewRole(ctx, <span class="hljs-string">"velero-sa-role"</span>, &amp;iam.RoleArgs{
            AssumeRolePolicy: pulumi.All(cluster.Core.OidcProvider().Arn(), cluster.Core.OidcProvider().Url()).ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args []<span class="hljs-keyword">interface</span>{})</span> <span class="hljs-title">string</span></span> {
                arn := args[<span class="hljs-number">0</span>].(<span class="hljs-keyword">string</span>)
                url := args[<span class="hljs-number">1</span>].(<span class="hljs-keyword">string</span>)
                assumeRolePolicy, _ := iam.GetPolicyDocument(ctx, &amp;iam.GetPolicyDocumentArgs{
                    Statements: []iam.GetPolicyDocumentStatement{
                        {
                            Actions: []<span class="hljs-keyword">string</span>{
                                <span class="hljs-string">"sts:AssumeRoleWithWebIdentity"</span>,
                            },
                            Conditions: []iam.GetPolicyDocumentStatementCondition{
                                {
                                    Test: <span class="hljs-string">"StringEquals"</span>,
                                    Values: []<span class="hljs-keyword">string</span>{
                                        veleroServiceAccount,
                                    },
                                    Variable: fmt.Sprintf(<span class="hljs-string">"%s:sub"</span>, url),
                                },
                            },
                            Principals: []iam.GetPolicyDocumentStatementPrincipal{
                                {
                                    Type: <span class="hljs-string">"Federated"</span>,
                                    Identifiers: []<span class="hljs-keyword">string</span>{
                                        arn,
                                    },
                                },
                            },
                            Effect: pulumi.StringRef(<span class="hljs-string">"Allow"</span>),
                        },
                    },
                })
                <span class="hljs-keyword">return</span> assumeRolePolicy.Json
            }).(pulumi.StringOutput),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        veleroIAMPolicy, err := iam.NewPolicy(ctx, <span class="hljs-string">"velero-iam-policy"</span>, &amp;iam.PolicyArgs{
            Policy: pulumi.All(bucket.Bucket).ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args []<span class="hljs-keyword">interface</span>{})</span> <span class="hljs-title">string</span></span> {
                name := args[<span class="hljs-number">0</span>].(<span class="hljs-keyword">string</span>)
                veleroIAMPolicy, _ := iam.GetPolicyDocument(ctx, &amp;iam.GetPolicyDocumentArgs{
                    Statements: []iam.GetPolicyDocumentStatement{
                        {
                            Effect: pulumi.StringRef(<span class="hljs-string">"Allow"</span>),
                            Actions: []<span class="hljs-keyword">string</span>{
                                <span class="hljs-string">"ec2:DescribeVolumes"</span>,
                                <span class="hljs-string">"ec2:DescribeSnapshots"</span>,
                                <span class="hljs-string">"ec2:CreateTags"</span>,
                                <span class="hljs-string">"ec2:CreateVolume"</span>,
                                <span class="hljs-string">"ec2:CreateSnapshot"</span>,
                                <span class="hljs-string">"ec2:DeleteSnapshot"</span>,
                            },
                            Resources: []<span class="hljs-keyword">string</span>{
                                <span class="hljs-string">"*"</span>,
                            },
                        },
                        {
                            Effect: pulumi.StringRef(<span class="hljs-string">"Allow"</span>),
                            Actions: []<span class="hljs-keyword">string</span>{
                                <span class="hljs-string">"s3:GetObject"</span>,
                                <span class="hljs-string">"s3:DeleteObject"</span>,
                                <span class="hljs-string">"s3:PutObject"</span>,
                                <span class="hljs-string">"s3:AbortMultipartUpload"</span>,
                                <span class="hljs-string">"s3:ListMultipartUploadParts"</span>,
                            },
                            Resources: []<span class="hljs-keyword">string</span>{
                                fmt.Sprintf(<span class="hljs-string">"arn:aws:s3:::%s/*"</span>, name),
                            },
                        },
                        {
                            Effect: pulumi.StringRef(<span class="hljs-string">"Allow"</span>),
                            Actions: []<span class="hljs-keyword">string</span>{
                                <span class="hljs-string">"s3:ListBucket"</span>,
                            },
                            Resources: []<span class="hljs-keyword">string</span>{
                                fmt.Sprintf(<span class="hljs-string">"arn:aws:s3:::%s"</span>, name),
                            },
                        },
                    },
                })
                <span class="hljs-keyword">return</span> veleroIAMPolicy.Json
            }).(pulumi.StringOutput),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        _, err = iam.NewRolePolicyAttachment(ctx, <span class="hljs-string">"velero-iam-role-attachment"</span>, &amp;iam.RolePolicyAttachmentArgs{
            PolicyArn: veleroIAMPolicy.Arn,
            Role:      saRole.Name,
        })

        _, err = helm.NewRelease(ctx, <span class="hljs-string">"velero"</span>, &amp;helm.ReleaseArgs{
            Chart:   pulumi.String(<span class="hljs-string">"velero"</span>),
            Version: pulumi.String(<span class="hljs-string">"5.0.2"</span>),
            RepositoryOpts: helm.RepositoryOptsArgs{
                Repo: pulumi.String(<span class="hljs-string">"https://vmware-tanzu.github.io/helm-charts"</span>),
            },
            Namespace:       pulumi.String(veleroNamespace),
            CreateNamespace: pulumi.Bool(<span class="hljs-literal">true</span>),
            Values: pulumi.Map{
                <span class="hljs-string">"serviceAccount"</span>: pulumi.Map{
                    <span class="hljs-string">"server"</span>: pulumi.Map{
                        <span class="hljs-string">"name"</span>: pulumi.String(<span class="hljs-string">"velero"</span>),
                        <span class="hljs-string">"annotations"</span>: pulumi.StringMap{
                            <span class="hljs-string">"eks.amazonaws.com/role-arn"</span>: saRole.Arn,
                        },
                    },
                },
                <span class="hljs-string">"credentials"</span>: pulumi.Map{
                    <span class="hljs-string">"useSecret"</span>: pulumi.Bool(<span class="hljs-literal">false</span>),
                },
                <span class="hljs-string">"podSecurityContext"</span>: pulumi.Map{
                    <span class="hljs-string">"fsGroup"</span>: pulumi.Int(<span class="hljs-number">65534</span>),
                },
                <span class="hljs-string">"initContainers"</span>: pulumi.Array{
                    pulumi.Map{
                        <span class="hljs-string">"name"</span>:            pulumi.String(<span class="hljs-string">"velero-plugin-for-aws"</span>),
                        <span class="hljs-string">"image"</span>:           pulumi.String(<span class="hljs-string">"velero/velero-plugin-for-aws:v1.7.1"</span>),
                        <span class="hljs-string">"imagePullPolicy"</span>: pulumi.String(<span class="hljs-string">"IfNotPresent"</span>),
                        <span class="hljs-string">"volumeMounts"</span>: pulumi.Array{
                            pulumi.Map{
                                <span class="hljs-string">"name"</span>:      pulumi.String(<span class="hljs-string">"plugins"</span>),
                                <span class="hljs-string">"mountPath"</span>: pulumi.String(<span class="hljs-string">"/target"</span>),
                            },
                        },
                    },
                },
                <span class="hljs-string">"configuration"</span>: pulumi.Map{
                    <span class="hljs-string">"backupStorageLocation"</span>: pulumi.Array{
                        pulumi.Map{
                            <span class="hljs-string">"name"</span>:     pulumi.String(<span class="hljs-string">"velero-k8s"</span>),
                            <span class="hljs-string">"provider"</span>: pulumi.String(<span class="hljs-string">"aws"</span>),
                            <span class="hljs-string">"bucket"</span>:   bucket.Bucket,
                            <span class="hljs-string">"prefix"</span>:   pulumi.String(<span class="hljs-string">"velero"</span>),
                            <span class="hljs-string">"config"</span>: pulumi.Map{
                                <span class="hljs-string">"region"</span>: pulumi.String(<span class="hljs-string">"eu-central-1"</span>),
                            },
                            <span class="hljs-string">"default"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                        },
                    },
                    <span class="hljs-string">"volumeSnapshotLocation"</span>: pulumi.Array{
                        pulumi.Map{
                            <span class="hljs-string">"name"</span>:     pulumi.String(<span class="hljs-string">"velero-k8s-snapshots"</span>),
                            <span class="hljs-string">"provider"</span>: pulumi.String(<span class="hljs-string">"aws"</span>),
                            <span class="hljs-string">"config"</span>: pulumi.Map{
                                <span class="hljs-string">"region"</span>: pulumi.String(<span class="hljs-string">"eu-central-1"</span>),
                            },
                        },
                    },
                },
            },
        }, pulumi.Provider(provider))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>Similar to the EBS CSI driver setup, we first create a service account for Velero, annotating it with <code>eks.amazonaws.com/role-arn</code>. Thanks to the IRS approach, there's no need to create a separate AWS secret; you can simply set <code>credentials.useSecret</code> to <code>false</code> in the Helm chart values.</p>
<p>To enable backup and restore functionalities for EBS volumes, it's crucial to install the AWS plugin for Velero. This can be achieved using <code>initContainers</code> in the Helm chart.</p>
<p>The final step involves establishing the backup storage location and the volume snapshot location. We will use the previously created S3 bucket for storing backups and set the default AWS volume snapshot location, all of which can be configured in the Helm chart values (<code>configurations).</code></p>
<h2 id="heading-deploy-the-stack">Deploy the stack</h2>
<p>Now we can deploy the stack. Run the following commands to deploy the stack:</p>
<pre><code class="lang-bash">pulumi up
</code></pre>
<blockquote>
<p>Make sure you have the AWS CLI configured with the correct credentials and region before you run the command.</p>
</blockquote>
<h2 id="heading-testing-velero-backup-and-restore">Testing Velero backup and restore</h2>
<p>With the stack now deployed, it's time to test the backup and restore functionalities. Ensure that you have the Velero CLI installed, as we'll be using it to execute the backup and restore processes.</p>
<p>Before proceeding with creating a backup, you'll first need to set up a namespace and a simple pod that includes a persistent volume claim (PVC).</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Namespace</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">velero-test</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">PersistentVolumeClaim</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">ebs-claim</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">velero-test</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">accessModes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">ReadWriteOnce</span>
  <span class="hljs-attr">storageClassName:</span> <span class="hljs-string">ebs-sc</span>
  <span class="hljs-attr">resources:</span>
    <span class="hljs-attr">requests:</span>
      <span class="hljs-attr">storage:</span> <span class="hljs-string">1Gi</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Pod</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">app</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">velero-test</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">containers:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">app</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">centos</span>
    <span class="hljs-attr">command:</span> [<span class="hljs-string">"/bin/sh"</span>]
    <span class="hljs-attr">args:</span> [<span class="hljs-string">"-c"</span>, <span class="hljs-string">"while true; do sleep 5; done"</span>]
    <span class="hljs-attr">volumeMounts:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">persistent-storage</span>
      <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/data</span>
  <span class="hljs-attr">volumes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">persistent-storage</span>
    <span class="hljs-attr">persistentVolumeClaim:</span>
      <span class="hljs-attr">claimName:</span> <span class="hljs-string">ebs-claim</span>
</code></pre>
<p>Save the code above in a file called <code>test.yaml</code> and run the following command to create the Pod. You need to get the <code>kubeconfig</code> for the cluster first using the following command:</p>
<pre><code class="lang-bash">pulumi stack output kubeconfig --show-secrets  &gt; kubeconfig.yaml
</code></pre>
<p>Now you can create the pod using the following command:</p>
<pre><code class="lang-bash">kubectl --kubeconfig=kubeconfig.yaml apply -f test.yaml
</code></pre>
<p>This will create a Pod in the <code>velero-test</code> namespace with a persistent volume claim. To create some data in the volume, we can exec into the Pod and create a file in the volume:</p>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -it app -n velero-test -- sh -c <span class="hljs-string">'echo "Hi, today is $(date)" &gt; /data/test.txt'</span>
</code></pre>
<p>Check if the file is created:</p>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -it app -n velero-test -- sh -c <span class="hljs-string">'cat /data/test.txt'</span>
</code></pre>
<p>You should see the following output:</p>
<pre><code class="lang-bash">Hi, today is Tue Sep  5 09:21:53 UTC 2023
</code></pre>
<p>This should be enough to simulate to show the backup and restore functionality of Velero.</p>
<h2 id="heading-create-a-backup">Create a backup</h2>
<p>To create a backup, we need to create a backup CR in the Kubernetes cluster. We can do this by running the following command:</p>
<pre><code class="lang-bash">velero backup create velero-test-backup --include-namespaces velero-test --<span class="hljs-built_in">wait</span>
</code></pre>
<p>This will create a backup of the <code>velero-test</code> namespace and wait until the backup is completed.</p>
<pre><code class="lang-bash">Backup request <span class="hljs-string">"velero-test-backup"</span> submitted successfully.
Waiting <span class="hljs-keyword">for</span> backup to complete. You may safely press ctrl-c to stop waiting - your backup will <span class="hljs-built_in">continue</span> <span class="hljs-keyword">in</span> the background.
..
Backup completed with status: Completed. You may check <span class="hljs-keyword">for</span> more information using the commands `velero backup describe velero-test-backup` and `velero backup logs velero-test-backup`.
</code></pre>
<p>To display details about the backup, run the following command with the <code>--details</code> flag.</p>
<pre><code class="lang-bash">velero backup describe velero-test-backup --details
</code></pre>
<p>You should see similar output like this.</p>
<pre><code class="lang-bash">Name:         velero-test-backup
Namespace:    velero
Labels:       velero.io/storage-location=velero-k8s
Annotations:  velero.io/source-cluster-k8s-gitversion=v1.27.4-eks-2d98532
              velero.io/source-cluster-k8s-major-version=1
              velero.io/source-cluster-k8s-minor-version=27+

Phase:  Completed


Namespaces:
  Included:  velero-test
  Excluded:  &lt;none&gt;

Resources:
  Included:        *
  Excluded:        &lt;none&gt;
  Cluster-scoped:  auto

Label selector:  &lt;none&gt;

Storage Location:  velero-k8s

Velero-Native Snapshot PVs:  auto

TTL:  720h0m0s

CSISnapshotTimeout:    10m0s
ItemOperationTimeout:  1h0m0s

Hooks:  &lt;none&gt;

Backup Format Version:  1.1.0

Started:    2023-09-03 15:45:28 +0200 CEST
Completed:  2023-09-03 15:45:30 +0200 CEST

Expiration:  2023-10-03 15:45:28 +0200 CEST

Total items to be backed up:  6
Items backed up:              6

Resource List:
  v1/ConfigMap:
    - velero-test/kube-root-ca.crt
  v1/Namespace:
    - velero-test
  v1/PersistentVolume:
    - pvc-1395e1fd-eae6-4bb2-922a-e5dd5839a696
  v1/PersistentVolumeClaim:
    - velero-test/ebs-claim
  v1/Pod:
    - velero-test/app
  v1/ServiceAccount:
    - velero-test/default

Velero-Native Snapshots:
  pvc-1395e1fd-eae6-4bb2-922a-e5dd5839a696:
    Snapshot ID:        snap-0595966a00a41783b
    Type:               gp3
    Availability Zone:  eu-central-1b
    IOPS:               &lt;N/A&gt;
</code></pre>
<p>Navigate to the AWS console and inspect the S3 bucket. Inside, you'll likely find a folder named <code>velero</code> containing the backup files.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693762522199/83c51db0-a32e-49fa-89d0-cb228f691305.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-simulate-a-data-loss">Simulate a Data Loss</h2>
<p>To simulate a data loss, we can simply delete the <code>velero-test</code> namespace:</p>
<pre><code class="lang-bash">kubectl delete ns velero-test
</code></pre>
<p>Check if the namespace is deleted:</p>
<pre><code class="lang-bash">kubectl get ns
</code></pre>
<p><strong><mark>Oh no, the namespace is gone. We need to restore it from the backup. Urgently! Production is down!</mark></strong></p>
<h2 id="heading-restore-a-backup">Restore a backup</h2>
<p>To restore the backup, we need to create a restore CR in the Kubernetes cluster. We can do this by running the following command:</p>
<pre><code class="lang-bash">velero restore create velero-test-restored --from-backup velero-test-backup --<span class="hljs-built_in">wait</span>
</code></pre>
<p>This will restore the backup and wait until the restore is completed.</p>
<pre><code class="lang-bash">Restore request <span class="hljs-string">"velero-test-restored"</span> submitted successfully.
Waiting <span class="hljs-keyword">for</span> restore to complete. You may safely press ctrl-c to stop waiting - your restore will <span class="hljs-built_in">continue</span> <span class="hljs-keyword">in</span> the background.
.
Restore completed with status: Completed. You may check <span class="hljs-keyword">for</span> more information using the commands `velero restore describe velero-test-restored` and `velero restore logs velero-test-restored`.
</code></pre>
<p>We can also check the details of the restore similar to the backup:</p>
<pre><code class="lang-bash">velero restore describe velero-test-restored --details
</code></pre>
<pre><code class="lang-bash">velero restore describe velero-test-restored --details
Name:         velero-test-restored
Namespace:    velero
Labels:       &lt;none&gt;
Annotations:  &lt;none&gt;

Phase:                       Completed
Total items to be restored:  6
Items restored:              6

Started:    2023-09-03 18:18:26 +0200 CEST
Completed:  2023-09-03 18:18:27 +0200 CEST

... omitted <span class="hljs-keyword">for</span> brevity
</code></pre>
<p>Now we can check if the everything is restored, including the data in the volume:</p>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -it app -n velero-test -- sh -c <span class="hljs-string">'cat /data/test.txt'</span>
</code></pre>
<p>And yes, the data is back, the timestamp is the same as before:</p>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -it app -n velero-test -- sh -c <span class="hljs-string">'cat /data/test.txt'</span>
Hi, today is Tue Sep  5 09:21:53 UTC 2023
</code></pre>
<h2 id="heading-housekeeping">Housekeeping</h2>
<p>Since the stack is no longer needed, you can proceed to delete it. Execute the following command to remove the stack:</p>
<pre><code class="lang-bash">pulumi destroy
</code></pre>
<p>This will delete all the resources created by the stack.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this blog post, we've explored how to deploy Velero using Pulumi and how to carry out backup and restore operations on an EKS cluster with EBS volumes. Velero is a great tool to handle disaster recovery and data migration scenarios.</p>
<p>In further blog posts, I will show some more advanced use cases of Velero including the integration into GitOps engines like ArgoCD or Flux.</p>
<p><mark>Stay tuned for more!</mark></p>
<h2 id="heading-resources">Resources</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.pulumi.com/registry/packages/eks/">https://www.pulumi.com/registry/packages/eks/</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/vmware-tanzu/velero">https://github.com/vmware-tanzu/velero</a></div>
]]></content:encoded></item><item><title><![CDATA[Deploying a Kubernetes Cluster in Strasbourg?!]]></title><description><![CDATA[TL;DR: The code
https://github.com/dirien/quick-bites/tree/main/pulumi-ovh-kube
 
Introduction
Thanks to the posts of Aurélie Vache, I discovered the OVH Managed Kubernetes Service.
https://twitter.com/aurelievache/status/1689173513851043840?s=20
 
I...]]></description><link>https://blog.ediri.io/deploying-a-kubernetes-cluster-in-strasbourg</link><guid isPermaLink="true">https://blog.ediri.io/deploying-a-kubernetes-cluster-in-strasbourg</guid><category><![CDATA[Pulumi]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[ovh]]></category><category><![CDATA[Infrastructure as code]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Thu, 10 Aug 2023 08:52:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1691657587038/a60f2380-10a5-414e-9069-75242422ec3c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-the-code">TL;DR: The code</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/pulumi-ovh-kube">https://github.com/dirien/quick-bites/tree/main/pulumi-ovh-kube</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>Thanks to the posts of <a target="_blank" href="https://twitter.com/aurelievache">Aurélie Vache</a>, I discovered the <a target="_blank" href="https://www.ovh.com/en/kubernetes/">OVH Managed Kubernetes Service</a>.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/aurelievache/status/1689173513851043840?s=20">https://twitter.com/aurelievache/status/1689173513851043840?s=20</a></div>
<p> </p>
<p>I had already heard about it, but I had never tried it. So I decided to give it a try and use <a target="_blank" href="https://www.pulumi.com">Pulumi</a> to deploy a Kubernetes cluster in the <a target="_blank" href="https://www.ovh.com/">OVH</a> data center in Strasbourg.</p>
<p>What could be better than a Kubernetes cluster in Strasbourg, one of the European Union capitals, to deploy a Minecraft server?</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p><a target="_blank" href="https://www.pulumi.com/docs/get-started/install/">Pulumi</a></p>
</li>
<li><p><a target="_blank" href="https://app.pulumi.com/signup">Pulumi Account</a> - this is optional, but convenient to handle the state of stack.</p>
</li>
<li><p><a target="_blank" href="https://www.ovhcloud.com/en/public-cloud/">OVH Account</a> - this is required to use the OVH Managed Kubernetes Service.</p>
</li>
<li><p>kubectl - this is required to interact with the Kubernetes cluster. You can install it by following the instructions <a target="_blank" href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">here</a>.</p>
</li>
<li><p><a target="_blank" href="https://golang.org/doc/install">Go</a></p>
</li>
<li><p>Minecraft client - this is required to connect to the Minecraft server. You can download it <a target="_blank" href="https://www.minecraft.net/en-us/download">here</a>.</p>
</li>
</ul>
<h2 id="heading-create-a-pulumi-project">Create a Pulumi Project</h2>
<p>Everything starts with a Pulumi project. Let's create one and we will use the Golang as our programming language of choice.</p>
<pre><code class="lang-bash">mkdir pulumi-ovh-kube
<span class="hljs-built_in">cd</span> pulumi-ovh-kube
pulumi new go --force
</code></pre>
<p>For the sake of simplicity, we can keep all the default values in the wizard.</p>
<p>Now, we need to install the OVH provider for Pulumi. We can do that by running the following command:</p>
<pre><code class="lang-bash">go get github.com/dirien/pulumi-ovh/sdk
</code></pre>
<p>Now, we can start writing our code. Open the <code>main.go</code> file and replace the content with the following code:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"github.com/dirien/pulumi-ovh/sdk/go/ovh/cloudproject"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi/sdk/v3/go/pulumi"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {

        serviceName := config.Require(ctx, <span class="hljs-string">"serviceName"</span>)

        mykube, err := cloudproject.NewKube(ctx, <span class="hljs-string">"mykube"</span>, &amp;cloudproject.KubeArgs{
            ServiceName: pulumi.String(serviceName),
            Name:        pulumi.String(<span class="hljs-string">"myFirstOVHKubernetesCluster"</span>),
            Region:      pulumi.String(<span class="hljs-string">"SBG5"</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        ctx.Export(<span class="hljs-string">"kubeconfig"</span>, mykube.Kubeconfig)

        nodePool, err := cloudproject.NewKubeNodePool(ctx, <span class="hljs-string">"mykubenodepool"</span>, &amp;cloudproject.KubeNodePoolArgs{
            ServiceName:   pulumi.String(serviceName),
            KubeId:        mykube.ID(),
            Name:          pulumi.String(<span class="hljs-string">"default"</span>),
            Autoscale:     pulumi.BoolPtr(<span class="hljs-literal">true</span>),
            DesiredNodes:  pulumi.Int(<span class="hljs-number">1</span>),
            MinNodes:      pulumi.Int(<span class="hljs-number">1</span>),
            MaxNodes:      pulumi.Int(<span class="hljs-number">2</span>),
            FlavorName:    pulumi.String(<span class="hljs-string">"d2-8"</span>),
            MonthlyBilled: pulumi.BoolPtr(<span class="hljs-literal">false</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        ctx.Export(<span class="hljs-string">"nodePoolId"</span>, nodePool.ID())
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>This code will create a Kubernetes cluster in the OVH data center in Strasbourg (<code>SBG5</code>). Additionally, it will create a node pool with 2 nodes. The node pool will be configured to scale between 1 and 2 nodes and I want that it starts with 1 (<code>desiredNodes</code>).</p>
<p>The node pool will use the flavor <code>d2-8</code> which is a flavor with 4 vCPUs and 8GB of RAM from the so-called <code>Discover</code> range.</p>
<p>These instances are perfect for test, development and <mark>sandbox environments </mark> and <mark>Minecraft is a sandbox game,</mark> so it fits perfectly!</p>
<h2 id="heading-add-the-kubernetes-provider">Add the Kubernetes provider</h2>
<p>Before deploying applications in our Kubernetes clusters, we need to add the Kubernetes provider to our project as we are going to run the Minecraft server as a container.</p>
<p>I wrote a dedicated blog post about the usage of Helm charts in Pulumi, go check it out:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.linkedin.com/pulse/how-use-pulumi-kubernetes-provider-helm-resource-engin-diri">https://www.linkedin.com/pulse/how-use-pulumi-kubernetes-provider-helm-resource-engin-diri</a></div>
<p> </p>
<p>We need to add the Kubernetes provider like this:</p>
<pre><code class="lang-bash">go get github.com/pulumi/pulumi-kubernetes/sdk/v4
</code></pre>
<p>And after we successfully added the Kubernetes provider, we can add the following code to the <code>main.go</code> file:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"github.com/dirien/pulumi-ovh/sdk/go/ovh/cloudproject"</span>
    k8s <span class="hljs-string">"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi/sdk/v3/go/pulumi"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// truncated for brevity</span>

        k8sProvider, err := k8s.NewProvider(ctx, <span class="hljs-string">"k8s"</span>, &amp;k8s.ProviderArgs{
            Kubeconfig: mykube.Kubeconfig,
        }, pulumi.DependsOn([]pulumi.Resource{nodePool, mykube}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        _, err = helm.NewRelease(ctx, <span class="hljs-string">"minecraft"</span>, &amp;helm.ReleaseArgs{
            Chart:   pulumi.String(<span class="hljs-string">"minecraft"</span>),
            Version: pulumi.String(<span class="hljs-string">"4.9.3"</span>),
            RepositoryOpts: &amp;helm.RepositoryOptsArgs{
                Repo: pulumi.String(<span class="hljs-string">"https://itzg.github.io/minecraft-server-charts"</span>),
            },
            CreateNamespace: pulumi.Bool(<span class="hljs-literal">true</span>),
            Namespace:       pulumi.String(<span class="hljs-string">"minecraft"</span>),
            Values: pulumi.Map{
                <span class="hljs-string">"minecraftServer"</span>: pulumi.Map{
                    <span class="hljs-string">"eula"</span>:        pulumi.Bool(<span class="hljs-literal">true</span>),
                    <span class="hljs-string">"motd"</span>:        pulumi.String(<span class="hljs-string">"OVH Strasbourg - Minecraft Server"</span>),
                    <span class="hljs-string">"serviceType"</span>: pulumi.String(<span class="hljs-string">"LoadBalancer"</span>),
                },
            },
        }, pulumi.Provider(k8sProvider))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>This code will programmatically create a Kubernetes provider using the <code>kubeconfig</code> of the Kubernetes cluster we just created.</p>
<p>One thing to note here is the <code>pulumi.DependsOn([]pulumi.Resource{nodePool, mykube})</code> part. This will make sure that the Kubernetes provider is created after the Kubernetes cluster AND the node pool are created. Because we need the be able to schedule pods on the Kubernetes cluster. Only the Kubernetes API up and running is not enough.</p>
<p>Secondly, it will create a Helm release using the <code>itzg/minecraft-server-charts</code> chart. This chart will create a vanilla Java Minecraft server with a load balancer service, so that we can connect to the Minecraft server from the internet.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/itzg/minecraft-server-charts">https://github.com/itzg/minecraft-server-charts</a></div>
<p> </p>
<h2 id="heading-deploy-the-stack">Deploy the stack</h2>
<blockquote>
<p>Don't forget to set the <code>serviceName</code> configuration value to the name of your OVH Public Cloud project and add this to the <code>Pulumi.yaml</code> file.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">config:</span>
<span class="hljs-attr">serviceName:</span> <span class="hljs-string">&lt;your-service-name&gt;</span>
</code></pre>
</blockquote>
<p>Also, don't forget to set the <code>OVH_APPLICATION_KEY</code>, <code>OVH_APPLICATION_SECRET</code>, <code>OVH_CONSUMER_KEY</code> and <code>OVH_ENDPOINT</code> environment variables.</p>
<p>Now, we can deploy the stack by running the following command:</p>
<pre><code class="lang-bash">go mod tidy
pulumi up
</code></pre>
<p>This will take a few minutes, because it needs to create the Kubernetes cluster and the node pool.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1691655970491/f0fb4f85-4269-4f0a-8634-4a16ceeb2238.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1691655975835/17abb9ff-847e-4999-a66d-6db290e7eda3.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-connect-to-the-minecraft-server">Connect to the Minecraft server</h2>
<p>To connect to the Minecraft server, we need to get the IP address of the load balancer service. We can do that by running the following command:</p>
<pre><code class="lang-bash">pulumi stack output kubeconfig --show-secrets  &gt; kubeconfig.yaml
kubectl --kubeconfig=kubeconfig.yaml get svc -n minecraft -o jsonpath=<span class="hljs-string">'{.items[0].status.loadBalancer.ingress[0].ip}'</span>
</code></pre>
<p>This will give us the kubeconfig file we defined as output in the <code>main.go</code> file, and the <code>kubectl</code> command will be used to get the IP address of the load balancer service.</p>
<p>Now, we can connect to the Minecraft server using the IP address we just got:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1691655947425/0667f69e-fd76-432a-ba51-72510863de4d.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1691655952932/ad72187b-69f4-4462-a647-0596b7303852.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-housekeeping">Housekeeping</h2>
<p>To clean up the resources we created, we can run the following command:</p>
<pre><code class="lang-bash">pulumi destroy
</code></pre>
<p>This will destroy all the resources we created.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>We saw how easy it is to use Pulumi to create infrastructure as code.</p>
<p>This time, we used the OVH Public Cloud and it was a breeze to create a Kubernetes cluster and deploy a Minecraft server on it.</p>
]]></content:encoded></item><item><title><![CDATA[How to cross-compile your Rust applications using cross-rs and GitHub Actions]]></title><description><![CDATA[TL;DR: Le code
https://github.com/dirien/rust-cross-compile
 
Introduction
In this blog post, we will have a look at how to cross-compile your Rust applications using cross-rs and GitHub Actions. We will also have a look at how to use the cross-rs Do...]]></description><link>https://blog.ediri.io/how-to-cross-compile-your-rust-applications-using-cross-rs-and-github-actions</link><guid isPermaLink="true">https://blog.ediri.io/how-to-cross-compile-your-rust-applications-using-cross-rs-and-github-actions</guid><category><![CDATA[Rust]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[Developer]]></category><category><![CDATA[release management]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sun, 30 Jul 2023 21:37:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1690752953037/8354552a-28b4-4c70-8f75-d6e65ad4da96.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-le-code">TL;DR: Le code</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/rust-cross-compile">https://github.com/dirien/rust-cross-compile</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>In this blog post, we will have a look at how to cross-compile your Rust applications using cross-rs and GitHub Actions. We will also have a look at how to use the cross-rs Docker image to cross-compile your Rust applications locally.</p>
<p>But before we dig into the details, let's have a look at multi-platform support in Rust.</p>
<h3 id="heading-motivation-for-multi-platform-support">Motivation for multi-platform support</h3>
<p>Nowadays, it is common to use different operating systems and architectures. We have IoT devices that run on ARM processors and servers which run on x86 processors. Or Apple computers that run on Apple Silicon processors. And then we have Windows, Linux and macOS. Enough reasons to support multiple platforms when developing software.</p>
<p>But now comes the downside: Most OS APIs are not compatible with each other. This difference in APIs is the reason why have to create platform-dependent code.</p>
<h3 id="heading-how-rust-supports-multi-platform">How Rust supports multi-platform</h3>
<p>The good news is that Rust makes it easy to write multi-platform code. Rust has a built-in macro called <code>cfg</code> which enables us the conditional compilation of code. It supports a lot of options so we can easily write platform-dependent parts of our code depending on the target platform.</p>
<p><a target="_blank" href="https://doc.rust-lang.org/reference/conditional-compilation.html">https://doc.rust-lang.org/reference/conditional-compilation.html</a></p>
<p>Some examples of <code>cfg</code>:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[cfg(target_os = <span class="hljs-meta-string">"linux"</span>)]</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"This is Linux"</span>);
}

<span class="hljs-meta">#[cfg(target_os = <span class="hljs-meta-string">"macos"</span>)]</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"This is macOS"</span>);
}
</code></pre>
<p>The <code>cfg</code> macro also supports <code>any</code>, <code>all</code> and <code>not</code>:</p>
<ul>
<li><p><code>any</code> - If any of the given predicates is true, the code is included.</p>
</li>
<li><p><code>all</code> - If all of the given predicates are true, the code is included.</p>
</li>
<li><p><code>not</code> - If the given predicate is false, the code is included.</p>
</li>
</ul>
<pre><code class="lang-rust"><span class="hljs-meta">#[cfg(any(target_os = <span class="hljs-meta-string">"linux"</span>, target_os = <span class="hljs-meta-string">"macos"</span>))]</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"This is Linux or macOS"</span>);
}

<span class="hljs-meta">#[cfg(all(target_os = <span class="hljs-meta-string">"linux"</span>, target_arch = <span class="hljs-meta-string">"x86_64"</span>))]</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"This is Linux on x86_64"</span>);
}

<span class="hljs-meta">#[cfg(not(target_os = <span class="hljs-meta-string">"windows"</span>))]</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"This is not Windows"</span>);
}
</code></pre>
<p>Rust also supports platform-dependent dependencies. We can use the <code>target</code> attribute to specify dependencies for a specific platform. The <code>cfg</code> syntax is also supported.</p>
<pre><code class="lang-toml"><span class="hljs-section">[dependencies]</span>
<span class="hljs-comment"># This dependency is only used on Linux</span>
<span class="hljs-section">[target.'cfg(unix)'.dependencies]</span>
<span class="hljs-attr">openssl</span> = <span class="hljs-string">"1.0.1"</span>
</code></pre>
<p>Same for <code>dev-dependencies</code> and <code>build-dependencies</code>:</p>
<pre><code class="lang-toml"><span class="hljs-section">[dev-dependencies]</span>
<span class="hljs-comment"># This dev-dependency is only used on Linux</span>
<span class="hljs-section">[target.'cfg(unix)'.dev-dependencies]</span>
<span class="hljs-attr">openssl</span> = <span class="hljs-string">"1.0.1"</span>

<span class="hljs-section">[build-dependencies]</span>
<span class="hljs-comment"># This build-dependency is only used on Linux</span>
<span class="hljs-section">[target.'cfg(unix)'.build-dependencies]</span>
<span class="hljs-attr">openssl</span> = <span class="hljs-string">"1.0.1"</span>
</code></pre>
<h3 id="heading-rust-support-tiers">Rust support tiers</h3>
<p>Rust is organized in support tiers when it comes to multi-platform support. The Rust team provides different levels of support for different platforms. The support tiers are:</p>
<ul>
<li><p><mark>Tier 1 </mark> - Tier 1 platforms are "guaranteed to work", this is the highest level of support</p>
</li>
<li><p><mark>Tier 2</mark> - Tier 2 platforms are "guaranteed to build", but not necessarily to pass all tests</p>
</li>
<li><p><mark>Tier 3</mark> - Tier 3 platforms are those for which the Rust code has support, but which are not built or tested automatically. So there is no guarantee that they work.</p>
</li>
</ul>
<p>According to the Rust team, <a target="_blank" href="https://doc.rust-lang.org/nightly/rustc/platform-support.html">the following platforms are Tier 1</a>:</p>
<ul>
<li><p>aarch64-unknown-linux-gnu</p>
</li>
<li><p>i686-pc-windows-gnu</p>
</li>
<li><p>i686-pc-windows-msvc</p>
</li>
<li><p>i686-unknown-linux-gnu</p>
</li>
<li><p>x86_64-apple-darwin</p>
</li>
<li><p>x86_64-pc-windows-gnu</p>
</li>
<li><p>x86_64-pc-windows-msvc</p>
</li>
<li><p>x86_64-unknown-linux-gnu</p>
</li>
</ul>
<h3 id="heading-cross-rs-to-the-rescue"><code>cross-rs</code> to the rescue</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/cross-rs/cross">https://github.com/cross-rs/cross</a></div>
<p> </p>
<p>With <a target="_blank" href="https://github.com/cross-rs/cross"><code>cross-rs</code></a> we have an easy way to cross-compile our Rust applications. <code>cross-rs</code> works by using pre-made Dockerfiles to build and run your application inside a Docker container. The list of supported platforms is quite long, have a look at the following link for more information:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/cross-rs/cross/tree/main/docker">https://github.com/cross-rs/cross/tree/main/docker</a></div>
<p> </p>
<p>Here is a short selection of supported platforms:</p>
<ol>
<li><p><code>Dockerfile.x86_64-unknown-linux-gnu</code></p>
</li>
<li><p><code>Dockerfile.x86_64-pc-windows-gnu</code></p>
</li>
<li><p><code>Dockerfile.aarch64-unknown-linux-gnu</code></p>
</li>
<li><p><code>Dockerfile.i686-unknown-linux-gnu</code></p>
</li>
<li><p><code>Dockerfile.i686-pc-windows-gnu</code></p>
</li>
<li><p><code>Dockerfile.armv7-unknown-linux-gnueabihf</code></p>
</li>
<li><p><code>Dockerfile.riscv64gc-unknown-linux-gnu</code></p>
</li>
<li><p><code>Dockerfile.mips64-unknown-linux-gnuabi64</code></p>
</li>
<li><p><code>Dockerfile.powerpc64le-unknown-linux-gnu</code></p>
</li>
<li><p><code>Dockerfile.x86_64-unknown-linux-musl</code></p>
</li>
</ol>
<p>Pretty impressive, right?</p>
<p>Let us create a simple demo project to see <code>cross-rs</code> in action.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p><a target="_blank" href="https://www.rust-lang.org">Rust</a></p>
</li>
<li><p>An IDE or text editor of your choice</p>
</li>
<li><p><a target="_blank" href="https://www.docker.com">Docker</a></p>
</li>
<li><p>GitHub account</p>
</li>
<li><p><a target="_blank" href="https://cli.github.com">GitHub CLI</a></p>
</li>
</ul>
<h2 id="heading-initialize-the-demo-project">Initialize the demo project</h2>
<p>The demo project is a simple Rust application that prints a <a target="_blank" href="http://www.figlet.org/">FIGlet</a> text to the console. We use clap to parse the command line arguments. That's it. Nothing fancy!</p>
<pre><code class="lang-bash">cargo init --bin figctl
</code></pre>
<p>Then we add the <code>clap</code> dependency:</p>
<pre><code class="lang-bash">cargo add clap --features derive
</code></pre>
<p>And then we add a <code>figlet-rs</code> dependency:</p>
<pre><code class="lang-bash">cargo add figlet-rs
</code></pre>
<p>Now we can add the following code to <code>src/main.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> clap::{Parser, Args};
<span class="hljs-keyword">use</span> figlet_rs::FIGfont;

<span class="hljs-meta">#[derive(Parser, Debug)]</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">FigletCtl</span></span> {
    message: <span class="hljs-built_in">String</span>,
}

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-keyword">let</span> args = FigletCtl::parse();
    <span class="hljs-keyword">let</span> standard_font = FIGfont::standard().unwrap();
    <span class="hljs-keyword">let</span> figure = standard_font.convert(args.message.as_str());
    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"{}"</span>, figure.unwrap());
}
</code></pre>
<p>If you run the application now, you should see the following output:</p>
<pre><code class="lang-bash">➜ cargo run -q -- <span class="hljs-string">'Hello World!'</span>
  _   _          _   _            __        __                 _       _   _ 
 | | | |   ___  | | | |   ___     \ \      / /   ___    _ __  | |   __| | | |
 | |_| |  / _ \ | | | |  / _ \     \ \ /\ / /   / _ \  | <span class="hljs-string">'__| | |  / _` | | |
 |  _  | |  __/ | | | | | (_) |     \ V  V /   | (_) | | |    | | | (_| | |_|
 |_| |_|  \___| |_| |_|  \___/       \_/\_/     \___/  |_|    |_|  \__,_| (_)</span>
</code></pre>
<h2 id="heading-cross-compile-the-demo-project">Cross-compile the demo project</h2>
<p>You can install <code>cross-rs</code> with the following command:</p>
<pre><code class="lang-bash">cargo install cross --git https://github.com/cross-rs/cross
</code></pre>
<p>And let test it with a simple example.</p>
<pre><code class="lang-bash">cross build --target aarch64-unknown-linux-gnu
</code></pre>
<p>Now you should have a <code>target/aarch64-unknown-linux-gnu/debug/figctl</code> binary. Let's run it and see what happens.</p>
<pre><code class="lang-bash">➜ ./target/aarch64-unknown-linux-gnu/debug/figctl <span class="hljs-string">"Hello World!"</span>
zsh: <span class="hljs-built_in">exec</span> format error: ./target/aarch64-unknown-linux-gnu/debug/figctl
</code></pre>
<p>The binary is not executable on our host system. But we can run it inside a Docker container.</p>
<pre><code class="lang-bash">docker run --rm -it -v $(<span class="hljs-built_in">pwd</span>):/app  --platform=linux/arm64 -w /app rust ./target/aarch64-unknown-linux-gnu/debug/figctl <span class="hljs-string">'Hello World!'</span>
  _   _          _   _            __        __                 _       _   _ 
 | | | |   ___  | | | |   ___     \ \      / /   ___    _ __  | |   __| | | |
 | |_| |  / _ \ | | | |  / _ \     \ \ /\ / /   / _ \  | <span class="hljs-string">'__| | |  / _` | | |
 |  _  | |  __/ | | | | | (_) |     \ V  V /   | (_) | | |    | | | (_| | |_|
 |_| |_|  \___| |_| |_|  \___/       \_/\_/     \___/  |_|    |_|  \__,_| (_)</span>
</code></pre>
<p>It works! So let's see how we can use <code>cross-rs</code> to build our application for multiple platforms with GitHub Actions.</p>
<h2 id="heading-github-actions">GitHub Actions</h2>
<p>We need to create a file called <code>.github/workflows/ci.yml</code> and add the following content:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">build</span> <span class="hljs-string">and</span> <span class="hljs-string">release</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">workflow_dispatch:</span>
  <span class="hljs-attr">release:</span>
    <span class="hljs-attr">types:</span> [ <span class="hljs-string">created</span> ]

<span class="hljs-attr">permissions:</span>
  <span class="hljs-attr">contents:</span> <span class="hljs-string">write</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.platform.os_name</span> <span class="hljs-string">}}</span> <span class="hljs-string">with</span> <span class="hljs-string">rust</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.toolchain</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.platform.os</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">strategy:</span>
      <span class="hljs-attr">fail-fast:</span> <span class="hljs-literal">false</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-attr">platform:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">os_name:</span> <span class="hljs-string">Linux-aarch64</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">ubuntu-20.04</span>
            <span class="hljs-attr">target:</span> <span class="hljs-string">aarch64-unknown-linux-musl</span>
            <span class="hljs-attr">bin:</span> <span class="hljs-string">figctl-linux-arm64</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">os_name:</span> <span class="hljs-string">Linux-x86_64</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">ubuntu-20.04</span>
            <span class="hljs-attr">target:</span> <span class="hljs-string">x86_64-unknown-linux-gnu</span>
            <span class="hljs-attr">bin:</span> <span class="hljs-string">figctl-linux-amd64</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">os_name:</span> <span class="hljs-string">Windows-x86_64</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">windows-latest</span>
            <span class="hljs-attr">target:</span> <span class="hljs-string">x86_64-pc-windows-msvc</span>
            <span class="hljs-attr">bin:</span> <span class="hljs-string">figctl-amd64.exe</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">os_name:</span> <span class="hljs-string">macOS-x86_64</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">macOS-latest</span>
            <span class="hljs-attr">target:</span> <span class="hljs-string">x86_64-apple-darwin</span>
            <span class="hljs-attr">bin:</span> <span class="hljs-string">figctl-darwin-amd64</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">os_name:</span> <span class="hljs-string">macOS-aarch64</span>
            <span class="hljs-attr">os:</span> <span class="hljs-string">macOS-latest</span>
            <span class="hljs-attr">target:</span> <span class="hljs-string">aarch64-apple-darwin</span>
            <span class="hljs-attr">bin:</span> <span class="hljs-string">figctl-darwin-arm64</span>
        <span class="hljs-attr">toolchain:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">stable</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v3</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">binary</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">houseabsolute/actions-rust-cross@v0</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">command:</span> <span class="hljs-string">"build"</span>
          <span class="hljs-attr">target:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.platform.target</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">toolchain:</span> <span class="hljs-string">${{</span> <span class="hljs-string">matrix.toolchain</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">args:</span> <span class="hljs-string">"--locked --release"</span>
          <span class="hljs-attr">strip:</span> <span class="hljs-literal">true</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Rename</span> <span class="hljs-string">binary</span> <span class="hljs-string">(linux</span> <span class="hljs-string">and</span> <span class="hljs-string">macos)</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">mv</span> <span class="hljs-string">target/${{</span> <span class="hljs-string">matrix.platform.target</span> <span class="hljs-string">}}/release/figctl</span> <span class="hljs-string">target/${{</span> <span class="hljs-string">matrix.platform.target</span> <span class="hljs-string">}}/release/${{</span> <span class="hljs-string">matrix.platform.bin</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">matrix.platform.os_name</span> <span class="hljs-type">!=</span> <span class="hljs-string">'Windows-x86_64'</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Rename</span> <span class="hljs-string">binary</span> <span class="hljs-string">(windows)</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">mv</span> <span class="hljs-string">target/${{</span> <span class="hljs-string">matrix.platform.target</span> <span class="hljs-string">}}/release/figctl.exe</span> <span class="hljs-string">target/${{</span> <span class="hljs-string">matrix.platform.target</span> <span class="hljs-string">}}/release/${{</span> <span class="hljs-string">matrix.platform.bin</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">matrix.platform.os_name</span> <span class="hljs-string">==</span> <span class="hljs-string">'Windows-x86_64'</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Generate</span> <span class="hljs-string">SHA-256</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">shasum</span> <span class="hljs-string">-a</span> <span class="hljs-number">256</span> <span class="hljs-string">target/${{</span> <span class="hljs-string">matrix.platform.target</span> <span class="hljs-string">}}/release/${{</span> <span class="hljs-string">matrix.platform.bin</span> <span class="hljs-string">}}</span> <span class="hljs-string">|</span> <span class="hljs-string">cut</span> <span class="hljs-string">-d</span> <span class="hljs-string">' '</span> <span class="hljs-string">-f</span> <span class="hljs-number">1</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">target/${{</span> <span class="hljs-string">matrix.platform.target</span> <span class="hljs-string">}}/release/${{</span> <span class="hljs-string">matrix.platform.bin</span> <span class="hljs-string">}}.sha256</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Release</span> <span class="hljs-string">binary</span> <span class="hljs-string">and</span> <span class="hljs-string">SHA-256</span> <span class="hljs-string">checksum</span> <span class="hljs-string">to</span> <span class="hljs-string">GitHub</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">softprops/action-gh-release@v1</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">files:</span> <span class="hljs-string">|
            target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }}
            target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }}.sha256</span>
</code></pre>
<p>This workflow will build the application for the following platforms:</p>
<ul>
<li><p>Linux-aarch64</p>
</li>
<li><p>Linux-x86_64</p>
</li>
<li><p>Windows-x86_64</p>
</li>
<li><p>macOS-x86_64</p>
</li>
<li><p>macOS-aarch64</p>
</li>
</ul>
<p>It will also create a GitHub release for each platform.</p>
<p>Let's push the changes to GitHub and create a new release.</p>
<pre><code class="lang-bash">git add .
git commit -m <span class="hljs-string">"Add all the things"</span>
git push
</code></pre>
<p>Now go to the GitHub repository and create a new release or use the GitHub CLI.</p>
<pre><code class="lang-bash">➜ gh release create v0.1.0 
? Title (optional) My figctl cli
? Release notes Leave blank
? Is this a prerelease? No
? Submit? Publish release
https://github.com/dirien/rust-cross-compile/releases/tag/v0.1.0
</code></pre>
<p>This will trigger the GitHub Actions workflow and build the application for all the platforms.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1690752217312/ce86de1f-7439-4eca-a7a6-25ae4b9b1b06.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1690752244717/c4142236-0ede-4ba8-acd5-c52f6c412fc3.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Creating a cross-platform application with Rust is easy as Rust has a great toolchain and ecosystem. But it's not always easy to build the application for multiple platforms. With <code>cross-rs</code> and GitHub Actions, we can build our application for multiple platforms and with GitHub Releases, we can distribute the application to our users.</p>
<h2 id="heading-links">Links</h2>
<ul>
<li><p>https://github.com/cross-rs/cross</p>
</li>
<li><p>https://doc.rust-lang.org/nightly/rustc/platform-support.html</p>
</li>
<li><p>https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Kubernetes 1.26: Implementing Validating Admission Policies with Pulumi]]></title><description><![CDATA[TL;DR: Le code
As usual, the link to the repo:
https://github.com/dirien/quick-bites/tree/main/pulumi-validating-admission-policy
 
Introduction
In this blog article, we will discover how we can leverage Pulumi and the kubernetes provider to write an...]]></description><link>https://blog.ediri.io/kubernetes-126-implementing-validating-admission-policies-with-pulumi</link><guid isPermaLink="true">https://blog.ediri.io/kubernetes-126-implementing-validating-admission-policies-with-pulumi</guid><category><![CDATA[Pulumi]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[policy as code]]></category><category><![CDATA[Developer]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sun, 30 Jul 2023 16:10:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1690733265420/78943690-884a-4c3b-9c80-5754b12df0a8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-le-code">TL;DR: Le code</h2>
<p>As usual, the link to the repo:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/pulumi-validating-admission-policy">https://github.com/dirien/quick-bites/tree/main/pulumi-validating-admission-policy</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>In this blog article, we will discover how we can leverage <a target="_blank" href="https://www.pulumi.com/">Pulumi</a> and the <a target="_blank" href="https://www.pulumi.com/registry/packages/kubernetes/"><code>kubernetes</code> provider</a> to write and deploy a validating admission policy in Kubernetes. And all of these during the creation of the Kubernetes cluster.</p>
<p>This is interesting for a variety of reasons. It enables the immediate installation of essential policies right after the initiation of the cluster. Not only ensures this alignment with your set of company rules from the outset but also guarantees some kind of peace of mind. You can rest assured that any tools or services deployed on the cluster subsequently will abide by these policies.</p>
<p>The application of policies takes precedence over any service deployment, including potential policy tools like <a target="_blank" href="https://kyverno.io/">Kyverno</a> or <a target="_blank" href="https://open-policy-agent.github.io/gatekeeper/website/docs/">Gatekeeper (OPA)</a>. This is crucial because the sequence of subsequent tool deployments can't always be relied upon, thus these policies must be established before anything else is deployed.</p>
<p>This feature itself is currently in <code>alpha</code> and only available in Kubernetes starting from version <a target="_blank" href="https://kubernetes.io/blog/2022/12/20/validating-admission-policies-alpha/">1.26</a> and above and requires that the <code>ValidatingAdmissionPolicy</code> feature gate is enabled.</p>
<p>Depending on the way you deploy your cluster, this might be the case already. For example, if you use the <code>kubeadm</code> tool to bootstrap your cluster, you can enable the feature gate by adding the following to your <code>kubeadm-config.yaml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">kubeadm.k8s.io/v1beta3</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ClusterConfiguration</span>
<span class="hljs-attr">featureGates:</span>
  <span class="hljs-attr">ValidatingAdmissionPolicy:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>But this is not the topic of this article. We will focus on the provision of a managed Kubernetes cluster with the possibility to enable the feature gate during the creation of the cluster. Recently <a target="_blank" href="https://www.scaleway.com/en/">Scaleway</a> added the <code>ValidatingAdmissionPolicy</code> to their list of supported features.</p>
<p>To see all the features supported by Scaleway, you can run the following command using their <code>scw</code> CLI:</p>
<pre><code class="lang-shell">scw k8s version get 1.26.0
Name    1.26.0
Label   Kubernetes 1.26.0
Region  fr-par

Available Kubelet Arguments:
map[containerLogMaxFiles:uint16 containerLogMaxSize:quantity cpuCFSQuota:bool cpuCFSQuotaPeriod:duration cpuManagerPolicy:enum:none|static enableDebuggingHandlers:bool imageGCHighThresholdPercent:uint32 imageGCLowThresholdPercent:uint32 maxPods:uint16]

Available CNIs:
[cilium calico kilo]

Available Ingresses:
[none]

Available Container Runtimes:
[containerd]

Available Feature Gates:
[HPAScaleToZero GRPCContainerProbe ReadWriteOncePod ValidatingAdmissionPolicy CSINodeExpandSecret]

Available Admission Plugins:
[PodNodeSelector AlwaysPullImages PodTolerationRestriction]
</code></pre>
<p>As you can see, the <code>ValidatingAdmissionPolicy</code> feature gate is available, we just have to keep it in mind when creating our Pulumi code.</p>
<p>It is also worth mentioning that the validating admission policies use CEL (Common Expression Language) to declare its rules. So what is CEL?</p>
<h3 id="heading-cel">CEL?</h3>
<blockquote>
<p>The Common Expression Language (CEL) implements common semantics for expression evaluation, enabling different applications to more easily interoperate.</p>
</blockquote>
<p>In short, CEL is a language that allows you to write expressions that can be evaluated. It is capable of creating complex policies that can be used in a variety of use cases with dealing with Webhooks at all.</p>
<p>The full spec is available on GitHub:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/google/cel-spec">https://github.com/google/cel-spec</a></div>
<p> </p>
<p>In my former blog article, I wrote how to write a DIY policy engine using webhooks and a server. Feel free to check this article, it is very interesting.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.kubesimplify.com/diy-how-to-build-a-kubernetes-policy-engine">https://blog.kubesimplify.com/diy-how-to-build-a-kubernetes-policy-engine</a></div>
<p> </p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this tutorial, you will need the following:</p>
<ul>
<li><p>A <a target="_blank" href="https://console.scaleway.com/">Scaleway account</a></p>
</li>
<li><p>Pulumi CLI installed on your machine (<a target="_blank" href="https://www.pulumi.com/docs/get-started/install/">installation instructions</a>)</p>
</li>
<li><p><a target="_blank" href="https://app.pulumi.com/signup">Free Pulumi account</a></p>
</li>
<li><p><a target="_blank" href="https://nodejs.org/en/download/">node.js</a></p>
</li>
</ul>
<p>Optionally, you can also install the following tools:</p>
<ul>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/">kubectl</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/scaleway/scaleway-cli">The Scaleway CLI</a></p>
</li>
</ul>
<h2 id="heading-create-a-new-pulumi-project">Create a new Pulumi project</h2>
<p>You may spot it already, that we will use <code>TypeScript</code> as the programming language for this project. But feel free to use any of the <a target="_blank" href="https://www.pulumi.com/docs/languages-sdks/">Pulumi-supported languages</a>.</p>
<p>To create a new Pulumi project, run the following command in your terminal:</p>
<pre><code class="lang-bash">mkdir pulumi-validating-admission-policy
<span class="hljs-built_in">cd</span> pulumi-validating-admission-policy
pulumi new typescript --force
</code></pre>
<p>You will be prompted with a wizard to create a new Pulumi project. You can keep the default values for all questions except the last one.</p>
<blockquote>
<p>Note: You may have to run <code>pulumi login</code> before you can create a new project, depending if you already have a Pulumi account or not.</p>
</blockquote>
<pre><code class="lang-bash">pulumi new typescript --force
This <span class="hljs-built_in">command</span> will walk you through creating a new Pulumi project.

Enter a value or leave blank to accept the (default), and press &lt;ENTER&gt;.
Press ^C at any time to quit.

project name: (pulumi-validating-admission-policy) 
project description: (A minimal TypeScript Pulumi program) 
Created project <span class="hljs-string">'pulumi-validating-admission-policy'</span>

Please enter your desired stack name.
To create a stack <span class="hljs-keyword">in</span> an organization, use the format &lt;org-name&gt;/&lt;stack-name&gt; (e.g. `acmecorp/dev`).
stack name: (dev) 
Created stack <span class="hljs-string">'dev'</span>

Installing dependencies...


added 193 packages, and audited 194 packages <span class="hljs-keyword">in</span> 14s

65 packages are looking <span class="hljs-keyword">for</span> funding
  run `npm fund` <span class="hljs-keyword">for</span> details

found 0 vulnerabilities
Finished installing dependencies

Your new project is ready to go! ✨

To perform an initial deployment, run `pulumi up`
</code></pre>
<p>Now we can add the <code>scaleway</code> provider to our project. To do so, run the following command:</p>
<pre><code class="lang-bash">npm install @ediri/scaleway@2.25.1 --save-exact
</code></pre>
<p>And we need also the <code>kubernetes</code> provider:</p>
<pre><code class="lang-bash">npm install @pulumi/kubernetes@4.0.3 --save-exac
</code></pre>
<blockquote>
<p>I like to use the <code>--save-exact</code> flag to ensure that the exact version of the provider is used. Reduce the risk of potential breaking changes/bugs, if a new version of the provider is released. But feel free to omit this flag.</p>
</blockquote>
<h2 id="heading-create-a-new-kubernetes-cluster">Create a new Kubernetes cluster</h2>
<p>After having the libraries ready, we can add some configuratrion to your <code>Pulumi.yaml</code> file. We will add the following configuration:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">pulumi-validating-admission-policy</span>
<span class="hljs-string">...</span>
<span class="hljs-attr">config:</span>
  <span class="hljs-attr">scaleway:region:</span> <span class="hljs-string">"fr-par"</span>
  <span class="hljs-attr">scaleway:zone:</span> <span class="hljs-string">"fr-par-1"</span>
  <span class="hljs-attr">cluster:version:</span> <span class="hljs-string">"1.27"</span>
  <span class="hljs-attr">cluster:auto_upgrade:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">node:node_type:</span> <span class="hljs-string">"PLAY2-NANO"</span>
  <span class="hljs-attr">node:auto_scale:</span> <span class="hljs-literal">false</span>
  <span class="hljs-attr">node:node_count:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">node:auto_heal:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>This set some default values for our cluster, like the region, zone, version, node type, node count and so on. You can keep the default values or change them to your needs. Every Pulumi <code>stack</code> can have its configuration values and override the default values.</p>
<p>After having the configuration in place, we can add the following code to our <code>index.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> pulumi <span class="hljs-keyword">from</span> <span class="hljs-string">"@pulumi/pulumi"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> scaleway <span class="hljs-keyword">from</span> <span class="hljs-string">"@ediri/scaleway"</span>;

<span class="hljs-keyword">const</span> clusterConfig = <span class="hljs-keyword">new</span> pulumi.Config(<span class="hljs-string">"cluster"</span>)

<span class="hljs-keyword">const</span> kapsule = <span class="hljs-keyword">new</span> scaleway.K8sCluster(<span class="hljs-string">"pulumi-validating-admission-policy-cluster"</span>, {
    version: clusterConfig.require(<span class="hljs-string">"version"</span>),
    cni: <span class="hljs-string">"cilium"</span>,
    deleteAdditionalResources: <span class="hljs-literal">true</span>,
    featureGates: [
        <span class="hljs-string">"ValidatingAdmissionPolicy"</span>,
    ],
    autoUpgrade: {
        enable: clusterConfig.requireBoolean(<span class="hljs-string">"auto_upgrade"</span>),
        maintenanceWindowStartHour: <span class="hljs-number">3</span>,
        maintenanceWindowDay: <span class="hljs-string">"monday"</span>
    },
});

<span class="hljs-keyword">const</span> nodeConfig = <span class="hljs-keyword">new</span> pulumi.Config(<span class="hljs-string">"node"</span>)

<span class="hljs-keyword">new</span> scaleway.K8sPool(<span class="hljs-string">"pulumi-validating-admission-policy-node-pool"</span>, {
    nodeType: nodeConfig.require(<span class="hljs-string">"node_type"</span>),
    size: nodeConfig.requireNumber(<span class="hljs-string">"node_count"</span>),
    autoscaling: nodeConfig.requireBoolean(<span class="hljs-string">"auto_scale"</span>),
    autohealing: nodeConfig.requireBoolean(<span class="hljs-string">"auto_heal"</span>),
    clusterId: kapsule.id,
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> kapsuleName = kapsule.name;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> kubeconfig = pulumi.secret(kapsule.kubeconfigs[<span class="hljs-number">0</span>].configFile);
</code></pre>
<p>Before we start to deploy our validating admission policy, let's have a look at the code.</p>
<p>The first resource we create is of type <code>K8sCluster</code>. This resource creates a new Kubernetes cluster on Scaleway. We use the configuration values to set the properties of the cluster.</p>
<p>The <code>featureGates</code> property is the one we are interested in. Here we activate the <code>ValidatingAdmissionPolicy</code> plugin.</p>
<p>The second resource we create is a <code>K8sPool</code>. This resource creates a new node pool for our cluster. Next to the definition of the node type, aka the instance type, we also set the <code>size</code> property to our <code>node_count</code> configuration value.</p>
<p>The last two lines of the code are used to export the name of the cluster and the <code>kubeconfig</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1690730975170/027616a2-7ea6-402a-8906-fa1cc7fbc0cb.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-writing-validating-admission-policies">Writing Validating Admission Policies</h2>
<p>Now that we have our cluster defined, we can head over to create some examples of validating admission policies.</p>
<p>First, we create a Pulumi Kubernetes provider. This provider gets the <code>kubeconfig</code> from our cluster and uses it to connect to the cluster.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> k8s <span class="hljs-keyword">from</span> <span class="hljs-string">"@pulumi/kubernetes"</span>;

<span class="hljs-comment">// omitted code for brevity</span>

<span class="hljs-keyword">const</span> provider = <span class="hljs-keyword">new</span> k8s.Provider(<span class="hljs-string">"k8s-provider"</span>, {
    kubeconfig: kubeconfig,
}, {dependsOn: [kapsule, nodePool]});
</code></pre>
<p>As the next step, we can finally start to write our validating admission policy. The policy itself will do a basic check if a team label is set on the <code>Deployment</code> and <code>StatefulSet</code> metadata tag. If not, we will reject the resource creation and return an error message.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> teamLabel = <span class="hljs-keyword">new</span> k8s.admissionregistration.v1alpha1.ValidatingAdmissionPolicy(<span class="hljs-string">"pulumi-validating-admission-policy-0"</span>, {
    metadata: {
        name: <span class="hljs-string">"team-label"</span>,
    },
    spec: {
        failurePolicy: <span class="hljs-string">"Fail"</span>,
        matchConstraints: {
            resourceRules: [
                {
                    apiGroups: [<span class="hljs-string">"apps"</span>],
                    apiVersions: [<span class="hljs-string">"v1"</span>],
                    operations: [<span class="hljs-string">"CREATE"</span>, <span class="hljs-string">"UPDATE"</span>],
                    resources: [<span class="hljs-string">"deployments"</span>],
                },
                {
                    apiGroups: [<span class="hljs-string">"apps"</span>],
                    apiVersions: [<span class="hljs-string">"v1"</span>],
                    operations: [<span class="hljs-string">"CREATE"</span>, <span class="hljs-string">"UPDATE"</span>],
                    resources: [<span class="hljs-string">"statefulsets"</span>],
                }
            ]
        },
        matchConditions: [
            {
                name: <span class="hljs-string">"team-label"</span>,
                expression: <span class="hljs-string">`has(object.metadata.namespace) &amp;&amp; !(object.metadata.namespace.startsWith("kube-"))`</span>
            }
        ],
        validations: [
            {
                expression: <span class="hljs-string">`has(object.metadata.labels.team)`</span>,
                message: <span class="hljs-string">"Team label is missing."</span>,
                reason: <span class="hljs-string">"Invalid"</span>,
            },
            {
                expression: <span class="hljs-string">`has(object.spec.template.metadata.labels.team)`</span>,
                message: <span class="hljs-string">"Team label is missing from pod template."</span>,
                reason: <span class="hljs-string">"Invalid"</span>,
            }
        ]
    }
}, {provider: provider});
</code></pre>
<p>There is a lot of code in this example, so let's have a more in-depth look at it:</p>
<ul>
<li><p>We set the <code>failurePolicy</code> to <code>Fail</code>. This means that the resource creation will fail if the validation fails. The other option is <code>Ignore</code>, which will ignore the validation and create the resource.</p>
</li>
<li><p>The <code>matchConstraints</code> property defines the resources, which should be validated. In our case, we want to validate <code>Deployment</code> and <code>StatefulSet</code> resources.</p>
</li>
<li><p>The <code>matchConditions</code> property defines the conditions, which should be met, to run the validation. In our case, we want to validate only resources in namespaces, which are not <code>kube-*</code>. This helps us to even add more fine-grained control over the resources, which should be validated.</p>
</li>
<li><p>Last but not least, the <code>validations</code> property defines the actual validation. In our case, we check if the <code>team</code> label is set on the resource and the pod template.</p>
</li>
</ul>
<p>Now we need to connect the validating admission policy to a specific scope or context. We can do this by creating a <code>ValidatingAdmissionPolicyBinding</code> resource.</p>
<blockquote>
<p><strong>Note:</strong> You can bind one validating admission policy to multiple scopes or contexts.</p>
</blockquote>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> teamLabelBinding = <span class="hljs-keyword">new</span> k8s.admissionregistration.v1alpha1.ValidatingAdmissionPolicyBinding(<span class="hljs-string">"pulumi-validating-admission-policy-1"</span>, {
    metadata: {
        name: <span class="hljs-string">"team-label-binding"</span>,
    },
    spec: {
        policyName: teamLabel.metadata.name,
        validationActions: [<span class="hljs-string">"Deny"</span>],
        matchResources: {}
    }
}, {provider: provider});
</code></pre>
<p>Here we set the <code>policyName</code> to the name of the validating admission policy we created before. One interesting property is the <code>validationActions</code> property. Here we can define, how validations of a policy should be enforced. We have the following options:</p>
<ul>
<li><p><code>Audit</code>: This will only audit the validation and not enforce it and will be added to the audit event.</p>
</li>
<li><p><code>Deny</code>: This will deny the request and return an error message.</p>
</li>
<li><p><code>Warn</code>: This will warn the user, but will not deny the request.</p>
</li>
</ul>
<p>The last property we need to set is the <code>matchResources</code> property. This property defines the resources, in our case the value and this means all resources, which should be validated by the validating admission policy.</p>
<h2 id="heading-deploy-the-cluster-and-the-validating-admission-policies">Deploy the Cluster and the Validating Admission Policies</h2>
<p>Now that we have our cluster and our validating admission policies defined, we can deploy them. To do so, run the following command:</p>
<pre><code class="lang-bash">pulumi up
Previewing update (dev)

     Type                                                                                  Name                                                   Plan       
 +   pulumi:pulumi:Stack                                                                   pulumi-validating-admission-policy-dev                 create     
 +   ├─ scaleway:index:K8sCluster                                                          pulumi-validating-admission-policy-cluster             create     
 +   ├─ scaleway:index:K8sPool                                                             pulumi-validating-admission-policy-node-pool           create     
 +   ├─ pulumi:providers:kubernetes                                                        k8s-provider                                           create     
 +   ├─ kubernetes:admissionregistration.k8s.io/v1alpha1:ValidatingAdmissionPolicy         pulumi-validating-admission-policy-team-label          create     
 +   └─ kubernetes:admissionregistration.k8s.io/v1alpha1:ValidatingAdmissionPolicyBinding  pulumi-validating-admission-policy-binding-team-label  create     


Outputs:
    kapsuleName: <span class="hljs-string">"pulumi-validating-admission-policy-cluster-d786e2b"</span>
    kubeconfig : output&lt;string&gt;

Resources:
    + 6 to create

Do you want to perform this update?  [Use arrows to move, <span class="hljs-built_in">type</span> to filter]
  yes
&gt; no
  details
</code></pre>
<p>And confirm the deployment with <code>yes</code>. After a few minutes, the deployment should be finished and you should see similar output:</p>
<pre><code class="lang-bash">Do you want to perform this update? yes
Updating (dev)

...

Resources:
    + 6 created

Duration: 5m39s
</code></pre>
<h2 id="heading-test-the-validating-admission-policy">Test the Validating Admission Policy</h2>
<p>Now that we have our cluster and our validating admission policies deployed, we can test them. To do so, we will create a <code>Deployment</code> resource without the <code>team</code> label. First, we need to get the <code>kubeconfig</code> from the cluster. We can do this by running this command:</p>
<pre><code class="lang-bash">pulumi stack output kubeconfig --show-secrets -s dev &gt; kubeconfig
</code></pre>
<p>Create a new file called <code>deployment.yaml</code> and add this as content:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">frontend</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">environment:</span> <span class="hljs-string">production</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">guestbook</span>
      <span class="hljs-attr">tier:</span> <span class="hljs-string">frontend</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">2</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">guestbook</span>
        <span class="hljs-attr">tier:</span> <span class="hljs-string">frontend</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">php-redis</span>
          <span class="hljs-attr">image:</span> <span class="hljs-string">gcr.io/google-samples/gb-frontend:v4</span>
          <span class="hljs-attr">resources:</span>
            <span class="hljs-attr">requests:</span>
              <span class="hljs-attr">cpu:</span> <span class="hljs-string">100m</span>
              <span class="hljs-attr">memory:</span> <span class="hljs-string">100Mi</span>
            <span class="hljs-attr">limits:</span>
              <span class="hljs-attr">cpu:</span> <span class="hljs-string">"3"</span>
              <span class="hljs-attr">memory:</span> <span class="hljs-string">4Gi</span>
</code></pre>
<p>Create the <code>Deployment</code> resource by executing this command:</p>
<pre><code class="lang-bash">kubectl apply -f deployment.yaml --kubeconfig kubeconfig
</code></pre>
<p>This should fail with a similar error message:</p>
<pre><code class="lang-bash">The deployments <span class="hljs-string">"frontend"</span> is invalid: : ValidatingAdmissionPolicy <span class="hljs-string">'team-label'</span> with binding <span class="hljs-string">'team-label-binding'</span> denied request: Team label is missing.
</code></pre>
<h2 id="heading-create-more-complex-validating-admission-policy">Create More Complex Validating Admission Policy</h2>
<p>In the previous example, we created a simple validating admission policy, which checks if the <code>team</code> label is set. Useful but still pretty basic.</p>
<p>Let us build a more complex validating admission policy. We will check in the next policy several different rules for the <code>Deployment</code> resource using multiple <code>expression</code> in the <code>validations</code> field.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> prodReadyPolicy = <span class="hljs-keyword">new</span> k8s.admissionregistration.v1alpha1.ValidatingAdmissionPolicy(<span class="hljs-string">"pulumi-validating-admission-policy-prod-ready"</span>, {
    metadata: {
        name: <span class="hljs-string">"prod-ready-policy"</span>,
    },
    spec: {
        failurePolicy: <span class="hljs-string">"Fail"</span>,
        matchConstraints: {
            resourceRules: [
                {
                    apiGroups: [<span class="hljs-string">"apps"</span>],
                    apiVersions: [<span class="hljs-string">"v1"</span>],
                    operations: [<span class="hljs-string">"CREATE"</span>, <span class="hljs-string">"UPDATE"</span>],
                    resources: [<span class="hljs-string">"deployments"</span>],
                }]
        },
        validations: [
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, has(c.resources) &amp;&amp; has(c.resources.limits) &amp;&amp; has(c.resources.limits.cpu)
)`</span>,
                message: <span class="hljs-string">"No CPU resource limits specified for any container."</span>,
                reason: <span class="hljs-string">"Invalid"</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, has(c.resources) &amp;&amp; has(c.resources.limits) &amp;&amp; has(c.resources.limits.memory)
)`</span>,
                message: <span class="hljs-string">"No memory resource limits specified for any container."</span>,
                reason: <span class="hljs-string">"Invalid"</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, !c.image.endsWith(':latest')
)`</span>,
                message: <span class="hljs-string">"Image tag must not be latest."</span>,
                reason: <span class="hljs-string">"Invalid"</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, c.image.startsWith('myregistry.azurecr.io')
)`</span>,
                message: <span class="hljs-string">"Image must be pulled from myregistry.azurecr.io."</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, has(c.securityContext)
)`</span>,
                message: <span class="hljs-string">"Security context is missing."</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
c, has(c.readinessProbe) &amp;&amp; has(c.livenessProbe)
)`</span>,
                message: <span class="hljs-string">"No health checks configured."</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, !('securityContext' in c) || !('privileged' in c.securityContext) || c.securityContext.privileged == false
)`</span>,
                message: <span class="hljs-string">"Privileged containers are not allowed."</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.initContainers.all(
    c, !('securityContext' in c) || !('privileged' in c.securityContext) || c.securityContext.privileged == false
)`</span>,
                message: <span class="hljs-string">"Privileged init containers are not allowed."</span>,
            }]
    }
}, {provider: provider});

<span class="hljs-keyword">const</span> prodReadyPolicyBinding = <span class="hljs-keyword">new</span> k8s.admissionregistration.v1alpha1.ValidatingAdmissionPolicyBinding(<span class="hljs-string">"pulumi-validating-admission-policy-prod-ready-binding"</span>, {
    metadata: {
        name: <span class="hljs-string">"prod-ready-policy-binding"</span>,
    },
    spec: {
        policyName: prodReadyPolicy.metadata.name,
        validationActions: [<span class="hljs-string">"Deny"</span>],
        matchResources: {
            objectSelector: {
                matchLabels: {
                    <span class="hljs-string">"environment"</span>: <span class="hljs-string">"production"</span>,
                }
            }
        }
    }
}, {provider: provider});
</code></pre>
<p>Let us go through this <strong>monster</strong> step by step and we focus only on the <code>validations</code> property as the rest is similar to the previous example:</p>
<ul>
<li><p><code>object.spec.template.spec.containers.all(c, has(c.resources) &amp;&amp; has(c.resources.limits) &amp;&amp; has(c.resources.limits.cpu))</code>: This expression checks if all containers have a CPU resource limit set.</p>
</li>
<li><p><code>object.spec.template.spec.containers.all(c, has(c.resources) &amp;&amp; has(c.resources.limits) &amp;&amp; has(c.resources.limits.memory))</code>: This expression checks if all containers have a memory resource limit set.</p>
</li>
<li><p><code>object.spec.template.spec.containers.all(c, !c.image.endsWith(':latest'))</code>: This expression checks if the image tag is not <code>latest</code>. We enforce this to make sure that we do not use the <code>latest</code> tag in production.</p>
</li>
<li><p><code>object.spec.template.spec.containers.all(c, c.image.startsWith('myregistry.azurecr.io'))</code>: This expression checks if the image is pulled from <code>myregistry.azurecr.io</code>. This can be useful if you want to enforce that only images from a specific registry are used.</p>
</li>
<li><p><code>object.spec.template.spec.containers.all(c, has(c.securityContext))</code>: This expression checks if all containers have a security context set.</p>
</li>
<li><p><code>object.spec.template.spec.containers.all(c, has(c.readinessProbe) &amp;&amp; has(c.livenessProbe))</code>: This expression checks if all containers have a readiness and liveness probe configured.</p>
</li>
<li><p><code>object.spec.template.spec.containers.all(c, !('securityContext' in c) || !('privileged' in c.securityContext) || c.securityContext.privileged == false)</code>: This expression checks that no container is privileged.</p>
</li>
<li><p><code>object.spec.template.spec.initContainers.all(c, !('securityContext' in c) || !('privileged' in c.securityContext) || c.securityContext.privileged == false)</code>: This expression checks that no init container is privileged.</p>
</li>
</ul>
<h2 id="heading-parameter-resources">Parameter resources</h2>
<p>Now we come to an even more advanced topic. In the previous example, we hard-coded the policy configuration within the definition. With the property <code>paramKind</code> we can define a custom Kubernetes resource that contains the policy configuration.</p>
<p>But we need to create a <a target="_blank" href="https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/">custom resource definition (CRD)</a> for our parameter resource before.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> prodReadyCRD = <span class="hljs-keyword">new</span> k8s.apiextensions.v1.CustomResourceDefinition(<span class="hljs-string">"pulumi-validating-admission-policy-prod-ready-crd"</span>, {
    metadata: {
        name: <span class="hljs-string">"prodreadychecks.pulumi.com"</span>,
    },
    spec: {
        group: <span class="hljs-string">"pulumi.com"</span>,
        versions: [
            {
                name: <span class="hljs-string">"v1"</span>,
                served: <span class="hljs-literal">true</span>,
                storage: <span class="hljs-literal">true</span>,
                schema: {
                    openAPIV3Schema: {
                        <span class="hljs-keyword">type</span>: <span class="hljs-string">"object"</span>,
                        properties: {
                            spec: {
                                <span class="hljs-keyword">type</span>: <span class="hljs-string">"object"</span>,
                                properties: {
                                    registry: {
                                        <span class="hljs-keyword">type</span>: <span class="hljs-string">"string"</span>,
                                    },
                                    version: {
                                        <span class="hljs-keyword">type</span>: <span class="hljs-string">"string"</span>,
                                    },
                                    privileged: {
                                        <span class="hljs-keyword">type</span>: <span class="hljs-string">"boolean"</span>,
                                    }
                                }
                            }
                        }
                    }
                }
            }
        ],
        scope: <span class="hljs-string">"Namespaced"</span>,
        names: {
            plural: <span class="hljs-string">"prodreadychecks"</span>,
            singular: <span class="hljs-string">"prodreadycheck"</span>,
            kind: <span class="hljs-string">"ProdReadyCheck"</span>,
            shortNames: [<span class="hljs-string">"prc"</span>],
        },
    },
}, {provider: provider});

<span class="hljs-keyword">const</span> prodReadyCR = <span class="hljs-keyword">new</span> k8s.apiextensions.CustomResource(<span class="hljs-string">"pulumi-validating-admission-policy-prod-ready-crd-validation"</span>, {
    apiVersion: <span class="hljs-string">"pulumi.com/v1"</span>,
    kind: <span class="hljs-string">"ProdReadyCheck"</span>,
    metadata: {
        name: <span class="hljs-string">"prodreadycheck-validation"</span>,
    },
    spec: {
        registry: <span class="hljs-string">"myregistry.azurecr.io/"</span>,
        version: <span class="hljs-string">"latest"</span>,
        privileged: <span class="hljs-literal">false</span>,
    }

}, {provider: provider, dependsOn: [prodReadyCRD]});
</code></pre>
<p>The CRD defines the schema of the parameter resource. In our case, we define a schema with three properties:</p>
<ul>
<li><p><code>registry</code>: The registry from which the image must be pulled.</p>
</li>
<li><p><code>version</code>: The image version to block.</p>
</li>
<li><p><code>privileged</code>: If the privileged flag is allowed.</p>
</li>
</ul>
<p>And then we create a custom resource (CR) out of this.</p>
<p>We can now use this CR in our policy, for this example I will create a new policy and binding:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> prodReadyPolicyParam = <span class="hljs-keyword">new</span> k8s.admissionregistration.v1alpha1.ValidatingAdmissionPolicy(<span class="hljs-string">"pulumi-validating-admission-policy-prod-ready-param"</span>, {
    metadata: {
        name: <span class="hljs-string">"prod-ready-policy-param"</span>,
    },
    spec: {
        failurePolicy: <span class="hljs-string">"Fail"</span>,
        paramKind: {
            apiVersion: prodReadyCR.apiVersion,
            kind: prodReadyCR.kind,
        },
        matchConstraints: {
            resourceRules: [
                {
                    apiGroups: [<span class="hljs-string">"apps"</span>],
                    apiVersions: [<span class="hljs-string">"v1"</span>],
                    operations: [<span class="hljs-string">"CREATE"</span>, <span class="hljs-string">"UPDATE"</span>],
                    resources: [<span class="hljs-string">"deployments"</span>],
                }]
        },
        validations: [
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, has(c.resources) &amp;&amp; has(c.resources.limits) &amp;&amp; has(c.resources.limits.cpu)
)`</span>,
                message: <span class="hljs-string">"No CPU resource limits specified for any container."</span>,
                reason: <span class="hljs-string">"Invalid"</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, has(c.resources) &amp;&amp; has(c.resources.limits) &amp;&amp; has(c.resources.limits.memory)
)`</span>,
                message: <span class="hljs-string">"No memory resource limits specified for any container."</span>,
                reason: <span class="hljs-string">"Invalid"</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, !c.image.endsWith(params.spec.version)
)`</span>,
                messageExpression: <span class="hljs-string">"'Image tag must not be ' + params.spec.version"</span>,
                reason: <span class="hljs-string">"Invalid"</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, c.image.startsWith(params.spec.registry)
)`</span>,
                reason: <span class="hljs-string">"Invalid"</span>,
                messageExpression: <span class="hljs-string">"'Registry only allowed from: ' + params.spec.registry"</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, has(c.securityContext)
)`</span>,
                message: <span class="hljs-string">"Security context is missing."</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
c, has(c.readinessProbe) &amp;&amp; has(c.livenessProbe)
)`</span>,
                message: <span class="hljs-string">"No health checks configured."</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.containers.all(
    c, !('securityContext' in c) || !('privileged' in c.securityContext) || c.securityContext.privileged == params.spec.privileged
)`</span>,
                message: <span class="hljs-string">"Privileged containers are not allowed."</span>,
            },
            {
                expression: <span class="hljs-string">`object.spec.template.spec.initContainers.all(
    c, !('securityContext' in c) || !('privileged' in c.securityContext) || c.securityContext.privileged == params.spec.privileged
)`</span>,
                message: <span class="hljs-string">"Privileged init containers are not allowed."</span>,
            }]
    }
}, {provider: provider});

<span class="hljs-keyword">const</span> prodReadyPolicyBindingParam = <span class="hljs-keyword">new</span> k8s.admissionregistration.v1alpha1.ValidatingAdmissionPolicyBinding(<span class="hljs-string">"pulumi-validating-admission-policy-prod-ready-binding-param"</span>, {
    metadata: {
        name: <span class="hljs-string">"prod-ready-policy-binding-param"</span>,
    },
    spec: {
        policyName: prodReadyPolicyParam.metadata.name,
        validationActions: [<span class="hljs-string">"Deny"</span>],
        paramRef: {
            name: prodReadyCR.metadata.name,
            <span class="hljs-keyword">namespace</span>: prodReadyCR.metadata.namespace,
        },
        matchResources: {
            objectSelector: {
                matchLabels: {
                    <span class="hljs-string">"environment"</span>: <span class="hljs-string">"production"</span>,
                }
            }
        }
    }
}, {provider: provider});
</code></pre>
<p>The policy is the same as in the previous example, but now we use the parameter resource in the policy and in the binding resource. To access the value we use the <code>params.spec.xxx</code> CEL path, where xxx stands for the property we defined in our CR.</p>
<blockquote>
<p><strong>Note:</strong> To get better error messages, we use the <code>messageExpression</code> property instead of the <code>message</code> property.</p>
</blockquote>
<p>Now we can test the new policy by creating a deployment with a container that violates the policy and we should see a message similar to this:</p>
<pre><code class="lang-bash">The deployments <span class="hljs-string">"frontend"</span> is invalid: : ValidatingAdmissionPolicy <span class="hljs-string">'prod-ready-policy-param'</span> with binding <span class="hljs-string">'prod-ready-policy-binding-param'</span> denied request: Image tag must not be latest
</code></pre>
<h2 id="heading-housekeeping">Housekeeping</h2>
<p>To clean up all resources created by this example, run the following command:</p>
<pre><code class="lang-bash">pulumi destroy
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>We have seen how to use the <code>ValidatingAdmissionPolicy</code> resource to create a policy that validates the resources using CEL expressions. We have also seen how to use parameter resources to make the policy more flexible and reusable.</p>
<p>It is very nice to have a policy functionality inbuilt in Kubernetes without the need to install a separate policy engine. Especially when you want to ensure that the policy right from the start of the cluster is available.</p>
<p>And: You don't need to handle Webhooks, which is a big plus for me. You can easily render your cluster useless when your webhook is not available or not working correctly.</p>
<h2 id="heading-links">Links</h2>
<ul>
<li><p>https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#getting-started-with-validating-admission-policy</p>
</li>
<li><p>https://minikube.sigs.k8s.io/docs/handbook/config/#:~:text=in%20constants.go-,Enabling%20feature%20gates,is%20the%20status%20of%20it</p>
</li>
<li><p>https://www.pulumi.com/registry/packages/kubernetes/api-docs/admissionregistration/v1alpha1/</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Rust Development with Testcontainers]]></title><description><![CDATA[Introduction
In this blog post, we're going to explore how to use Testcontainers as part of our integration testing strategy in Rust. To have hands-on experience, we're going to build a simple web application that exposes a REST API to manage cars. T...]]></description><link>https://blog.ediri.io/rust-development-with-testcontainers</link><guid isPermaLink="true">https://blog.ediri.io/rust-development-with-testcontainers</guid><category><![CDATA[Rust]]></category><category><![CDATA[Testcontainers]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Developer]]></category><category><![CDATA[ci-cd]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Wed, 05 Jul 2023 11:23:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1688556150405/2e46e6e9-0b28-4329-8314-a424eded2d08.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In this blog post, we're going to explore how to use <a target="_blank" href="https://testcontainers.com/">Testcontainers</a> as part of our integration testing strategy in Rust. To have hands-on experience, we're going to build a simple web application that exposes a REST API to manage cars. The cars are stored in a <a target="_blank" href="https://www.mongodb.com/">MongoDB</a> database and as a web framework, we're going to use <a target="_blank" href="https://actix.rs/">Actix-Web</a>.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://testcontainers.com/">https://testcontainers.com/</a></div>
<p> </p>
<p>I highly recommend my previous blog post about the <code>actix-web</code> framework</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.ediri.io/rust-development-creating-a-rest-api-with-actix-web-for-beginners">https://blog.ediri.io/rust-development-creating-a-rest-api-with-actix-web-for-beginners</a></div>
<p> </p>
<p>Or check my whole Rust learning journey</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.ediri.io/series/learning-rust">https://blog.ediri.io/series/learning-rust</a></div>
<p> </p>
<h2 id="heading-what-is-testcontainers">What is Testcontainers?</h2>
<p><code>Testcontainers</code> is an open-source framework to provide throwaway instances of dependencies such as databases, message brokers, or any other service that can be started in a Docker container. It's available for many programming languages such as Java, Python, Go, and Rust and allows us to write test code that allows the user to start and stop containers.</p>
<p>This has <mark>several advantages</mark>:</p>
<ul>
<li><p>We can run tests against real components like PostgreSQL instead of an H2 in-memory database. This allows us to use PostgreSQL-specific features like JSONB columns or full-text search.</p>
</li>
<li><p>We can mock AWS services with Localstack. This allows us to test our code against AWS services without the need to create real resources in the cloud.</p>
</li>
<li><p>We can run our tests also in an offline environment.</p>
</li>
<li><p>We can also test better some edge cases like network failures or slow responses from the database.</p>
</li>
</ul>
<p>In this blog post, we will use the Rust version of Testcontainers, which is called <code>rust-testcontainers</code>.</p>
<p>Testcontainers offers also preconfigured implementations called <a target="_blank" href="https://testcontainers.com/modules/">modules</a> for different databases and services. As they are not available for Rust, I skipped them in this blog post.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://testcontainers.com/modules/">https://testcontainers.com/modules/</a></div>
<p> </p>
<h2 id="heading-the-testing-pyramid">The Testing Pyramid</h2>
<p>Before we head over to the implementation, let's talk about the different testing approaches. There are three different approaches to testing strategies:</p>
<ul>
<li><p><em>The Test Ice Cream Cone</em></p>
</li>
<li><p><em>The Test Pyramid</em></p>
</li>
<li><p><em>The Practical Test Pyramid</em></p>
</li>
</ul>
<h3 id="heading-the-test-ice-cream-cone">The Test Ice Cream Cone</h3>
<p><a target="_blank" href="https://yellow.systems/blog/choosing-the-right-automation-testing-strategy-dos-and-don-ts"><img src="https://images.ctfassets.net/0nm5vlv2ad7a/4S6PqgeIdYeIwvJBavNxIp/e621b702f425fd531e4e64d788d5a28a/choosing-the-right-automation-testing-strategy-dos-and-don-ts.png" alt="https://yellow.systems/blog/choosing-the-right-automation-testing-strategy-dos-and-don-ts" class="image--center mx-auto" /></a></p>
<p>The Test Ice Cream Cone is a testing strategy that is often used by companies that are new to testing. The problem with this approach is that we have a lot of manual tests that are expensive to maintain and slow to execute.</p>
<p>The Test Ice Cream Cone is an anti-pattern and should be avoided at all costs.</p>
<h3 id="heading-the-test-pyramid">The Test Pyramid</h3>
<p><a target="_blank" href="https://semaphoreci.com/blog/testing-pyramid"><img src="https://wpblog.semaphoreci.com/wp-content/uploads/2022/03/pyramid1.jpg" alt class="image--center mx-auto" /></a></p>
<p>The Test Pyramid is a testing strategy that was introduced by <strong>Mike Cohn</strong> in his book "Succeeding with Agile". The pyramid shows three levels of tests: small, medium, and large.</p>
<p><strong><mark>Unit tests</mark></strong> are the primary level of the pyramid. They are small, fast, and cheap to maintain. They are focused on testing the logic in the code.</p>
<p><strong><mark>Service tests</mark></strong> are the second level of the pyramid. They are medium-sized, slower, and more expensive to maintain. They are not as productive as unit tests.</p>
<p>And the third level of the pyramid is <strong><mark>UI tests</mark></strong>. They are large, slow, and expensive to maintain as they are very fragile since every change in the UI can break the tests.</p>
<p>One of the <mark>problems</mark> with the Test Pyramid is that it's not clear what service tests are which leads to the situation that developers jump directly to UI tests.</p>
<h3 id="heading-the-practical-test-pyramid">The Practical Test Pyramid</h3>
<p><img src="https://miro.medium.com/v2/resize:fit:1400/1*8NtJX228Arq5fjB4LCv8jw.png" alt="https://medium.com/tide-engineering-team/the-practical-test-pyramid-c4fcdbc8b497" class="image--center mx-auto" /></p>
<p>The Practical Test Pyramid is an improved version of the Test Pyramid. It was introduced by <strong>Alister Scott</strong> and emphasizes more on medium-level tests and manual exploratory testing.</p>
<p>The <mark>Services Tests</mark> from the Test Pyramid are replaced by <strong>Component Tests</strong>, <strong>Integration Tests</strong>, and <strong>Contract Tests</strong>.</p>
<p>The next improvement of the Test Pyramid is that is now more clear that manual testing is also part of the testing strategy. As we can never be 100% sure that our tests are covering all edge cases, we need to add <mark>Manual Exploratory Testing</mark> on top of our automated tests.</p>
<p>If you find a bug with manual testing, you should write a new test to cover this case.</p>
<h2 id="heading-add-testcontainers-to-our-project">Add Testcontainers to our Project</h2>
<p>I will not go into the details of the existing code for setting up the project. If you want to know more about this, feel free to browse the code on GitHub.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/rust-testcontainers">https://github.com/dirien/quick-bites/tree/main/rust-testcontainers</a></div>
<p> </p>
<p>To add Testcontainers to our project, we need to add the following dependency to our project:</p>
<pre><code class="lang-bash">cargo add testcontainers
</code></pre>
<p>Now open the <code>main.rs</code> file and add following code at the end of the file:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[cfg(test)]</span>
<span class="hljs-keyword">mod</span> tests {
    <span class="hljs-keyword">use</span> std::env;
    <span class="hljs-keyword">use</span> std::io::Read;
    <span class="hljs-keyword">use</span> std::thread::sleep;
    <span class="hljs-keyword">use</span> super::*;
    <span class="hljs-keyword">use</span> actix_web::http::StatusCode;
    <span class="hljs-keyword">use</span> actix_web::test;
    <span class="hljs-keyword">use</span> actix_web::test::TestRequest;
    <span class="hljs-keyword">use</span> testcontainers::{clients, Image};
    <span class="hljs-keyword">use</span> testcontainers::core::{ExecCommand, WaitFor};
    <span class="hljs-keyword">use</span> testcontainers::images::generic::GenericImage;
    <span class="hljs-keyword">use</span> crate::model::CarType;
    <span class="hljs-keyword">use</span> crate::model::Car;

    <span class="hljs-meta">#[actix_web::test]</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">test_index</span></span>() {
        <span class="hljs-keyword">let</span> app = test::init_service(App::new().service(index)).<span class="hljs-keyword">await</span>;
        <span class="hljs-keyword">let</span> req = TestRequest::default().to_request();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, req).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::OK, resp.status());
    }

    <span class="hljs-meta">#[actix_web::test]</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">test_healthcheck</span></span>() {
        <span class="hljs-keyword">let</span> app = test::init_service(App::new().service(healthcheck)).<span class="hljs-keyword">await</span>;
        <span class="hljs-keyword">let</span> req = TestRequest::default().uri(<span class="hljs-string">"/health"</span>).to_request();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, req).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::OK, resp.status());
    }

    <span class="hljs-meta">#[actix_web::test]</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">test_get_cars</span></span>() {
        <span class="hljs-keyword">let</span> docker = clients::Cli::default();
        <span class="hljs-keyword">let</span> msg = WaitFor::message_on_stdout(<span class="hljs-string">"server is ready"</span>);
        <span class="hljs-keyword">let</span> generic = GenericImage::new(<span class="hljs-string">"mongo"</span>, <span class="hljs-string">"6.0.7"</span>).with_wait_for(msg.clone())
            .with_env_var(<span class="hljs-string">"MONGO_INITDB_DATABASE"</span>, <span class="hljs-string">"cars_info"</span>)
            .with_env_var(<span class="hljs-string">"MONGO_INITDB_ROOT_USERNAME"</span>, <span class="hljs-string">"root"</span>)
            .with_env_var(<span class="hljs-string">"MONGO_INITDB_ROOT_PASSWORD"</span>, <span class="hljs-string">"root"</span>)
            .with_exposed_port(<span class="hljs-number">27017</span>);

        <span class="hljs-keyword">let</span> node = docker.run(generic);
        <span class="hljs-keyword">let</span> port = node.get_host_port_ipv4(<span class="hljs-number">27017</span>);
        <span class="hljs-built_in">println!</span>(<span class="hljs-string">"Port: {}"</span>, port);

        <span class="hljs-keyword">let</span> data = setup(Config::new_mongodb_uri(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"mongodb://root:root@localhost:{}"</span>, port))).<span class="hljs-keyword">await</span>;
        <span class="hljs-keyword">let</span> app = test::init_service(App::new().app_data(data.clone()).service(get_cars).service(create_car)).<span class="hljs-keyword">await</span>;
        <span class="hljs-keyword">let</span> req = TestRequest::default().uri(<span class="hljs-string">"/cars"</span>).to_request();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, req).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::OK, resp.status());
        <span class="hljs-keyword">let</span> result: <span class="hljs-built_in">Vec</span>&lt;Car&gt; = test::read_body_json(resp).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(result.len(), <span class="hljs-number">0</span>);

        <span class="hljs-keyword">let</span> post = create_one_test_car();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, post.to_request()).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::OK, resp.status());
        <span class="hljs-keyword">let</span> result: Car = test::read_body_json(resp).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(result.name, <span class="hljs-string">"Test"</span>);

        <span class="hljs-keyword">let</span> req = TestRequest::default().uri(<span class="hljs-string">"/cars"</span>).to_request();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, req).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::OK, resp.status());
        <span class="hljs-keyword">let</span> result: <span class="hljs-built_in">Vec</span>&lt;Car&gt; = test::read_body_json(resp).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(result.len(), <span class="hljs-number">1</span>);
        <span class="hljs-built_in">assert_eq!</span>(result[<span class="hljs-number">0</span>].name, <span class="hljs-string">"Test"</span>);
    }

    <span class="hljs-meta">#[actix_web::test]</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">test_get_car</span></span>() {
        <span class="hljs-keyword">let</span> docker = clients::Cli::default();
        <span class="hljs-keyword">let</span> msg = WaitFor::message_on_stdout(<span class="hljs-string">"server is ready"</span>);
        <span class="hljs-keyword">let</span> generic = GenericImage::new(<span class="hljs-string">"mongo"</span>, <span class="hljs-string">"6.0.7"</span>).with_wait_for(msg.clone())
            .with_env_var(<span class="hljs-string">"MONGO_INITDB_DATABASE"</span>, <span class="hljs-string">"cars_info"</span>)
            .with_env_var(<span class="hljs-string">"MONGO_INITDB_ROOT_USERNAME"</span>, <span class="hljs-string">"root"</span>)
            .with_env_var(<span class="hljs-string">"MONGO_INITDB_ROOT_PASSWORD"</span>, <span class="hljs-string">"root"</span>);

        <span class="hljs-keyword">let</span> node = docker.run(generic);
        <span class="hljs-keyword">let</span> port = node.get_host_port_ipv4(<span class="hljs-number">27017</span>);

        <span class="hljs-keyword">let</span> data = setup(Config::new_mongodb_uri(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"mongodb://root:root@localhost:{}"</span>, port))).<span class="hljs-keyword">await</span>;
        <span class="hljs-keyword">let</span> app = test::init_service(App::new().app_data(data.clone()).service(get_cars).service(create_car).service(get_car)).<span class="hljs-keyword">await</span>;


        <span class="hljs-keyword">let</span> create_car_req = create_one_test_car();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, create_car_req.to_request()).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::OK, resp.status());
        <span class="hljs-keyword">let</span> new_car: CarDto = test::read_body_json(resp).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(new_car.name, <span class="hljs-string">"Test"</span>);

        <span class="hljs-keyword">let</span> get_car_req = TestRequest::get().uri(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"/cars/{}"</span>, new_car.id.unwrap()).as_str()).to_request();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, get_car_req).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::OK, resp.status());
        <span class="hljs-keyword">let</span> result: CarDto = test::read_body_json(resp).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(result.name, new_car.name);
    }

    <span class="hljs-meta">#[actix_web::test]</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">test_delete_car</span></span>() {
        <span class="hljs-keyword">let</span> docker = clients::Cli::default();
        <span class="hljs-keyword">let</span> msg = WaitFor::message_on_stdout(<span class="hljs-string">"server is ready"</span>);
        <span class="hljs-keyword">let</span> generic = GenericImage::new(<span class="hljs-string">"mongo"</span>, <span class="hljs-string">"6.0.7"</span>).with_wait_for(msg.clone())
            .with_env_var(<span class="hljs-string">"MONGO_INITDB_DATABASE"</span>, <span class="hljs-string">"cars_info"</span>)
            .with_env_var(<span class="hljs-string">"MONGO_INITDB_ROOT_USERNAME"</span>, <span class="hljs-string">"root"</span>)
            .with_env_var(<span class="hljs-string">"MONGO_INITDB_ROOT_PASSWORD"</span>, <span class="hljs-string">"root"</span>);

        <span class="hljs-keyword">let</span> node = docker.run(generic);
        <span class="hljs-keyword">let</span> port = node.get_host_port_ipv4(<span class="hljs-number">27017</span>);

        <span class="hljs-keyword">let</span> data = setup(Config::new_mongodb_uri(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"mongodb://root:root@localhost:{}"</span>, port))).<span class="hljs-keyword">await</span>;
        <span class="hljs-keyword">let</span> app = test::init_service(App::new().app_data(data.clone())
            .service(get_cars).service(create_car).service(get_car)
            .service(delete_car)).<span class="hljs-keyword">await</span>;

        <span class="hljs-keyword">let</span> create_car_req = create_one_test_car();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, create_car_req.to_request()).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::OK, resp.status());
        <span class="hljs-keyword">let</span> new_car: CarDto = test::read_body_json(resp).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(new_car.name, <span class="hljs-string">"Test"</span>);

        <span class="hljs-keyword">let</span> new_car_id = new_car.id.unwrap();
        <span class="hljs-keyword">let</span> get_car_req = TestRequest::get().uri(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"/cars/{}"</span>, new_car_id).as_str()).to_request();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, get_car_req).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::OK, resp.status());
        <span class="hljs-keyword">let</span> result: CarDto = test::read_body_json(resp).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(result.name, new_car.name);

        <span class="hljs-keyword">let</span> delete_car_req = TestRequest::delete().uri(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"/cars/{}"</span>, new_car_id).as_str()).to_request();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, delete_car_req).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::OK, resp.status());

        <span class="hljs-keyword">let</span> get_car_req = TestRequest::get().uri(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"/cars/{}"</span>, new_car_id).as_str()).to_request();
        <span class="hljs-keyword">let</span> resp = test::call_service(&amp;app, get_car_req).<span class="hljs-keyword">await</span>;
        <span class="hljs-built_in">assert_eq!</span>(StatusCode::NOT_FOUND, resp.status());
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">create_one_test_car</span></span>() -&gt; TestRequest {
        <span class="hljs-keyword">let</span> post = TestRequest::post().uri(<span class="hljs-string">"/cars"</span>).set_json(&amp;dto::CarDto {
            id: <span class="hljs-literal">None</span>,
            name: <span class="hljs-string">"Test"</span>.to_string(),
            brand: <span class="hljs-string">"Test"</span>.to_string(),
            year: <span class="hljs-number">2021</span>,
            r#<span class="hljs-class"><span class="hljs-keyword">type</span>: <span class="hljs-title">CarType</span></span>::Other,
        });
        post
    }
}
</code></pre>
<p>Let's describe what is happening here:</p>
<ul>
<li><p>The module defines a set of tests and imports the required dependencies.</p>
</li>
<li><p>Each test is marked with the <code>#[actix_web::test]</code> attribute for recognition by the testing framework.</p>
</li>
<li><p>The initial two tests (<code>test_index</code> and <code>test_healthcheck</code>) validate the response of <code>index</code> and <code>healthcheck</code> endpoints, respectively. These tests simply call the service and assert that the response is as expected.</p>
</li>
<li><p>The <code>test_get_cars</code> function introduces the use of Testcontainers. It creates an instance of a Testcontainers client, which allows the use of Docker containers during tests. This function specifically employs MongoDB as the test container:</p>
<ul>
<li><p>A new container is instantiated from the "<code>mongo</code>" image (version "<code>6.0.7</code>").</p>
</li>
<li><p>The container is prepared with specific environment variables for <strong>MongoDB's</strong> initial database setup.</p>
</li>
<li><p>An exposed port, <code>27017</code>, is specified for network communications.</p>
</li>
<li><p>A MongoDB URI is assembled, pointing towards the Docker-hosted MongoDB instance.</p>
</li>
<li><p>After the setup, test requests are sent and their responses are validated.</p>
</li>
</ul>
</li>
<li><p>The <code>test_get_car</code> and <code>test_delete_car</code> functions employ a similar testing approach as <code>test_get_cars</code>. These functions create a MongoDB container using the Testcontainers client and then execute tests that are supposed to interact with the MongoDB database.</p>
</li>
</ul>
<h2 id="heading-testing">Testing</h2>
<p>To run the tests, execute the following command:</p>
<pre><code class="lang-bash">cargo <span class="hljs-built_in">test</span>
</code></pre>
<p>Because the tests are using Docker containers, the first run will take a while to download the required images. And, of course, you need Docker installed on your machine.</p>
<p>If everything goes well, you should see the following output:</p>
<pre><code class="lang-bash">running 5 tests
<span class="hljs-built_in">test</span> tests::test_healthcheck ... ok
<span class="hljs-built_in">test</span> tests::test_index ... ok
<span class="hljs-built_in">test</span> tests::test_get_car ... ok
<span class="hljs-built_in">test</span> tests::test_delete_car ... ok
<span class="hljs-built_in">test</span> tests::test_get_cars ... ok

<span class="hljs-built_in">test</span> result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished <span class="hljs-keyword">in</span> 6.44s
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Testcontainers undoubtedly offer a powerful framework to improve the fidelity of your application testing through the use of Docker containers. However, they are not without limitations. Here are some notable considerations based on my personal experience:</p>
<ul>
<li><p>The Rust iteration of Testcontainers is not on par with its Java or Go counterparts, resulting in the absence of certain features.</p>
</li>
<li><p>As of July 5th, 2023, no Rust Testcontainer Modules exist, which means that you must use the GenericImage and configure the container yourself.</p>
</li>
<li><p>Testcontainers introduce another dependency to manage in your project.</p>
</li>
<li><p>Containers can be resource-intensive, requiring careful management to avoid overconsumption.</p>
</li>
<li><p>Your CI/CD pipeline must not only support containers but also possess the capacity to run them efficiently.</p>
</li>
<li><p>These requirements are equally valid if you're running tests locally on your own machine.</p>
</li>
</ul>
<p>In conclusion, the decision to adopt Testcontainers hinges on individual project needs. I found the tool valuable and plan to use it in my non-Rust projects.</p>
<h2 id="heading-resources">Resources</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://medium.com/tide-engineering-team/the-practical-test-pyramid-c4fcdbc8b497">https://medium.com/tide-engineering-team/the-practical-test-pyramid-c4fcdbc8b497</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://testcontainers.com/">https://testcontainers.com/</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/rust-testcontainers">https://github.com/dirien/quick-bites/tree/main/rust-testcontainers</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://yellow.systems/blog/choosing-the-right-automation-testing-strategy-dos-and-don-ts">https://yellow.systems/blog/choosing-the-right-automation-testing-strategy-dos-and-don-ts</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://semaphoreci.com/blog/testing-pyramid">https://semaphoreci.com/blog/testing-pyramid</a></div>
]]></content:encoded></item><item><title><![CDATA[How to build an SSH client using Rust 🦀]]></title><description><![CDATA[TL;DR: The Code
As usual, here the link to the code:
https://github.com/dirien/quick-bites/tree/main/rust-ssh
 
Introduction
In this tutorial, we will build a simple SSH client using Rust 🦀. Having a way to connect to a remote server is often a requ...]]></description><link>https://blog.ediri.io/how-to-build-an-ssh-client-using-rust</link><guid isPermaLink="true">https://blog.ediri.io/how-to-build-an-ssh-client-using-rust</guid><category><![CDATA[Rust]]></category><category><![CDATA[Developer]]></category><category><![CDATA[developers]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sat, 17 Jun 2023 20:38:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1687034291777/99b77702-cbd1-409a-b33c-2d77b8bbb191.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-the-code">TL;DR: The Code</h2>
<p>As usual, here the link to the code:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/rust-ssh">https://github.com/dirien/quick-bites/tree/main/rust-ssh</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>In this tutorial, we will build a simple SSH client using <code>Rust 🦀</code>. Having a way to connect to a remote server is often a requirement for DevOps tools.</p>
<p>If you already know Go, you might have used the <a target="_blank" href="https://godoc.org/golang.org/x/crypto/ssh">ssh</a> package. Here is a refresh of how to use it, with the password authentication method:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"log"</span>
    <span class="hljs-string">"os"</span>

    <span class="hljs-string">"golang.org/x/crypto/ssh"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// SSH connection parameters</span>
    host := <span class="hljs-string">"192.168.64.5"</span>
    port := <span class="hljs-number">22</span>
    user := <span class="hljs-string">"steve"</span>
    password := <span class="hljs-string">"password"</span>

    <span class="hljs-comment">// Create SSH client configuration</span>
    config := &amp;ssh.ClientConfig{
        User: user,
        Auth: []ssh.AuthMethod{
            ssh.Password(password),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    <span class="hljs-comment">// Connect to the remote server</span>
    conn, err := ssh.Dial(<span class="hljs-string">"tcp"</span>, fmt.Sprintf(<span class="hljs-string">"%s:%d"</span>, host, port), config)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatalf(<span class="hljs-string">"Failed to connect to SSH server: %v"</span>, err)
    }
    <span class="hljs-keyword">defer</span> conn.Close()

    <span class="hljs-comment">// Create a new SSH session</span>
    session, err := conn.NewSession()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatalf(<span class="hljs-string">"Failed to create SSH session: %v"</span>, err)
    }
    <span class="hljs-keyword">defer</span> session.Close()

    <span class="hljs-comment">// Set the output writer for session's output</span>
    session.Stdout = os.Stdout

    <span class="hljs-comment">// Run a command on the remote server</span>
    err = session.Run(<span class="hljs-string">"ls -l"</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatalf(<span class="hljs-string">"Failed to run command on remote server: %v"</span>, err)
    }
}
</code></pre>
<p>Easy, right? Now let's see how to do the same thing in Rust.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow this blog post, you should have a basic understanding of <code>Rust 🦀</code> and the Cargo build tool. If you are new to <code>Rust 🦀</code> check out my blog post "Learn Rust in under 10 mins":</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.ediri.io/learn-rust-in-under-10-mins">https://blog.ediri.io/learn-rust-in-under-10-mins</a></div>
<p> </p>
<p>Before we start, we need to make sure we have the following tools installed:</p>
<ul>
<li><p><a target="_blank" href="https://www.rust-lang.org">Rust</a></p>
</li>
<li><p>An IDE or text editor of your choice</p>
</li>
</ul>
<h2 id="heading-initialize-the-demo-project">Initialize the demo project</h2>
<pre><code class="lang-bash">cargo init
</code></pre>
<p>This will create a new <code>Rust 🦀</code> project in the current directory. Now, we need to add the dependencies we need for our demo application.</p>
<p>Now add can add the <code>ssh2</code> crate to the <code>Cargo.toml</code> file using the following command:</p>
<pre><code class="lang-bash">cargo add ssh2 --features vendored-openssl
</code></pre>
<h2 id="heading-create-the-ssh-client">Create the SSH client</h2>
<p>With the dependencies in place, we can now head over to the <code>src/main.rs</code> file and start writing our SSH client.</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> std::io::Read;
<span class="hljs-keyword">use</span> std::net::TcpStream;
<span class="hljs-keyword">use</span> ssh2::Session;

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-keyword">let</span> stream = TcpStream::connect(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"{}:22"</span>, <span class="hljs-string">"192.168.64.5"</span>));
    <span class="hljs-keyword">match</span> stream {
        <span class="hljs-literal">Ok</span>(stream) =&gt; {
            <span class="hljs-built_in">println!</span>(<span class="hljs-string">"Connected to the server!"</span>);
            <span class="hljs-keyword">let</span> session = Session::new();
            <span class="hljs-keyword">match</span> session {
                <span class="hljs-literal">Ok</span>(<span class="hljs-keyword">mut</span> session) =&gt; {
                    session.set_tcp_stream(stream);
                    session.handshake().unwrap();
                    <span class="hljs-keyword">let</span> auth = session.userauth_password(<span class="hljs-string">"steve"</span>, <span class="hljs-string">"password"</span>);
                    <span class="hljs-keyword">match</span> auth {
                        <span class="hljs-literal">Ok</span>(_) =&gt; {
                            <span class="hljs-built_in">println!</span>(<span class="hljs-string">"Authenticated!"</span>);
                            <span class="hljs-keyword">let</span> channel = session.channel_session();
                            <span class="hljs-keyword">match</span> channel {
                                <span class="hljs-literal">Ok</span>(<span class="hljs-keyword">mut</span> channel) =&gt; {
                                    channel.exec(<span class="hljs-string">"whoami"</span>).unwrap();
                                    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> s = <span class="hljs-built_in">String</span>::new();
                                    channel.read_to_string(&amp;<span class="hljs-keyword">mut</span> s).unwrap();
                                    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"{}"</span>, s);
                                    channel.wait_close().unwrap();
                                    <span class="hljs-keyword">let</span> exit_status = channel.exit_status().unwrap();
                                    <span class="hljs-keyword">if</span> exit_status != <span class="hljs-number">0</span> {
                                        eprint!(<span class="hljs-string">"Exited with status {}"</span>, exit_status);
                                    }
                                }
                                <span class="hljs-literal">Err</span>(e) =&gt; {
                                    eprint!(<span class="hljs-string">"Failed to create channel: {}"</span>, e);
                                }
                            }
                        }
                        <span class="hljs-literal">Err</span>(e) =&gt; {
                            eprint!(<span class="hljs-string">"Failed to authenticate: {:?}"</span>, e);
                        }
                    }
                }
                <span class="hljs-literal">Err</span>(e) =&gt; {
                    eprint!(<span class="hljs-string">"Failed to create session: {}"</span>, e);
                }
            }
        }
        <span class="hljs-literal">Err</span>(e) =&gt; {
            eprint!(<span class="hljs-string">"Failed to connect: {}"</span>, e);
        }
    }
}
</code></pre>
<p>As you can see from the code, we try(!) to not use the <code>unwrap()</code> method as much as possible. Instead, we handle the errors explicitly. This is a good practice to follow in your code, to avoid <code>unwrap()</code> calls all over the place, as it can mask errors and make your code less readable.</p>
<p>But let's go through the code step by step:</p>
<ul>
<li><p>Initially, a new TCP stream is established to the remote server via the <code>TcpStream::connect()</code> method. Upon successful connection, a new <code>Session</code> object is created.</p>
</li>
<li><p>Subsequently, this TCP stream is set on the Session object utilizing <code>Session::set_tcp_stream()</code>.</p>
</li>
<li><p>We then carry out the SSH handshake through <code>Session::handshake()</code>.</p>
</li>
<li><p>After the handshake, an attempt at authentication is made using <code>Session::userauth_password()</code>. Alternatively, authentication could be done using a private key with <code>Session::userauth_pubkey_file()</code>.</p>
</li>
<li><p>Finally, a new channel is formed with <code>Session::channel_session()</code>, followed by the execution of the <code>whoami</code> command on the remote server using <code>Channel::exec()</code>. The output of this command is read into a string via <code>Channel:: read_to_string()</code> and subsequently printed to the console.</p>
</li>
<li><p>The channel is then closed with <code>Channel::wait_close()</code> and the exit status of the command is printed to the console using <code>Channel::exit_status()</code>.</p>
</li>
<li><p>If the exit status is not <code>0</code>, an error message is printed to the console.</p>
</li>
</ul>
<h2 id="heading-run-the-demo">Run the demo</h2>
<p>Now that we have our SSH client ready, we can run it using the following command:</p>
<pre><code class="lang-bash">cargo run
</code></pre>
<p>If everything went well, you should see the following output:</p>
<pre><code class="lang-bash">Connected to the server!
Authenticated!
steve
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, we have seen how quickly we can create an SSH client in <code>Rust 🦀</code> using the <code>ssh2</code> crate. Now we can start building our own SSH client applications in <code>Rust 🦀</code>!</p>
]]></content:encoded></item><item><title><![CDATA[How To Upgrade Your AKS Cluster Using Only Pulumi]]></title><description><![CDATA[TL;DR: The code
https://github.com/dirien/quick-bites/tree/main/pulumi-aks-upgrade
 
Introduction
Recently, I got a request from one of our customers asking how to upgrade their AKS cluster using only Pulumi and not the Azure CLI. The customer also m...]]></description><link>https://blog.ediri.io/how-to-upgrade-your-aks-cluster-using-only-pulumi</link><guid isPermaLink="true">https://blog.ediri.io/how-to-upgrade-your-aks-cluster-using-only-pulumi</guid><category><![CDATA[Pulumi]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Azure]]></category><category><![CDATA[aks]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Thu, 15 Jun 2023 12:31:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1686832219586/8282555f-35c4-4a84-bddc-6db8bb23d358.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-the-code">TL;DR: The code</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/pulumi-aks-upgrade">https://github.com/dirien/quick-bites/tree/main/pulumi-aks-upgrade</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>Recently, I got a request from one of our customers asking how to upgrade their AKS cluster using only <a target="_blank" href="https://www.pulumi.com">Pulumi</a> and not the Azure CLI. The customer also mentioned that they did not find a solution to this problem. So I took up this challenge, hoping that I can find a solution to this problem.</p>
<p>First I asked in my tech bubble for help, and as always <a target="_blank" href="https://twitter.com/Pixel_Robots">Richard</a> jumped in to give input and a way to achieve this in Bicep.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/Pixel_Robots/status/1668550263269892099?s=20">https://twitter.com/Pixel_Robots/status/1668550263269892099?s=20</a></div>
<p> </p>
<p>So, I knew it must be possible! Also with Pulumi! BTW: the link to Richards's repo for the Bicep way is listed under <code>Additional Resources</code></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we start, you need to have the following prerequisites in place:</p>
<ul>
<li><p><a target="_blank" href="https://www.pulumi.com/docs/get-started/install/">Pulumi</a></p>
</li>
<li><p>Optional: Pulumi Account (for state storage), see <a target="_blank" href="https://www.pulumi.com/docs/pulumi-cloud/accounts/">Pulumi Cloud Account</a></p>
</li>
<li><p><a target="_blank" href="https://nodejs.org/en/download/">Node.js</a></p>
</li>
<li><p><a target="_blank" href="https://docs.microsoft.com/en-us/cli/azure/install-azure-cli">Azure CLI</a></p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">kubectl</a></p>
</li>
</ul>
<p>In the folder <code>infrastructure</code> you will find the Pulumi code to create the AKS cluster. This will be the starting point for this blog post.</p>
<h3 id="heading-create-the-aks-cluster">Create the AKS Cluster</h3>
<blockquote>
<p><strong>Note:</strong> You need to be logged in to Azure. You can see all the different ways to log into Azure in the Pulumi provider documentation for <a target="_blank" href="https://www.pulumi.com/registry/packages/azure-native-v2/installation-configuration/">Azure Native</a>.</p>
</blockquote>
<p>To create the AKS cluster, you need to run the following commands:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> infrastructure
pulumi up
</code></pre>
<p>The code is written in TypeScript, because why not! It may take a while to create the AKS cluster. So go and grab yourself a coffee.</p>
<h2 id="heading-upgrade-your-aks-cluster-the-azure-cli-way">Upgrade Your AKS Cluster, the Azure CLI Way</h2>
<p>Before I start with the Pulumi solution, I want to show you how to upgrade your AKS cluster using the Azure CLI. No, I am not showing you how to upgrade your AKS cluster using the Azure Portal. I am sure you will figure that out yourself, I am not going to support ClickOps here!</p>
<p>Before we start our upgrade, let's check the available Kubernetes versions for our AKS cluster:</p>
<pre><code class="lang-bash">az aks get-upgrades --name my-cluster --resource-group my-resource-group0012049e --output table
</code></pre>
<p>The output should look like this:</p>
<pre><code class="lang-text">Name     ResourceGroup              MasterVersion    Upgrades
-------  -------------------------  ---------------  -----------------------
default  my-resource-group0012049e  1.24.9           1.24.10, 1.25.5, 1.25.6
</code></pre>
<p>Nice, we pick the latest version, which is <code>1.25.6</code> and upgrade our AKS cluster:</p>
<pre><code class="lang-bash">az aks upgrade --name my-cluster --resource-group my-resource-group0012049e --kubernetes-version 1.25.6
</code></pre>
<p>With the flag <code>--control-plane-only</code> you can upgrade only the control plane and not the nodes.</p>
<blockquote>
<p><strong>Note:</strong> Keep in mind, that you can only upgrade one minor version at a time. So if you are on version <code>1.24.9</code> you can't upgrade to <code>1.26.x</code> directly. You need to upgrade to <code>1.25.x</code> first and then to <code>1.26.x</code>.</p>
</blockquote>
<p>You should see the following output:</p>
<pre><code class="lang-text">ubernetes may be unavailable during cluster upgrades.
 Are you sure you want to perform this operation? (y/N): y
Since control-plane-only argument is not specified, this will upgrade the control plane AND all nodepools to version 1.25.6. Continue? (y/N): y
{
...
}
</code></pre>
<p>And we check also via the <code>kubectl</code> command, if the upgrade was successful:</p>
<pre><code class="lang-bash">kubectl get nodes

aks-agentpool-32360557-vmss000000   Ready    agent   12m     v1.25.6
aks-agentpool-32360557-vmss000001   Ready    agent   11m     v1.25.6
aks-agentpool-32360557-vmss000002   Ready    agent   9m53s   v1.25.6
aks-workload1-32360557-vmss000000   Ready    agent   15m     v1.25.6
aks-workload1-32360557-vmss000001   Ready    agent   14m     v1.25.6
aks-workload1-32360557-vmss000002   Ready    agent   12m     v1.25.6
</code></pre>
<p>And the control plane:</p>
<pre><code class="lang-bash">kubectl version --short

...
Server Version: v1.25.6
</code></pre>
<p>Looks good, right? Before we move on to the Pulumi solution, let me give you some background information on the choreography Azure is doing in the background to ensure minimal disruption to your running workloads.</p>
<h2 id="heading-how-does-the-upgrade-work">How Does the Upgrade Work?</h2>
<p>As mentioned before, Azure is doing certain steps in the background to ensure minimal disruption to your running.</p>
<ul>
<li><p>Adds a new, so-called buffer node to the cluster with the specified Kubernetes version.</p>
</li>
<li><p>Cordons and drains one of the old nodes.</p>
</li>
<li><p>When the node is drained, it's re-imaged with the new Kubernetes version and becomes a new buffer node.</p>
</li>
<li><p>Rinse and repeat until all nodes are upgraded.</p>
</li>
<li><p>In the end, the buffer node is removed to make sure you have the same number of nodes as before.</p>
</li>
</ul>
<h2 id="heading-upgrade-your-aks-cluster-the-pulumi-way">Upgrade Your AKS Cluster, the Pulumi Way!</h2>
<p>With the knowledge we have now, we can start to write our Pulumi code to upgrade our AKS cluster. First of all, we reset our AKS cluster to the original state</p>
<blockquote>
<p><strong>Note:</strong> You can't downgrade your AKS cluster. So if you want to downgrade your AKS cluster, you need to delete it and create a new one.</p>
</blockquote>
<pre><code class="lang-bash">pulumi destroy
pulumi up
</code></pre>
<h3 id="heading-the-problem">The Problem</h3>
<p>If you look at the Pulumi code, you will see that there are two variables defined for the Kubernetes version.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> kubernetesVersion = <span class="hljs-string">"1.24.9"</span>
<span class="hljs-keyword">const</span> agentKubernetesVersion = <span class="hljs-string">"1.24.9"</span>
</code></pre>
<p>So naturally, you would think that you can just change the value to the new version and run <code>pulumi up</code> again. But if you do that, you will see the following error:</p>
<pre><code class="lang-bash"> error: Code=<span class="hljs-string">"NotAllAgentPoolOrchestratorVersionSpecifiedAndUnchanged"</span> Message=<span class="hljs-string">"Using managed cluster api, all Agent pools' OrchestratorVersion must be all specified or all unspecified. If all specified, they must be stay unchanged or the same with control plane. For agent pool specific change, please use per agent pool operations: https://aka.ms/agent-pool-rest-api"</span>
</code></pre>
<p>To safely upgrade your AKS cluster, we create a second Pulumi program, which we can execute when we want to upgrade our AKS cluster.</p>
<p>To do that, we create a new folder <code>upgrade-aks</code> and initialize a new Pulumi project:</p>
<pre><code class="lang-bash">mkdir upgrade-aks
<span class="hljs-built_in">cd</span> upgrade-aks
pulumi new azure-typescript -n upgrade-aks -d <span class="hljs-string">"upgrade aks cluster"</span> -s dev
</code></pre>
<p>And the magic sauce is the usage of the <code>pulumi-azapi</code> provider. In particular, the <code>UpdateResource</code> resource. This resource is used to add or modify properties on an existing resource. The good thing is, we can delete this Pulumi project afterward, because when <code>UpdateResource</code> is deleted, no operation will be performed in Azure.</p>
<p>To get information about the AKS cluster, we will also use the Pulumi concept of <code>StackReference</code>. This allows us to access the information from the other Pulumi project.</p>
<p>If you peek into the <code>index.ts</code> file, in the <code>infrastructure</code> folder, you will see that we are exporting the following values</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> resourceGroupName = resourceGroup.name;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> managedClusterName = managedCluster.name;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> managedClusterId = managedCluster.id;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> resourceGroupId = resourceGroup.id;
</code></pre>
<p>The code for the <code>upgrade-aks</code> project looks like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> pulumi <span class="hljs-keyword">from</span> <span class="hljs-string">"@pulumi/pulumi"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> cluster <span class="hljs-keyword">from</span> <span class="hljs-string">"@pulumi/azure-native/containerservice/v20230301"</span>;

<span class="hljs-keyword">const</span> stackReference = <span class="hljs-keyword">new</span> pulumi.StackReference(<span class="hljs-string">"&lt;org/projectname/stack&gt;"</span>);

<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> azapi <span class="hljs-keyword">from</span> <span class="hljs-string">"@ediri/azapi"</span>;

<span class="hljs-keyword">const</span> newKubernetesVersion = <span class="hljs-string">"1.24.10"</span>
<span class="hljs-keyword">const</span> newAgentKubernetesVersion = <span class="hljs-string">"1.24.10"</span>

<span class="hljs-keyword">const</span> clusterUpdate = <span class="hljs-keyword">new</span> azapi.UpdateResource(<span class="hljs-string">"cluster"</span>, {
    <span class="hljs-keyword">type</span>: <span class="hljs-string">"Microsoft.ContainerService/managedClusters@2023-03-01"</span>,
    parentId: stackReference.requireOutput(<span class="hljs-string">"resourceGroupId"</span>),
    name: stackReference.requireOutput(<span class="hljs-string">"managedClusterName"</span>),
    body: pulumi.jsonStringify({
        properties: {
            id: stackReference.requireOutput(<span class="hljs-string">"managedClusterId"</span>),
            kubernetesVersion: newKubernetesVersion
        }
    })
})

<span class="hljs-keyword">const</span> aks = cluster.getManagedClusterOutput({
    resourceGroupName: stackReference.requireOutput(<span class="hljs-string">"resourceGroupName"</span>),
    resourceName: stackReference.requireOutput(<span class="hljs-string">"managedClusterName"</span>),
})

aks.agentPoolProfiles?.apply(<span class="hljs-function"><span class="hljs-params">agentPoolProfiles</span> =&gt;</span> {
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> agentPoolProfile <span class="hljs-keyword">of</span> agentPoolProfiles ?? []) {
        <span class="hljs-keyword">const</span> agentPoolUpdate = <span class="hljs-keyword">new</span> azapi.UpdateResource(<span class="hljs-string">`agentpool-<span class="hljs-subst">${agentPoolProfile.name}</span>`</span>, {
            <span class="hljs-keyword">type</span>: <span class="hljs-string">"Microsoft.ContainerService/managedClusters/agentPools@2023-03-01"</span>,
            parentId: stackReference.requireOutput(<span class="hljs-string">"managedClusterId"</span>),
            name: agentPoolProfile.name,
            body: pulumi.jsonStringify({
                properties: {
                    id: stackReference.requireOutput(<span class="hljs-string">"managedClusterId"</span>),
                    orchestratorVersion: newAgentKubernetesVersion
                }
            })
        }, {
            dependsOn: clusterUpdate
        })
    }
})
</code></pre>
<p>And we add the <code>pulumi-azapi</code> provider to the <code>package.json</code></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"upgrade-aks"</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.ts"</span>,
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@types/node"</span>: <span class="hljs-string">"^16"</span>
  },
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"@pulumi/pulumi"</span>: <span class="hljs-string">"^3.0.0"</span>,
    <span class="hljs-attr">"@pulumi/azure-native"</span>: <span class="hljs-string">"2.0.0-beta.1"</span>,
    <span class="hljs-attr">"@ediri/azapi"</span>: <span class="hljs-string">"1.2.9"</span>
  }
}
</code></pre>
<p>Now we can change the values of the variables in the <code>index.ts</code> file and run <code>pulumi up</code>.</p>
<pre><code class="lang-bash">const newKubernetesVersion = <span class="hljs-string">"1.24.10"</span>
const newAgentKubernetesVersion = <span class="hljs-string">"1.24.10"</span>
</code></pre>
<blockquote>
<p><strong>Note:</strong> Keep in mind to have not a resource quota limit in your subscription. Otherwise, the update will fail, with an error message like this: <code>Operation could not be completed as it results in exceeding approved standardBSFamily Cores quota</code>. In this case, you need to request a quota increase.</p>
</blockquote>
<p>This will first update the AKS cluster and then update the agent pools. This can take a while, so be patient.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686831170139/5bdd09ff-e65c-452b-bdd4-95d482f3b10d.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686831186447/2e072a76-4bc3-4ecd-abf8-1f1f2add85b4.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-bash">Updating (dev)

View <span class="hljs-keyword">in</span> Browser (Ctrl+O): https://app.pulumi.com/dirien/agent-pool/dev/updates/14

     Type                           Name                 Status              
 +   pulumi:pulumi:Stack            agent-pool-dev       created (0.85s)     
 +   ├─ azapi:index:UpdateResource  cluster              created (134s)      
 +   ├─ azapi:index:UpdateResource  agentpool-workload2  created (197s)      
 +   ├─ azapi:index:UpdateResource  agentpool-workload1  created (177s)      
 +   └─ azapi:index:UpdateResource  agentpool-agentpool  created (474s)      


Resources:
    + 5 created

Duration: 10m16s
</code></pre>
<h2 id="heading-how-to-speed-up-the-update-process-with-the-help-of-surge">How To Speed Up the Update Process With the Help of <code>surge</code>?</h2>
<p>If you have a lot of agent pools, the update process can take a while. To speed up the process, you can tweak the max <code>surge</code> settings of your AKS cluster. Per default, the max <code>surge</code> setting is set to 1. This means that one extra node will be created and only one old node cordoned and drained.</p>
<p>So if you set the surge to <code>100%</code> will cause all old nodes to be cordoned off and drained at the same time. While this speeds up your update process, it can also cause some disruption to your workload. So be careful with this setting!</p>
<p>A rule of thumb is to set the surge for production clusters to <code>33%</code>.</p>
<h2 id="heading-wrap-up">Wrap Up</h2>
<p>In this blog post, I showed you an easy way to upgrade your AKS cluster with the help of Pulumi and the help of an extra update Pulumi project. This allows you to upgrade your AKS cluster in a controlled way without using the Azure Portal or the Azure CLI.</p>
<h2 id="heading-additional-resources">Additional Resources</h2>
<ul>
<li><p>The Bicep version from Richard -&gt; <a target="_blank" href="https://github.com/PixelRobots/AKS-Bicep">https://github.com/PixelRobots/AKS-Bicep</a></p>
</li>
<li><p>The Azure CLI way -&gt; <a target="_blank" href="https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-upgrade-cluster">https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-upgrade-cluster</a>?</p>
</li>
<li><p>Surge -&gt; <a target="_blank" href="https://learn.microsoft.com/en-us/azure/aks/upgrade-cluster?tabs=azure-cli#customize-node-surge-upgrade">https://learn.microsoft.com/en-us/azure/aks/upgrade-cluster?tabs=azure-cli#customize-node-surge-upgrade</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Advanced Secrets Management on Kubernetes with Pulumi: Doppler Secrets Operator]]></title><description><![CDATA[TL;DR The code
https://github.com/dirien/quick-bites/tree/main/pulumi-doppler
 
Introduction
This article is part three of my series on how to manage secrets on Kubernetes by using Pulumi. If you haven't read the previous articles, here are the links...]]></description><link>https://blog.ediri.io/advanced-secrets-management-on-kubernetes-with-pulumi-doppler-secrets-operator</link><guid isPermaLink="true">https://blog.ediri.io/advanced-secrets-management-on-kubernetes-with-pulumi-doppler-secrets-operator</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[secrets]]></category><category><![CDATA[doppler]]></category><category><![CDATA[Pulumi]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sun, 14 May 2023 18:29:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1684088890342/677624f1-77a6-4705-9d1e-2130939d2d87.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-the-code">TL;DR The code</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/pulumi-doppler">https://github.com/dirien/quick-bites/tree/main/pulumi-doppler</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>This article is part three of my series on how to manage secrets on Kubernetes by using <a target="_blank" href="https://www.pulumi.com/"><code>Pulumi</code></a>. If you haven't read the previous articles, here are the links to them:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.ediri.io/advanced-secret-management-on-kubernetes-with-pulumi-and-gitops-sealed-secrets-controller">https://blog.ediri.io/advanced-secret-management-on-kubernetes-with-pulumi-and-gitops-sealed-secrets-controller</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.ediri.io/advanced-secret-management-on-kubernetes-with-pulumi-secrets-store-csi-driver">https://blog.ediri.io/advanced-secret-management-on-kubernetes-with-pulumi-secrets-store-csi-driver</a></div>
<p> </p>
<p>In this article, we will be looking at how to use <a target="_blank" href="https://www.doppler.com/"><code>Doppler</code></a> to manage secrets on Kubernetes. But before we get into the details of how to set up the integration, let's take a look at what <code>Doppler</code> is what it can do for us.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.doppler.com/">https://www.doppler.com/</a></div>
<p> </p>
<h2 id="heading-what-is-doppler">What is Doppler?</h2>
<p><code>Doppler</code> defines itself as a "SecretOps Platform". Currently, it is a cloud-based service that allows teams to securely manage and distribute secrets across their applications and infrastructure.</p>
<p>The <code>Doppler</code> platform is built with collaboration in mind. It supports out-of-the-box concepts like users, groups, and roles. So far it's like any other tool that supports role-based access control (RBAC). But with <code>Doppler</code>, there is also the concept of <code>projects</code>. A project is a collection of secrets that are scoped to a specific application or service. This allows you also to have project-based access control. Next to <code>projects</code>, <code>Doppler</code> also supports <code>environments</code>, which allows you to have different values for the same secret in different environments. For example, you can have a <code>DB_URL</code> secret has a different value in your <code>dev</code> environment than in your <code>prod</code> environment.</p>
<p>What I like the most about <code>Doppler</code> is a very huge list of integrations that it supports. And of all the integrations, the Kubernetes integration is the one that I like the most. And that's what we will be looking at in this article.</p>
<p><img src="https://files.readme.io/4d55cb3-Kubernetes2x_3.png" alt="2110" class="image--center mx-auto" /></p>
<p>The <code>Doppler Secrets Operator</code> is an automated service that operates within a Kubernetes cluster, ensuring that secrets are continuously synchronized and deployments are updated accordingly.</p>
<p>Operating from within its unique namespace, <code>doppler-operator-system</code>, the Operator maintains strict access control through RBAC policies. It utilizes <code>DopplerSecret</code> custom resources, which specify the <code>Doppler</code> configuration to synchronize, the name of the Kubernetes secret under its management, and the namespace where it will be created.</p>
<p>The <code>Doppler Secrets Operator</code> is exclusively responsible for the constant synchronization of secret updates from <code>Doppler</code> to the Kubernetes secrets it manages. It can also optionally reload deployments that reference a managed secret, assuring your applications always have access to the most recent version of secrets.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this article, you will need the following:</p>
<ul>
<li><p><a target="_blank" href="https://www.pulumi.com/docs/get-started/install/">Pulumi CLI</a> installed.</p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">kubectl</a> installed.</p>
</li>
<li><p>optional <a target="_blank" href="https://k9scli.io/topics/install/">K9s</a>, if you want to quickly interact with your cluster.</p>
</li>
<li><p><a target="_blank" href="https://dashboard.doppler.com/register">Doppler Account</a>.</p>
</li>
<li><p><a target="_blank" href="https://cloud.digitalocean.com/registrations/new">DigitalOcean Account</a>.</p>
</li>
<li><p><a target="_blank" href="https://www.vcluster.com/docs/getting-started/setup/">vcluster cli</a> installed.</p>
</li>
</ul>
<h2 id="heading-the-demo-setup">The Demo Setup</h2>
<p>In this demo, we will add a little twist to the setup! We will deploy two <a target="_blank" href="https://www.digitalocean.com/">DigitalOcean</a> Kubernetes (<code>DOKS</code>) clusters, one for <code>development</code> and <code>staging</code> and one for <code>production</code>. The twist is, that the <code>development</code> and <code>staging</code> cluster will be deployed as <code>vcluster</code> on top of the <code>DOKS</code> cluster. The <code>production</code> cluster will be deployed as a standalone cluster.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684087130616/82fb22b3-396b-406e-b732-f53be39b338a.png" alt class="image--center mx-auto" /></p>
<p>You don't know, what <code>vcluster</code> is? Spend three minutes and watch this.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=gQ-KG57ruvY">https://www.youtube.com/watch?v=gQ-KG57ruvY</a></div>
<p> </p>
<h3 id="heading-create-a-new-pulumi-project">Create a New Pulumi Project</h3>
<p>Let's start by creating a new Pulumi project. We will call it <code>pulumi-doppler</code> and we will use <code>golang</code> as the language</p>
<pre><code class="lang-bash">mkdir pulumi-doppler
<span class="hljs-built_in">cd</span> pulumi-doppler
pulumi new digitalocean-go --force
</code></pre>
<p>You can leave the default values in the prompts.</p>
<pre><code class="lang-bash">This <span class="hljs-built_in">command</span> will walk you through creating a new Pulumi project.

Enter a value or leave blank to accept the (default), and press &lt;ENTER&gt;.
Press ^C at any time to quit.

project name: (pulumi-doppler) 
project description: (A minimal DigitalOcean Go Pulumi program) 
Created project <span class="hljs-string">'pulumi-doppler'</span>

Please enter your desired stack name.
To create a stack <span class="hljs-keyword">in</span> an organization, use the format &lt;org-name&gt;/&lt;stack-name&gt; (e.g. `acmecorp/dev`).
stack name: (dev)   
Created stack <span class="hljs-string">'dev'</span>

Installing dependencies...

go: downloading github.com/pulumi/pulumi-digitalocean/sdk/v4 v4.19.1
Finished installing dependencies

Your new project is ready to go! ✨

To perform an initial deployment, run `pulumi up`
</code></pre>
<p>We can then delete the contents of the <code>main.go</code> file and replace it with the following code:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"github.com/pulumi/pulumi-digitalocean/sdk/v4/go/digitalocean"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi/sdk/v3/go/pulumi"</span>
)

<span class="hljs-keyword">type</span> NodePoolArgs <span class="hljs-keyword">struct</span> {
    Name      <span class="hljs-keyword">string</span>
    Size      <span class="hljs-keyword">string</span>
    NodeCount <span class="hljs-keyword">int</span>
    Label     <span class="hljs-keyword">string</span>
}

<span class="hljs-keyword">type</span> ClusterArgs <span class="hljs-keyword">struct</span> {
    Name     <span class="hljs-keyword">string</span>
    Region   <span class="hljs-keyword">string</span>
    Version  <span class="hljs-keyword">string</span>
    NodePool *[]NodePoolArgs
}

<span class="hljs-keyword">const</span> (
    DefaultNodePoolName = <span class="hljs-string">"default"</span>
    DefaultNodePoolSize = <span class="hljs-string">"s-2vcpu-4gb"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">createDOKSCLuster</span><span class="hljs-params">(ctx *pulumi.Context, args *ClusterArgs)</span> <span class="hljs-params">(pulumi.StringOutput, error)</span></span> {
    <span class="hljs-comment">// Create a new DOKS cluster</span>
    cluster, err := digitalocean.NewKubernetesCluster(ctx, args.Name, &amp;digitalocean.KubernetesClusterArgs{
        Region:  pulumi.String(args.Region),
        Version: pulumi.String(args.Version),
        NodePool: &amp;digitalocean.KubernetesClusterNodePoolArgs{
            Name:      pulumi.String(DefaultNodePoolName),
            Size:      pulumi.String(DefaultNodePoolSize),
            NodeCount: pulumi.Int(<span class="hljs-number">1</span>),
            AutoScale: pulumi.Bool(<span class="hljs-literal">false</span>),
        },
    })

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> pulumi.StringOutput{}, err
    }
    <span class="hljs-keyword">if</span> args.NodePool != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">for</span> _, nodePool := <span class="hljs-keyword">range</span> *args.NodePool {
            _, err := digitalocean.NewKubernetesNodePool(ctx, nodePool.Name, &amp;digitalocean.KubernetesNodePoolArgs{
                ClusterId: cluster.ID(),
                Name:      pulumi.String(nodePool.Name),
                Size:      pulumi.String(nodePool.Size),
                NodeCount: pulumi.Int(nodePool.NodeCount),
                Labels:    pulumi.StringMap{<span class="hljs-string">"env"</span>: pulumi.String(nodePool.Label)},
            })
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">return</span> pulumi.StringOutput{}, err
            }
        }
    }

    output, _ := cluster.KubeConfigs.ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(kcs []digitalocean.KubernetesClusterKubeConfig)</span> <span class="hljs-params">(<span class="hljs-keyword">string</span>, error)</span></span> {
        <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(kcs) == <span class="hljs-number">0</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-string">""</span>, <span class="hljs-literal">nil</span>
        }
        <span class="hljs-keyword">return</span> *kcs[<span class="hljs-number">0</span>].RawConfig, <span class="hljs-literal">nil</span>
    }).(pulumi.StringOutput)

    <span class="hljs-keyword">return</span> output, <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// Create the DOKS cluster for development and staging</span>
        preProd, err := createDOKSCLuster(ctx, &amp;ClusterArgs{
            Name:    <span class="hljs-string">"pre-prod-cluster"</span>,
            Region:  <span class="hljs-string">"fra1"</span>,
            Version: <span class="hljs-string">"1.26.3-do.0"</span>,
            NodePool: &amp;[]NodePoolArgs{
                {
                    Name:      <span class="hljs-string">"development"</span>,
                    Size:      <span class="hljs-string">"s-2vcpu-4gb"</span>,
                    NodeCount: <span class="hljs-number">1</span>,
                    Label:     <span class="hljs-string">"development"</span>,
                },
                {
                    Name:      <span class="hljs-string">"staging"</span>,
                    Size:      <span class="hljs-string">"s-2vcpu-4gb"</span>,
                    NodeCount: <span class="hljs-number">1</span>,
                    Label:     <span class="hljs-string">"staging"</span>,
                },
            },
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        ctx.Export(<span class="hljs-string">"preProdKubeConfig"</span>, pulumi.ToSecret(preProd))

        <span class="hljs-comment">// Create the DOKS cluster for production</span>
        prod, err := createDOKSCLuster(ctx, &amp;ClusterArgs{
            Name:    <span class="hljs-string">"prod-cluster"</span>,
            Region:  <span class="hljs-string">"fra1"</span>,
            Version: <span class="hljs-string">"1.25.8-do.0"</span>,
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        ctx.Export(<span class="hljs-string">"prodKubeConfig"</span>, pulumi.ToSecret(prod))
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>Let's break down the code into smaller pieces to understand what's going on here:</p>
<ul>
<li><p>Two structures, <code>NodePoolArgs</code> and <code>ClusterArgs</code>, are declared. These structures are used to encapsulate information necessary for the creation of a <code>DOKS</code> cluster.</p>
<ul>
<li><p><code>NodePoolArgs</code> encapsulates details about a node pool, which includes its <code>Name</code>, <code>Size</code>, <code>NodeCount</code> (the number of nodes in the pool), and <code>Label</code> (a string to label the node pool).</p>
</li>
<li><p><code>ClusterArgs</code> encapsulates information about a <code>DOKS</code> cluster. This includes its <code>Name</code>, <code>Region</code>, <code>Version</code>, and <code>NodePool</code> configuration.</p>
</li>
</ul>
</li>
<li><p>The <code>createDOKSCLuster</code> function is declared. This function is responsible for creating a DOKS cluster using the provided arguments, which are encapsulated in the <code>ClusterArgs</code> structure.</p>
<ul>
<li><p>The function uses the <code>NewKubernetesCluster</code> method from the DigitalOcean Pulumi provider to create a new <code>DOKS</code> cluster.</p>
</li>
<li><p>If the cluster creation is successful and additional <code>NodePoolArgs</code> are provided, the function iterates over the slice of <code>NodePoolArgs</code> to create additional node pools using the <code>NewKubernetesNodePool</code> method.</p>
</li>
<li><p>If the creation of additional node pools is successful, the function retrieves the raw Kubernetes configuration of the newly created cluster. This configuration is essential for interacting with the cluster.</p>
</li>
</ul>
</li>
<li><p>The <code>main</code> function serves as the entry point of the program and two DOKS clusters are created – one for pre-production and another for production. Each cluster has its unique <code>Name</code>, <code>Region</code>, <code>Version</code>, and <code>NodePool</code> configuration.</p>
<ul>
<li><p>The pre-production cluster has a default node pool and two additional node pools labeled "development" and " staging". Each of these additional node pools has 1 node of size "s-2vcpu-4gb".</p>
</li>
<li><p>The production cluster is created with a default node pool.</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-deploy-the-doks-clusters">Deploy the DOKS Clusters</h3>
<p>With the infrastructure code in place, we can now deploy the DOKS clusters. Don't worry, we will add the <code>vcluster</code> and <code>Doppler</code> components later.</p>
<blockquote>
<p><strong>Note:</strong> Set the <code>DIGITALOCEAN_TOKEN</code> environment variable to your DigitalOcean API token before running the Pulumi command.</p>
</blockquote>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> DIGITALOCEAN_TOKEN=&lt;your-digitalocean-api-token&gt;
pulumi up
</code></pre>
<p>Deploying the DOKS clusters will take a few minutes. In the process, Pulumi will ask you to confirm the deployment and give you a summary of the resources that will be created.</p>
<blockquote>
<p><strong>Note:</strong> You can use the <code>--yes</code> and <code>--skip-preview</code> flags to skip the confirmation prompt and preview, respectively.</p>
</blockquote>
<pre><code class="lang-bash"> pulumi up
Previewing update (dev)

View <span class="hljs-keyword">in</span> Browser (Ctrl+O): https://app.pulumi.com/dirien/pulumi-doppler/dev/previews/758944fa-5643-410a-b11a-ee4a3a95a227

     Type                                     Name                Plan       
 +   pulumi:pulumi:Stack                      pulumi-doppler-dev  create     
 +   ├─ digitalocean:index:KubernetesCluster  prod-cluster        create     
 +   └─ digitalocean:index:KubernetesCluster  pre-prod-cluster    create     


Outputs:
    preProdKubeConfig: [secret]
    prodKubeConfig   : [secret]

Resources:
    + 3 to create

Do you want to perform this update? yes
Updating (dev)

View <span class="hljs-keyword">in</span> Browser (Ctrl+O): https://app.pulumi.com/dirien/pulumi-doppler/dev/updates/1

     Type                                     Name                Status             
 +   pulumi:pulumi:Stack                      pulumi-doppler-dev  created (415s)     
 +   ├─ digitalocean:index:KubernetesCluster  prod-cluster        created (413s)     
 +   └─ digitalocean:index:KubernetesCluster  pre-prod-cluster    created (302s)     


Outputs:
    preProdKubeConfig: [secret]
    prodKubeConfig   : [secret]

Resources:
    + 3 created

Duration: 6m58s
</code></pre>
<h3 id="heading-configure-kubectl-to-interact-with-the-doks-clusters">Configure <code>kubectl</code> to Interact with the DOKS Clusters</h3>
<p>If you have the <code>kubectl</code> CLI installed, you can use the <code>pulumi stack output</code> command to retrieve the raw Kubernetes configuration of the clusters.</p>
<p>This is helpful in case you want to debug the clusters or interact with them using the <code>kubectl</code> CLI.</p>
<pre><code class="lang-bash">pulumi stack output preProdKubeConfig --show-secrets &gt; pre-prod-cluster.yaml
<span class="hljs-built_in">export</span> KUBECONFIG=pre-prod-cluster.yaml
kubectl get nodes -o wide
</code></pre>
<p>You should see an output similar to the following:</p>
<pre><code class="lang-bash">pulumi stack output preProdKubeConfig --show-secrets &gt; pre-prod-cluster.yaml
<span class="hljs-built_in">export</span> KUBECONFIG=pre-prod-cluster.yaml
kubectl get nodes -o wide
NAME                STATUS   ROLES    AGE     VERSION   INTERNAL-IP   EXTERNAL-IP       OS-IMAGE                         KERNEL-VERSION          CONTAINER-RUNTIME
default-fx9bk       Ready    &lt;none&gt;   4m30s   v1.26.3   10.135.0.5    142.93.162.26     Debian GNU/Linux 11 (bullseye)   6.0.0-0.deb11.6-amd64   containerd://1.6.14
development-fx9gg   Ready    &lt;none&gt;   61s     v1.26.3   10.135.0.6    134.209.245.105   Debian GNU/Linux 11 (bullseye)   6.0.0-0.deb11.6-amd64   containerd://1.6.14
staging-fx9gw       Ready    &lt;none&gt;   26s     v1.26.3   10.135.0.7    207.154.215.11    Debian GNU/Linux 11 (bullseye)   6.0.0-0.deb11.6-amd64   containerd://1.6.14
</code></pre>
<h3 id="heading-add-the-vcluster-and-doppler-components">Add the <code>vcluster</code> and <code>Doppler</code> Components</h3>
<p>Now that we have the DOKS clusters up and running, we can add the <code>vcluster</code> and <code>Doppler</code> components to them.</p>
<p>First, we need to add the <code>pulumi-kubernetes</code> package to our project:</p>
<pre><code class="lang-bash">go get  github.com/pulumi/pulumi-kubernetes/sdk/go/kubernetes
</code></pre>
<p>Then head over to our <code>main.go</code> file and add the following code:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">deployProd</span><span class="hljs-params">(ctx *pulumi.Context, cluster pulumi.StringOutput)</span> <span class="hljs-title">error</span></span> {
    productionKubernetesProvider, err := kubernetes.NewProvider(ctx, <span class="hljs-string">"production-k8s"</span>, &amp;kubernetes.ProviderArgs{
        Kubeconfig:            cluster,
        EnableServerSideApply: pulumi.Bool(<span class="hljs-literal">true</span>),
    })
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    dopplerOperatorHelm, err := helm.NewRelease(ctx, <span class="hljs-string">"doppler-operator"</span>, &amp;helm.ReleaseArgs{
        Name:            pulumi.String(<span class="hljs-string">"doppler-operator"</span>),
        Chart:           pulumi.String(<span class="hljs-string">"doppler-kubernetes-operator"</span>),
        Namespace:       pulumi.String(<span class="hljs-string">"doppler-operator-system"</span>),
        CreateNamespace: pulumi.Bool(<span class="hljs-literal">false</span>),
        RepositoryOpts: &amp;helm.RepositoryOptsArgs{
            Repo: pulumi.String(<span class="hljs-string">"https://helm.doppler.com"</span>),
        },
        Version: pulumi.String(<span class="hljs-string">"1.2.5"</span>),
    }, pulumi.Provider(productionKubernetesProvider))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    secret, err := v1.NewSecret(ctx, <span class="hljs-string">"doppler-token-secret"</span>, &amp;v1.SecretArgs{
        Metadata: &amp;metav1.ObjectMetaArgs{
            Name:      pulumi.String(<span class="hljs-string">"doppler-token-secret"</span>),
            Namespace: pulumi.String(<span class="hljs-string">"doppler-operator-system"</span>),
        },
        Type: pulumi.String(<span class="hljs-string">"Opaque"</span>),
        Data: pulumi.StringMap{
            <span class="hljs-string">"serviceToken"</span>: config.GetSecret(ctx, <span class="hljs-string">"doks-production"</span>).ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(s <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(<span class="hljs-keyword">string</span>, error)</span></span> {
                <span class="hljs-keyword">return</span> base64.StdEncoding.EncodeToString([]<span class="hljs-keyword">byte</span>(s)), <span class="hljs-literal">nil</span>
            }).(pulumi.StringOutput),
        },
    }, pulumi.Provider(productionKubernetesProvider), pulumi.DependsOn([]pulumi.Resource{dopplerOperatorHelm}))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    _, err = apiextensions.NewCustomResource(ctx, <span class="hljs-string">"dopplersecrets"</span>, &amp;apiextensions.CustomResourceArgs{
        ApiVersion: pulumi.String(<span class="hljs-string">"secrets.doppler.com/v1alpha1"</span>),
        Kind:       pulumi.String(<span class="hljs-string">"DopplerSecret"</span>),
        Metadata: &amp;metav1.ObjectMetaArgs{
            Name:      pulumi.String(<span class="hljs-string">"doppler-token-secret"</span>),
            Namespace: secret.Metadata.Namespace().Elem(),
        },
        OtherFields: kubernetes.UntypedArgs{
            <span class="hljs-string">"spec"</span>: pulumi.Map{
                <span class="hljs-string">"tokenSecret"</span>: pulumi.Map{
                    <span class="hljs-string">"name"</span>:      secret.Metadata.Name().Elem(),
                    <span class="hljs-string">"namespace"</span>: secret.Metadata.Namespace().Elem(),
                },
                <span class="hljs-string">"managedSecret"</span>: pulumi.Map{
                    <span class="hljs-string">"name"</span>:      pulumi.String(<span class="hljs-string">"doppler-secret"</span>),
                    <span class="hljs-string">"namespace"</span>: pulumi.String(<span class="hljs-string">"default"</span>),
                },
            },
        },
    }, pulumi.Provider(productionKubernetesProvider), pulumi.DependsOn([]pulumi.Resource{dopplerOperatorHelm}))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    _, err = appsv1.NewDeployment(ctx, <span class="hljs-string">"hello-server"</span>, &amp;appsv1.DeploymentArgs{
        Metadata: &amp;metav1.ObjectMetaArgs{
            Name: pulumi.String(<span class="hljs-string">"hello-server"</span>),
            Annotations: pulumi.StringMap{
                <span class="hljs-string">"secrets.doppler.com/reload"</span>: pulumi.String(<span class="hljs-string">"true"</span>),
            },
        },
        Spec: &amp;appsv1.DeploymentSpecArgs{
            Replicas: pulumi.Int(<span class="hljs-number">1</span>),
            Selector: &amp;metav1.LabelSelectorArgs{
                MatchLabels: pulumi.StringMap{
                    <span class="hljs-string">"app"</span>: pulumi.String(<span class="hljs-string">"hello-server"</span>),
                },
            },
            Template: &amp;v1.PodTemplateSpecArgs{
                Metadata: &amp;metav1.ObjectMetaArgs{
                    Labels: pulumi.StringMap{
                        <span class="hljs-string">"app"</span>: pulumi.String(<span class="hljs-string">"hello-server"</span>),
                    },
                },
                Spec: &amp;v1.PodSpecArgs{
                    Containers: v1.ContainerArray{
                        v1.ContainerArgs{
                            Name:  pulumi.String(<span class="hljs-string">"hello-server"</span>),
                            Image: pulumi.String(<span class="hljs-string">"ghcr.io/dirien/hello-server/hello-server:latest"</span>),
                            Ports: v1.ContainerPortArray{
                                v1.ContainerPortArgs{
                                    ContainerPort: pulumi.Int(<span class="hljs-number">8080</span>),
                                },
                            },
                            EnvFrom: v1.EnvFromSourceArray{
                                v1.EnvFromSourceArgs{
                                    SecretRef: &amp;v1.SecretEnvSourceArgs{
                                        Name: pulumi.String(<span class="hljs-string">"doppler-secret"</span>),
                                    },
                                },
                            },
                        },
                    },
                },
            },
        },
    }, pulumi.Provider(productionKubernetesProvider))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">deployPreProd</span><span class="hljs-params">(ctx *pulumi.Context, cluster pulumi.StringOutput, stage <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
    preProdKubernetesProvider, err := kubernetes.NewProvider(ctx, fmt.Sprintf(<span class="hljs-string">"%s-k8s"</span>, stage), &amp;kubernetes.ProviderArgs{
        Kubeconfig:            cluster,
        EnableServerSideApply: pulumi.Bool(<span class="hljs-literal">true</span>),
    })
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    _, err = helm.NewRelease(ctx, fmt.Sprintf(<span class="hljs-string">"%s-vcluster"</span>, stage), &amp;helm.ReleaseArgs{
        Name:  pulumi.String(fmt.Sprintf(<span class="hljs-string">"%s-vcluster"</span>, stage)),
        Chart: pulumi.String(<span class="hljs-string">"vcluster"</span>),
        RepositoryOpts: &amp;helm.RepositoryOptsArgs{
            Repo: pulumi.String(<span class="hljs-string">"https://charts.loft.sh"</span>),
        },
        Values: pulumi.Map{
            <span class="hljs-string">"sync"</span>: pulumi.Map{
                <span class="hljs-string">"nodes"</span>: pulumi.Map{
                    <span class="hljs-string">"enabled"</span>:         pulumi.Bool(<span class="hljs-literal">true</span>),
                    <span class="hljs-string">"enableScheduler"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                    <span class="hljs-string">"nodeSelector"</span>:    pulumi.Sprintf(<span class="hljs-string">"env=%s"</span>, stage),
                },
            },
            <span class="hljs-string">"nodeSelector"</span>: pulumi.Map{
                <span class="hljs-string">"env"</span>: pulumi.String(stage),
            },
            <span class="hljs-string">"init"</span>: pulumi.Map{
                <span class="hljs-string">"helm"</span>: pulumi.Array{
                    pulumi.Map{
                        <span class="hljs-string">"chart"</span>: pulumi.Map{
                            <span class="hljs-string">"name"</span>:    pulumi.String(<span class="hljs-string">"doppler-kubernetes-operator"</span>),
                            <span class="hljs-string">"repo"</span>:    pulumi.String(<span class="hljs-string">"https://helm.doppler.com"</span>),
                            <span class="hljs-string">"version"</span>: pulumi.String(<span class="hljs-string">"1.2.5"</span>),
                        },
                        <span class="hljs-string">"release"</span>: pulumi.Map{
                            <span class="hljs-string">"name"</span>:      pulumi.String(<span class="hljs-string">"doppler-operator"</span>),
                            <span class="hljs-string">"namespace"</span>: pulumi.String(<span class="hljs-string">"doppler-operator-system"</span>),
                        },
                    },
                },
            },
        },
    }, pulumi.Provider(preProdKubernetesProvider))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
}
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitted for brevity</span>

        <span class="hljs-comment">// Deploy the vcluster to development and staging</span>
        err = deployPreProd(ctx, preProd, <span class="hljs-string">"development"</span>)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        err = deployPreProd(ctx, preProd, <span class="hljs-string">"staging"</span>)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        <span class="hljs-comment">// omitted for brevity</span>
        err = deployProd(ctx, prod)
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>A lot is going on here, so let's break it down again.</p>
<ul>
<li><p>The <code>deployPreProd</code> function is responsible for deploying a <code>vcluster</code> to the pre-production environments, including development and staging. This function employs the Helm chart for deploying the <code>vcluster</code> and each <code>vcluster</code> is configured to use specific node pools - namely <code>development</code> and <code>staging</code>.</p>
<ul>
<li><p>The <code>preProdKubernetesProvider</code> is created with the given cluster configuration, which is identified by the stage parameter. This provider enables Pulumi to manage resources in a Kubernetes cluster.</p>
</li>
<li><p>The <code>sync.nodes.nodeSelector</code> field is used to specify the node pool for the <code>vcluster</code> synchronization feature, whereas the <code>nodeSelector</code> field determines the node pool for the <code>vcluster</code> itself. This dual configuration is implemented to ensure the correct allocation and utilization of resources within the respective environments.</p>
</li>
<li><p>In addition to this, the <code>init.helm</code> field is leveraged to deploy the Doppler Secrets Operator within the <code>vcluster</code>. This is achieved by specifying the name, repository, and version of the Helm chart.</p>
</li>
</ul>
</li>
<li><p>The function <code>deployProd</code> is used to deploy the Doppler Secrets Operator without the usage of <code>vcluster</code>. Remember this was our production cluster.</p>
<ul>
<li><p>A Kubernetes provider <code>productionKubernetesProvider</code>, which is a representation of the Kubernetes cluster where the resources will be deployed. It uses the provided cluster string output (which is presumably a kubeconfig) to connect to the Kubernetes cluster.</p>
</li>
<li><p>A Kubernetes secret, named <code>doppler-token-secret</code>, is created in the <code>doppler-operator-system</code> namespace. This secret is created with data retrieved from the Pulumi configuration using <code>config.GetSecret(ctx, " doks-production")</code>. This data is then base64 encoded before being added to the secret.<br />  <mark>See below for how to set the config on Pulumi!</mark></p>
</li>
<li><p>A custom resource, of kind <code>DopplerSecret</code>, which includes references to the <code>doppler-token-secret</code> and a managed secret named <code>doppler-secret</code> in the <code>default</code> namespace.</p>
</li>
<li><p>A Kubernetes deployment, named <code>hello-server</code>, which creates a pod running the <code>ghcr.io/dirien/hello-server/hello-server:latest</code> Docker image and exposes port <code>8080</code>. This deployment includes an environment variable sourced from the <code>doppler-secret</code>.</p>
</li>
</ul>
</li>
<li><p>Both functions will be called in the <code>main</code> function, after the creation of the DOKS cluster.</p>
</li>
</ul>
<p>To generate a service token, head over to the <code>Doppler</code> dashboard, and select your project and environment. Click the <code>Access</code> tab and generate the token there</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684087842368/fd58c814-aa6e-44e6-b91b-31b63a11b91d.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684087859639/1b8a39a4-2622-4a77-8fbe-783017ef9989.png" alt class="image--center mx-auto" /></p>
<p>Now you can use the Pulumi config command to set the service token:</p>
<pre><code class="lang-bash">pulumi config <span class="hljs-built_in">set</span> doks-production &lt;toke&gt; --secret
</code></pre>
<h2 id="heading-deploying-the-vcluster-and-doppler-secrets-operator">Deploying the vcluster and Doppler Secrets Operator</h2>
<p>Now with all set, we can call our Pulumi function to deploy the <code>vclusters</code> and <code>Doppler Secrets Operator</code> to all of our environments.</p>
<pre><code class="lang-bash">pulumi up
</code></pre>
<blockquote>
<p>If you love the thrill, set <code>--yes</code> to skip the confirmation prompt and <code>--skip-preview</code> to skip the preview step.</p>
</blockquote>
<p>After a couple of minutes, everything should be deployed and ready to go. We can now try to test everything out.</p>
<h3 id="heading-testing-the-doppler-secrets-operator-in-all-environments">Testing the Doppler Secrets Operator in all environments</h3>
<p>I left the demo example at Doppler, as it is.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684087750835/a8ca8eb2-7c8a-407b-ac09-85195b96e68d.png" alt class="image--center mx-auto" /></p>
<p>The easiest way to test the Doppler Secrets Operator is in the production environment. This is because I already have deployed a demo application that uses the secrets the Doppler Secrets Operator retrieves from Doppler and created a Kubernetes secret for it.</p>
<p>Again, this is a short demo. I did not create any <code>Ingress</code> or <code>Service</code> to expose the application. Instead, we will use kube port forwarding to access the application.</p>
<pre><code class="lang-bash">pulumi stack output prodKubeConfig --show-secrets &gt; prod.yaml
<span class="hljs-built_in">export</span> KUBECONFIG=prod.yaml
kubectl port-forward deployment/hello-server 8080:8080
</code></pre>
<p>And then use <code>curl</code> to access the application and retrieve the secret. The <code>env</code> endpoint will return the value of any environment variable set in the pod. In this case, the <code>DB_URL</code> environment variable.</p>
<pre><code class="lang-bash">curl http://localhost:8080/env/DB_URL
DB_URL=psql://autopilot@10.127.172.12/modelX%
</code></pre>
<p>That looks good, let's try if the pod gets restarted when we update the secret in Doppler. To achieve this, you need to add <code>"secrets.doppler.com/reload": "true"</code> to the annotations of the deployment.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684088121296/32017f59-35a7-460a-9de9-52f679b81667.png" alt class="image--center mx-auto" /></p>
<p>We keep an eye on the pod using <code>kubectl get pods -w</code> and update the secret in Doppler. After a couple of seconds, the pod should be restarted.</p>
<pre><code class="lang-bash">k get pods -o wide -w
NAME                            READY   STATUS    RESTARTS   AGE   IP             NODE            NOMINATED NODE   READINESS GATES
hello-server-66bd4dd5f5-swktv   1/1     Running   0          19m   10.244.0.124   default-fx9cn   &lt;none&gt;           &lt;none&gt;
hello-server-55bbf4b78c-b49xq   0/1     Pending   0          0s    &lt;none&gt;         &lt;none&gt;          &lt;none&gt;           &lt;none&gt;
hello-server-55bbf4b78c-b49xq   0/1     Pending   0          0s    &lt;none&gt;         default-fx9cn   &lt;none&gt;           &lt;none&gt;
hello-server-55bbf4b78c-b49xq   0/1     ContainerCreating   0          0s    &lt;none&gt;         default-fx9cn   &lt;none&gt;           &lt;none&gt;
hello-server-55bbf4b78c-b49xq   1/1     Running             0          2s    10.244.0.73    default-fx9cn   &lt;none&gt;           &lt;none&gt;
hello-server-66bd4dd5f5-swktv   1/1     Terminating         0          20m   10.244.0.124   default-fx9cn   &lt;none&gt;           &lt;none&gt;
hello-server-66bd4dd5f5-swktv   0/1     Terminating         0          20m   10.244.0.124   default-fx9cn   &lt;none&gt;           &lt;none&gt;
hello-server-66bd4dd5f5-swktv   0/1     Terminating         0          20m   10.244.0.124   default-fx9cn   &lt;none&gt;           &lt;none&gt;
hello-server-66bd4dd5f5-swktv   0/1     Terminating         0          20m   10.244.0.124   default-fx9cn   &lt;none&gt;           &lt;none&gt;
</code></pre>
<p>Recreating the port-forward and calling the <code>env</code> endpoint again should show the updated secret.</p>
<pre><code class="lang-bash">curl http://localhost:8080/env/DB_URL
DB_URL=psql://me@127.0.0.1/reload
</code></pre>
<p>Full success! Now we can test the same in the development and staging environments. For this, we need to use the <code>kubeconfig</code> of the <code>pre-prod-cluster</code></p>
<pre><code class="lang-bash">pulumi stack output preProdKubeConfig --show-secrets &gt; pre-prod.yaml
<span class="hljs-built_in">export</span> KUBECONFIG=pre-prod.yaml
</code></pre>
<p>And then we use the <code>vcluster</code> context to access the <code>vcluster</code> in the development and staging environments.</p>
<pre><code class="lang-bash">vcluster connect development-vcluster -n default
<span class="hljs-keyword">done</span> √ Switched active kube context to vcluster_development-vcluster_default_do-fra1-pre-prod-cluster-e058708
warn   Since you are using port-forwarding to connect, you will need to leave this terminal open
- Use CTRL+C to <span class="hljs-built_in">return</span> to your previous kube context
- Use `kubectl get namespaces` <span class="hljs-keyword">in</span> another terminal to access the vcluster
Forwarding from 127.0.0.1:10511 -&gt; 8443
Forwarding from [::1]:10511 -&gt; 8443
</code></pre>
<p>Check that the <code>vcluster</code> has the <code>development</code> node pool assigned to it!</p>
<pre><code class="lang-bash">kubectl get nodes
NAME STATUS ROLES AGE VERSION
development-fx9gg Ready    &lt;none&gt;   25h v1.26.3
</code></pre>
<p>That looks very good, last check is to see if the Doppler Secrets Operator is running in the <code>vcluster</code>.</p>
<pre><code class="lang-bash">kubectl get pods -n doppler-operator-system
NAME                                                   READY   STATUS    RESTARTS   AGE
doppler-operator-controller-manager-57b55f6fdf-qmh26   2/2     Running   0          24h
</code></pre>
<p>Everything looks good, we can now test the Doppler Secrets Operator in the development environment.</p>
<p>We create a service token as Kubernetes secret in the <code>doppler-operator-system</code> namespace.</p>
<pre><code class="lang-bash">kubectl create secret generic doppler-token-secret \
  --namespace doppler-operator-system \
  --from-literal=serviceToken=&lt;DOPPLER_SERVICE_TOKEN_FOR_DEVELOPMENT_ENVIRONMENT&gt;
</code></pre>
<pre><code class="lang-bash">secret/doppler-token-secret created
</code></pre>
<p>Create the <code>DopperSecret</code> in the <code>default</code> namespace.</p>
<pre><code class="lang-bash">cat &lt;&lt;EOF &gt; manifest.yaml
apiVersion: secrets.doppler.com/v1alpha1
kind: DopplerSecret
metadata:
  name: doppler-secret-test
  namespace: doppler-operator-system
spec:
  tokenSecret:
    name: doppler-token-secret
  managedSecret:
    name: doppler-test-secret
    namespace: default
EOF
kubectl apply -f manifest.yaml
</code></pre>
<pre><code class="lang-bash">dopplersecret.secrets.doppler.com/doppler-secret-test created
</code></pre>
<p>Deploy the application in the <code>default</code> namespace.</p>
<pre><code class="lang-bash">cat &lt;&lt;EOF &gt; deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-server
  annotations:
    secrets.doppler.com/reload: <span class="hljs-string">"true"</span>
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello-server
  template:
    metadata:
      labels:
        app: hello-server
    spec:
      containers:
        - name: hello-server
          image: ghcr.io/dirien/hello-server/hello-server:latest
          ports:
            - containerPort: 8080
          envFrom:
            - secretRef:
                name: doppler-test-secret
EOF
kubectl apply -f deployment.yaml
</code></pre>
<pre><code class="lang-bash">deployment.apps/hello-server created
</code></pre>
<p>And now port-forward the pod and check if the secret is available.</p>
<pre><code class="lang-bash">kubectl port-forward deployments/hello-server 8080:8080
</code></pre>
<p>In another terminal, we can check the <code>env</code> endpoint like before.</p>
<pre><code class="lang-bash">curl http://localhost:8080/env/DB_URL
DB_URL=psql://elon@localhost/modelX%
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1684088176277/e7ce2464-4694-42f6-80aa-e2337ea685c6.png" alt class="image--center mx-auto" /></p>
<p>Success! The same steps can be done for the staging environment. I will leave that as an exercise for you.</p>
<h2 id="heading-cleanup">Cleanup</h2>
<p>The cleanup is very easy, we just need to call the destroy command.</p>
<pre><code class="lang-bash">pulumi destroy
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In my opinion, the <strong>Doppler SecretOps Platform</strong> is a very promising way to approach the problem of managing secrets for a nearly infinite number of integration possibilities. In this example, I showed how to use the Doppler Secrets Operator as all things Kubernetes is what I am most familiar with. But the Doppler SecretOps Platform can be used for much more!</p>
<p>I particularly liked the Dashboard the most! I can see all my projects and environments in one place and can easily change the secrets for each environment. I can also see the history of changes and who changed what and when and of course I can create different teams and assign them to different projects and environments.</p>
<p>But I have to mention that the Doppler Secrets Operator is still in beta and there are some things that I did not like much and tried to avoid in this example. For example: currently, you can not create the namespace for the operator in advance. The namespace is created by the operator itself. That broke for me the automation of the <code>vcluster</code>! The <code>init</code> values in <code>vcluster</code> are not allowing to run the installation of Helm charts before the <code>manifests</code> are.</p>
<p>This is an issue for me when I want to use a vanilla GitOps approach with Flux or ArgoCD. There are issues open for both, the Doppler Secrets Operator and the <code>vcluster</code> project. I hope that this will be fixed soon.</p>
<p>I have to say, that I could maybe go around this by using more glue code but this is a demo and I wanted to keep it as simple as possible.</p>
<p>On the other hand, the production cluster without the <code>vcluster</code> is working very well and everything works as expected.</p>
<p>I hope you enjoyed this demo and I hope that you will try out the Doppler SecretOps Platform yourself!</p>
<h2 id="heading-resources">Resources</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://docs.doppler.com/docs">https://docs.doppler.com/docs</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/loft-sh/vcluster/issues/834">https://github.com/loft-sh/vcluster/issues/834</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/DopplerHQ/kubernetes-operator/issues/31">https://github.com/DopplerHQ/kubernetes-operator/issues/31</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/DopplerHQ/kubernetes-operator/issues/31">https://github.com/DopplerHQ/kubernetes-operator/issues/31</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/DopplerHQ/kubernetes-operator/issues/28">https://github.com/DopplerHQ/kubernetes-operator/issues/28</a></div>
]]></content:encoded></item><item><title><![CDATA[Bulletproof Your Project: Automate NPM Package Security Fixes with Recurring CI Tasks!]]></title><description><![CDATA[Introduction
Nothing is more annoying than having a project that has outdated dependencies. If you are not using tools like Dependabot or Mend(Renovate) the process of updating dependencies can be a tedious and error-prone task. This is especially tr...]]></description><link>https://blog.ediri.io/bulletproof-your-project-automate-npm-package-security-fixes-with-recurring-ci-tasks</link><guid isPermaLink="true">https://blog.ediri.io/bulletproof-your-project-automate-npm-package-security-fixes-with-recurring-ci-tasks</guid><category><![CDATA[npm]]></category><category><![CDATA[Security]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[dependencies]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Fri, 28 Apr 2023 11:43:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1682682149949/0957af67-5503-414d-b359-4b2cbd0d846c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Nothing is more annoying than having a project that has outdated dependencies. If you are not using tools like <a target="_blank" href="https://github.com/dependabot">Dependabot</a> or <a target="_blank" href="https://www.mend.io/renovate/">Mend(Renovate)</a> the process of updating dependencies can be a tedious and error-prone task. This is especially true if you have a project which has a large number of dependencies.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dependabot">https://github.com/dependabot</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.mend.io/renovate/">https://www.mend.io/renovate/</a></div>
<p> </p>
<p>Outdated dependencies are a security risk and could cause bugs or performance impacts. So it is important to keep them up to date. But how can we do this in a safe and efficient way?</p>
<p>In this short blog post, I will show you how an easy way to update your dependencies using only the command line and <code>npm</code>.</p>
<h2 id="heading-preferred-way-as-github-user">Preferred Way (as GitHub user)</h2>
<p>I always recommend using <a target="_blank" href="https://github.com/dependabot">dependabot</a> or <a target="_blank" href="https://www.mend.io/renovate/">Mend(Renovate)</a> to check for outdated dependencies and create pull requests for you. This is the most convenient way to keep your project up to date without any manual work on your side. Both tools are free and support a huge variety of package systems and languages.</p>
<h2 id="heading-alternative-way-without-github">Alternative Way (without GitHub)</h2>
<p>If you don't use GitHub, or can't use one of the tools mentioned above, you can use the following approach to update your dependencies.</p>
<p>NPM ships with a command called <code>npm outdated</code>. This command will list all outdated dependencies of your project.</p>
<pre><code class="lang-bash">npm outdated
Package              Current   Wanted   Latest  Location                         Depended by
@pulumi/pulumi        3.50.0   3.50.0   3.65.1  node_modules/@pulumi/pulumi      purrl-ts
@pulumiverse/purrl     0.3.1    0.3.1    0.4.0  node_modules/@pulumiverse/purrl  purrl-ts
@types/node         18.11.17  18.16.2  18.16.2  node_modules/@types/node         purrl-ts
</code></pre>
<p>As you can see, the command lists all outdated dependencies. The <code>Current</code> column shows the version of the dependency you are currently using. The <code>Wanted</code> column shows the version you want to use according to your <code>semver</code> rules. The <code>Latest</code> column shows the latest version of the dependency available in the registry.</p>
<p>Now you can use the <code>npm update</code> command to update your dependencies.</p>
<pre><code class="lang-bash">npm update (-S)
</code></pre>
<p>This command will update all dependencies to the latest version according to your <code>semver</code> rules and will always use the <code>Wanted</code> version.</p>
<h2 id="heading-reference-implementation-in-azure-devops-and-ms-teams">Reference Implementation in Azure DevOps and MS Teams</h2>
<p>Let's take a look at how we can use this approach in Azure DevOps Pipelines and connect it to MS Teams to get a little ChatOps feeling.</p>
<p>First of all, you need to create a webhook in MS Teams. You can find a detailed description of how to do this in the official <a target="_blank" href="https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook">Microsoft documentation</a>.</p>
<p>Next, you create a new pipeline in Azure DevOps. I will use a simple pipeline with a single stage and a single job for the sake of simplicity.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">trigger:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>

<span class="hljs-attr">schedules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">cron:</span> <span class="hljs-string">'0 14 * * *'</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-attr">include:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>

<span class="hljs-attr">parameters:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">webhookUrl</span>

<span class="hljs-attr">pool:</span>
  <span class="hljs-attr">vmImage:</span> <span class="hljs-string">ubuntu-latest</span>

<span class="hljs-attr">steps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">NodeTool@0</span>
    <span class="hljs-attr">inputs:</span>
      <span class="hljs-attr">versionSource:</span> <span class="hljs-string">'spec'</span>
      <span class="hljs-attr">versionSpec:</span> <span class="hljs-string">'18.x'</span>
    <span class="hljs-attr">displayName:</span> <span class="hljs-string">'🧑‍🔧 Install Node.js'</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">script:</span> <span class="hljs-string">|
      npm install
      npm outdated
</span>    <span class="hljs-attr">displayName:</span> <span class="hljs-string">'🔎 Check for outdated dependencies'</span>
    <span class="hljs-attr">workingDirectory:</span> <span class="hljs-string">'examples/purrl-ts'</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">bash:</span> <span class="hljs-string">|
      curl -v -X POST ${{ parameters.webhookUrl }} \
        -H 'Content-Type: application/json; charset=utf-8' \
        --data-binary @- &lt;&lt; EOF
        {
          "type":"message",
          "attachments":[
            {
              "contentType":"application/vnd.microsoft.card.adaptive",
              "contentUrl":null,
              "content":{
                "$schema":"http://adaptivecards.io/schemas/adaptive-card.json",
                "type":"AdaptiveCard",
                "version":"1.2",
                "body":[
                  {
                    "type": "TextBlock",
                    "wrap": true,
                    "text": "NPN detected outdated packages at $(Build.Repository.Name) in the $(Build.SourceBranchName), more details can be found in the build log. \r\r $(Build.Repository.Uri)"
                  }
                ]
              }
            }
          ]
        }
      EOF      
</span>    <span class="hljs-attr">displayName:</span> <span class="hljs-string">'🛫Invoke webhook'</span>
    <span class="hljs-attr">condition:</span> <span class="hljs-string">failed()</span>
</code></pre>
<p>The pipeline is triggered on a schedule every day at 2pm. When the <code>npm outdated</code> command detects outdated dependencies the pipeline will fail. In this case the <code>Invoke webhook</code> task will be executed and send a message to MS Teams.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682681673998/8d6b7c57-b0f9-4ff4-8e66-25e03ec4bbbb.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Keeping your project's dependencies up to date is essential for maintaining security, fixing bugs, and improving performance. Tools like Dependabot and Mend(Renovate) can help automate the process, making it convenient and effortless. However, if you don't use GitHub or can't use these tools, you can still manage your dependencies efficiently using the command line and NPM with commands like npm outdated and npm update.</p>
<p>By adopting these practices, you can ensure that your project stays current, secure, and performs at its best while minimizing the manual work involved in dependency management.</p>
<h2 id="heading-resources">Resources</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://pakstech.com/blog/azure-devops-teams-message/?utm_content=cmp-true">https://pakstech.com/blog/azure-devops-teams-message/?utm_content=cmp-true</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/scheduled-triggers">https://learn.microsoft.com/en-us/azure/devops/pipelines/process/scheduled-triggers</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables">https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format">https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format</a></div>
]]></content:encoded></item><item><title><![CDATA[Learn Rust with ChatGPT]]></title><description><![CDATA[Introduction
ChatGPT is a large language model developed by OpenAI based on the GPT-3.5 architecture. It is designed to generate human-like responses to text inputs, making it an ideal tool for natural language processing tasks such as language trans...]]></description><link>https://blog.ediri.io/learn-rust-with-chatgpt</link><guid isPermaLink="true">https://blog.ediri.io/learn-rust-with-chatgpt</guid><category><![CDATA[Rust]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[AI]]></category><category><![CDATA[Developer]]></category><category><![CDATA[learning]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Thu, 27 Apr 2023 09:37:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1682588137239/98b00e6f-3d01-4c82-8042-e079654a0d7a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction"><strong>Introduction</strong></h2>
<p>ChatGPT is a large language model developed by OpenAI based on the GPT-3.5 architecture. It is designed to generate human-like responses to text inputs, making it an ideal tool for natural language processing tasks such as language translation, text summarization, and question answering. ChatGPT has been trained on a vast amount of data, allowing it to understand and generate responses to a wide range of topics.</p>
<h2 id="heading-getting-started"><strong>Getting Started</strong></h2>
<p>To begin using ChatGPT, you'll need an internet connection and a compatible web browser. While you don't have to install any software, additional steps like creating an account or obtaining an API key may be necessary, depending on the specific version of ChatGPT you want to access. For more information on getting started with an OpenAI-powered ChatGPT, visit their website for detailed instructions.</p>
<h3 id="heading-what-is-chatgpt"><strong>What is ChatGPT?</strong></h3>
<p>ChatGPT is an AI language model that generates human-like responses to text inputs. It uses a deep learning architecture called GPT-3.5, which has been trained on a large amount of text data. ChatGPT can understand and generate responses to a wide range of topics, making it an ideal tool for natural language processing tasks. With ChatGPT, you can have a conversation with an AI that feels like talking to a real person.</p>
<h2 id="heading-rust">Rust</h2>
<p>According to its official website, "Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety." Rust is a statically typed, compiled programming language that was designed with a focus on system-level programming, but it can also be used for web development, game development, and other application domains.</p>
<p>Rust's syntax is similar to that of C and C++, but it is a more modern language that provides features like memory safety, zero-cost abstractions, and guaranteed thread safety. Rust is a systems programming language that is memory-safe and thread-safe by default, making it a popular choice for building high-performance and reliable software.</p>
<p>Rust is a multi-paradigm language that supports both functional and imperative programming styles. It is designed to be easy to learn and use, with a clear and concise syntax that emphasizes code readability. Rust also has a growing community of developers who contribute to its ecosystem of libraries, frameworks, and tools.</p>
<p>Overall, Rust is a powerful and versatile programming language that is well-suited for building reliable and high-performance systems. Its emphasis on safety and concurrency make it a popular choice for developers who value those qualities in their software.</p>
<h2 id="heading-lets-get-rust-y-with-it">Let's get Rust-y with it!</h2>
<p>Okay, that was a terrible joke, but don't let that discourage you from learning Rust. Rust is a seriously cool language that offers a lot of benefits over other programming languages. It may take a bit of effort to get started, but trust me, it's worth it. So let's dive in and start learning Rust!</p>
<blockquote>
<p>Keep in mind that the response you receive could be completely different from the ones I got!</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585357228/346b2e1a-cbcc-4661-8403-0e9831368d07.png" alt class="image--center mx-auto" /></p>
<p>We've got a solid training plan so far. Let's ask our teacher to begin with the first chapters.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585466865/dd70c138-abb7-41e0-81b9-186867ade25f.png" alt class="image--center mx-auto" /></p>
<p>Moving on to the next chapter about <code>Basic Rust Programming</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585584702/b98e6d2a-b497-4274-8ef9-552c8ddbc19a.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585603259/68d79e71-a281-4033-8f6d-15b98eb42840.png" alt class="image--center mx-auto" /></p>
<p>This is excellent output; we can delve deeper if necessary. Let's switch to the next topic, <code>Rust Functions and Modules</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585626878/7ac4b004-7046-4846-9254-8068f558f7a2.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585659243/1b6cfb07-a574-416b-b10a-ebef62c100ab.png" alt class="image--center mx-auto" /></p>
<p>I noticed a new thing called <code>Cargo.toml</code>. Let's ask our teacher about it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585712362/2460f4ff-ef35-4813-a020-6e07ab655a1c.png" alt class="image--center mx-auto" /></p>
<p>That's pretty good, to be honest. Now we're getting to the big topic, <code>Rust Ownership and Borrowing</code>, and I want to compare it to Go.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585769878/cfa7aeaf-95c8-4f2b-a406-42930ec0e1ed.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585795316/00f12228-8449-4bd1-bb2f-b10931cbf9f3.png" alt class="image--center mx-auto" /></p>
<p>Next, I want to learn more about <code>Rust Collections and Iterators</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585870043/b891f2f7-f104-41bf-9c5c-32f5606fc7c4.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585899655/779e4958-4648-40d0-9c1f-21c29b5e0b7b.png" alt class="image--center mx-auto" /></p>
<p>Error handling is always important in a new programming language, so let's ask our teacher about <code>Rust Error Handling</code> and how it compares to Golang.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585922051/036b4095-a8e4-4a08-bf56-1ba9ff1495ff.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585952109/c39fb5d9-961a-44f1-b871-67827a0a31b4.png" alt class="image--center mx-auto" /></p>
<p>It's time to apply what we've learned so far. Let's ask our teacher to show and explain a basic CLI for calculating Fibonacci numbers.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682585993800/5f26ac09-7596-476c-8dbd-95ccb00212fe.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682586015471/798fd5d9-dc63-4739-9f44-889a361f80ef.png" alt class="image--center mx-auto" /></p>
<p>That worked very well! It's compiling and providing the numbers!</p>
<pre><code class="lang-bash">➜ cargo run 10
   Compiling fibonacci_cli v0.1.0 (/Users/dirien/trash-stacks/rust/fibonacci_cli)
    Finished dev [unoptimized + debuginfo] target(s) <span class="hljs-keyword">in</span> 0.76s
     Running `target/debug/fibonacci_cli 10`
The 10th Fibonacci number is 55
</code></pre>
<p>Let's move on to the next topic, <code>Rust Concurrency</code>, and compare it to Golang once again.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682586047649/f1572678-80bb-4f72-8ebc-c7a22a3fbb51.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682586071944/244ee242-6ab2-46a8-b1f4-2d44fcb955c1.png" alt class="image--center mx-auto" /></p>
<p>Let's ask for a working example here too:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682586141428/9d632625-fb67-421e-af4b-02982dd2fbd8.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-bash">cargo run  
   Compiling fibonacci_cli v0.1.0 (/Users/dirien/trash-stacks/rust/cli)
    Finished dev [unoptimized + debuginfo] target(s) <span class="hljs-keyword">in</span> 0.74s
     Running `target/debug/cli`
Result: 0
Result: 2
Result: 4
Result: 8
Result: 6
</code></pre>
<p>That's not bad. Let's see how our teacher handles the next topic about <code>Rust Web Development</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682586177797/53231f3a-8980-4273-95b2-16906c3d66a6.png" alt class="image--center mx-auto" /></p>
<p>The teacher talks about dependencies! We should ask about this and how to add them to our project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682586202605/bd0a2f1e-4019-482f-96bb-e777e1e62102.png" alt class="image--center mx-auto" /></p>
<p>Oh, I got an error message! Let's ask the teacher for help.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682586223081/b22884c3-4989-431c-87ff-8d91cf0ab48a.png" alt class="image--center mx-auto" /></p>
<p>Great, this answer fixed the error messages, and the program is working fine!</p>
<pre><code class="lang-bash">curl 127.0.0.1:8080/hello
{<span class="hljs-string">"greeting"</span>:<span class="hljs-string">"Hello, World!"</span>}%
</code></pre>
<p>Let's ask how we can create a second endpoint and post a JSON with our name to it! The new endpoint should then greet us with our name.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682586239601/abb97301-378f-49a6-9284-80a4c38d10bc.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682586257235/2145557d-543a-41fa-ac81-01302aa380ae.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-bash">curl -X POST -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"name": "Alice"}'</span> http://localhost:8080/greet

{<span class="hljs-string">"greeting"</span>:<span class="hljs-string">"Hello, Alice!"</span>}%
</code></pre>
<p>Everything worked fine so far. It's time to finish the session for today! Let's ask our teacher for some further resource recommendations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682586277761/221b62ff-db5b-42c7-a425-9b1cc77817a1.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Using ChatGPT to learn Rust can be a helpful tool for getting started with the language. Its ability to generate human-like responses to text inputs can provide a conversational approach to learning that can be engaging and intuitive. However, it is important to keep in mind that ChatGPT is an AI language model and it may generate responses that are incorrect, misleading, or outdated.</p>
<h2 id="heading-verify-the-information"><strong>Verify the Information</strong></h2>
<p>As a learner, it is essential to verify the information provided by ChatGPT by consulting official documentation or reliable sources. While ChatGPT can provide useful insights and guidance, it is not a substitute for learning from reputable sources and practicing programming on your own.</p>
<h2 id="heading-stay-informed"><strong>Stay Informed</strong></h2>
<p>Additionally, it is important to note that ChatGPT may not always have the most up-to-date information on Rust or its libraries. As Rust is a fast-evolving language with new features and updates being released frequently, it is essential to stay informed on the latest developments and to use current resources when learning the language.</p>
<p>In summary, while ChatGPT can be a useful tool for learning Rust, it is important to approach it with caution and to supplement its responses with official documentation and reliable sources. By doing so, learners can maximize the benefits of ChatGPT while ensuring that they are learning accurate and up-to-date information about Rust.</p>
<h2 id="heading-disclaimer"><strong>Disclaimer</strong></h2>
<p>All content in this blog was generated by ChatGPT, an AI language model. While the information provided is generally accurate, it may not be 100% correct or up-to-date. Always consult official documentation or seek assistance from the Rust community when in doubt.</p>
]]></content:encoded></item><item><title><![CDATA[KCD Israel Unplugged: My Thrilling Cloud Native Adventure in Tel Aviv!]]></title><description><![CDATA[A KCD in Tel Aviv? Hold my beer!
When I saw this tweet on my "For You" page, I knew that I needed to submit a CfP.
https://twitter.com/cncf_tlv/status/1612110590461333511?s=20
 
There's no shortage of meetups, conferences, and user groups out there, ...]]></description><link>https://blog.ediri.io/kcd-israel-unplugged-my-thrilling-cloud-native-adventure-in-tel-aviv</link><guid isPermaLink="true">https://blog.ediri.io/kcd-israel-unplugged-my-thrilling-cloud-native-adventure-in-tel-aviv</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[community]]></category><category><![CDATA[israel]]></category><category><![CDATA[CNCF]]></category><category><![CDATA[kcd]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sun, 16 Apr 2023 22:12:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1681683017709/77c83bf6-f520-4f3d-95c8-b6aa16c50f4c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-a-kcd-in-tel-aviv-hold-my-beer">A KCD in Tel Aviv? Hold my beer!</h2>
<p>When I saw this tweet on my "For You" page, I knew that I needed to submit a CfP.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/cncf_tlv/status/1612110590461333511?s=20">https://twitter.com/cncf_tlv/status/1612110590461333511?s=20</a></div>
<p> </p>
<p>There's no shortage of meetups, conferences, and user groups out there, and most of them are top-notch and definitely worth attending. If only I could visit them all, with an endless budget! Conference in Japan? Konnichi wa! How about India? Namaste, here I come!</p>
<p>But alas, that's not my reality. So, every year, I start my search for the most exciting conferences to add to my agenda. One of my go-to sources for this is <a target="_blank" href="https://twitter.com/aurelievache"><strong>Aurélie Vache's</strong></a> fantastic repository:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/scraly/developers-conferences-agenda">https://github.com/scraly/developers-conferences-agenda</a></div>
<p> </p>
<p>And, for real, KCD Israel is one of these highlights! I had to submit a CfP. After quickly confirming that English presentations were welcome, I got to work.</p>
<p><mark>Time to craft the perfect CfP!</mark></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681674123217/bf1bcd48-4ef1-49ac-b61a-722ea7122aed.gif" alt class="image--center mx-auto" /></p>
<p>I'm currently employed by <a target="_blank" href="https://www.pulumi.com/"><strong>Pulumi</strong></a>, and my position in a cloud-native startup provides endless inspiration for talk ideas! Plus, I genuinely enjoy sharing my knowledge and engaging with others.</p>
<h2 id="heading-when-things-dont-go-as-planned"><strong>When Things Don't Go as Planned</strong></h2>
<p>Well, this is what can happen:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681674353723/43e9e859-de5c-4e01-98c2-fefb7e26e503.png" alt class="image--center mx-auto" /></p>
<p>Yes, sometimes CfPs get declined. There are countless reasons for this, and it used to frustrate me to no end.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681674492325/51d07cf1-9fe0-4b48-8ecb-7ae2255a7f05.gif" alt class="image--center mx-auto" /></p>
<p>But that's just the name of the game! You win some, you lose some. As the French say, "c'est la vie"!</p>
<p>Despite the rejection, my manager at Pulumi and I agreed that it still made sense for me to head to Tel Aviv. We have clients there, and it's a great opportunity to engage with the local community and get a feel for the cloud-native landscape.</p>
<p>So <mark>Shalom Aleichem</mark> KCD Israel! I'm coming for you!</p>
<h2 id="heading-day-1-flight-and-the-first-evening">Day -1: Flight and the first evening</h2>
<p>Bright and early, I drove to Frankfurt and boarded my flight to Tel Aviv.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681675058641/3e3e6add-35c0-4952-bd98-6e51b80a749d.png" alt class="image--center mx-auto" /></p>
<p>It was a 5 hours flight out there, very calm and relaxed. Most of my fellow passengers were Germans embarking on a journey to explore the <mark>Holy Land</mark>! When they asked me where I was going, they were surprised that I go to a tech conference!</p>
<p>Check out this stunning shot of our approach to Tel Aviv</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681675410197/b2d3b004-9bce-4f2c-8e5c-d4d45f8b0b40.jpeg" alt class="image--center mx-auto" /></p>
<p>After I arrived, I went out to grab something to eat. Luckily I meet with <a target="_blank" href="https://twitter.com/vfarcic">Viktor Farcic</a> and had a very good lunch that evening. And as you can see from the picture, a very intense first talk about <mark>GitOps, Security and Infrastructure as Code</mark>!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681675549276/690b8341-f59b-4d86-8da9-ae81782314fc.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-conference-day">The conference day!</h2>
<p>Finally, the day of the conference! Yeah! I put on my best Pulumi swag shirt and head off from my hotel to the venue center.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681675767965/3c6e0d49-5831-4e34-b433-510dbee0f382.png" alt class="image--center mx-auto" /></p>
<p>Quickly, went to collect my badge to then head over to the main stage to wait for the KCD to start!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681675888980/70b18db9-f4f2-4711-af77-d06eccbadd0a.jpeg" alt class="image--center mx-auto" /></p>
<blockquote>
<p><strong>Important</strong>: Find a spot in the middle! It is really like the cinema! You don't want to sit too far away from the action!</p>
</blockquote>
<p>So, I think I get one of the best spots in the main stage place! Ready to enjoy a day of cloud native!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681676026256/723c391f-a1aa-4d9e-afa6-a1f4f884a495.jpeg" alt class="image--center mx-auto" /></p>
<p>I am still very shy, when it comes to taking selfies! I need to learn how to smile!</p>
<p><strong>The organization team</strong></p>
<p>Big thanks and shoutout to the organization team (<strong><em>Shimon Tolts, Dana Fine, Arthur Berezin, Dotan Horovits, Itay Shakury and Ronen Levinson</em></strong>) of KCD Israel and their well-deserved big round of applause.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681676210402/a36d118e-ce84-4dd7-bc8a-592f32d077c4.jpeg" alt class="image--center mx-auto" /></p>
<p>Shimon Tolts spend some time explaining the values of this event, unfortunatly it was on Hebrew and I was not able to understand everything but the slides transported the message very well.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681676390256/b50b8f26-1123-4b8d-8010-3ee354531561.jpeg" alt class="image--center mx-auto" /></p>
<p>The first talk of the conference was from <a target="_blank" href="https://twitter.com/LoriLorusso">Lori Lorusso</a> called <strong>The CNCF &amp; You - Growing Community in the CloudNative Landscape</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681676585026/52c00a0b-307b-444b-ad6a-b326c1f722af.jpeg" alt class="image--center mx-auto" /></p>
<p>She gave a very good talk about the growing community and the role of the CNCF! Very interesting to hear, as I did not understand before all the parts of the CNCF.</p>
<p>The next speaker was <a target="_blank" href="https://twitter.com/sublimino">Andrew Martin</a> and his talk: <strong><em>The Irresistible Rise of Cloud Native: What the Future Means for YOU</em></strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681676880245/c7d09066-886b-407e-81cc-cbf5701f8192.jpeg" alt class="image--center mx-auto" /></p>
<p>First time I listen to a talk from Andrew and I thoroughly enjoyed his prediction on what the cloud-native future will be. Of course, he mentioned things like WASM and platform engineering!</p>
<p>Next Daniel Gur about <strong><em>Prometheus That Scales</em></strong>. This talk was very interesting, as Daniel shared his experiences and lessons learned from running Prometheus at scale in his company Outbrain!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681677163913/fbfeade3-bc7f-49a4-89e2-2c507ac6becd.jpeg" alt class="image--center mx-auto" /></p>
<p>Next was from Lior Yantovski and Chen Fleisher about <strong><em>GitOps with Argo CD and Helm: Managing Your Kubernetes Repository.</em></strong> This talk was in Hebrew so I had to rely on the slides and the people in the audience!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681677343313/07e838c4-06dc-459e-8fb3-e3a8bba2fa00.jpeg" alt class="image--center mx-auto" /></p>
<p>Even though I did not understand the talk, I loved on memes on their slides!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681677379645/e1b787ff-e3b7-4a92-9630-76c67374e0a2.jpeg" alt class="image--center mx-auto" /></p>
<p>Yepp, you should never</p>
<pre><code class="lang-bash">kubectl apply ...
</code></pre>
<p>in production! Use <strong>GitOps</strong>!</p>
<p>Funny is also the amount of <a target="_blank" href="https://twitter.com/kelseyhightower">Kelsey Hightower</a> quotes on the slides! Here is number one!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681677555667/569e42d0-0146-4f7f-b10f-2b18c7fe5d4e.jpeg" alt class="image--center mx-auto" /></p>
<p>Now we had three lighting talks going on. I could not take a picture of all of them, as I wanted to listen to them and they were really quick with their slides:</p>
<ul>
<li><p>Udi Rot with <strong><em>Caretta: Mapping Services With eBPF</em></strong></p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681677688591/40c0ae68-8a58-4918-9e7e-b080d2be06f1.jpeg" alt class="image--center mx-auto" /></p>
</li>
<li><p>Benny Rochwerger and <strong><em>Never Go Limitless – On the Importance of ResourceQuotas at the Edge</em></strong><br />  Very important talk and a call for all Kubernetes ops teams: Set your resource quotes on edge Kubernetes clusters!</p>
</li>
<li><p>Anton Weiss: The Promise of WASM<br />  No further explanation needed! WASM so hot right now!</p>
</li>
</ul>
<p>Time for the lunch break, and I had the time to stroll a little bit through the booths. I spotted this nice booth from <a target="_blank" href="https://www.groundcover.com">groundcover</a>! I love this kind of statements!</p>
<blockquote>
<p>Monitoring is worthless</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681678042474/8d558211-cede-43b0-a261-47b9ff1c9a9f.jpeg" alt class="image--center mx-auto" /></p>
<p>Next to visiting all the booths and eating delicious hummus, I had the opportunity to talk to different persons at the venue about all things cloud and infrastructure as code. Interesting to see how people think and knew about <mark>Pulumi</mark>.</p>
<p>After the lunch break, we continue the talks with <strong><em>Knative: Is it for me?</em></strong> from Elad Hirsch and Kornel Chlebovics.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681678540798/37bdec87-2aba-40cd-87b8-b1ab0a781e71.jpeg" alt class="image--center mx-auto" /></p>
<p>I love event-driven architecture and Knative is one of my preferred projects. I worked with it since nearly day one on our Openshift Clusters at my former workplace. I organized a meetup around this too. Check out the recording, if you are interested into!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/live/aIYyafr3cug?feature=share">https://www.youtube.com/live/aIYyafr3cug?feature=share</a></div>
<p> </p>
<p>And it is time for the second Kelsey quote of the day! And tbh, you can't repeat the great quotes of Kelsey enough!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681678607097/8b93e1e9-7ff3-43de-bef5-9f64195d850c.jpeg" alt class="image--center mx-auto" /></p>
<p>The next talk is <strong><em>Kubernetes Reloaded: AKA How to Avoid the Dev Plateau with CRDs</em></strong> by Rona Hirsch and Guy Menahem!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681678717298/620f622b-7d16-42de-9dc3-985860ab0178.jpeg" alt class="image--center mx-auto" /></p>
<p>CRDs are crucial to extending Kubernetes to your needs and Rona and Guy did a great work to explain what CRDs are under the hood!</p>
<p>And the last Kelsey quote of the day!</p>
<p><img src="https://pbs.twimg.com/media/Fr6Gg9qXoAE3ApE?format=jpg&amp;name=large" alt="Image" /></p>
<p>A quick switch of the speakers on stage and we are already in the next talk <strong><em>The Power of OPA Gatekeeper: Enforcing Kubernetes Access Control to secure your runtime</em></strong> from Batel Zohar Tova and Dana Rozen</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681678956658/c33a59b8-ba8b-4de1-9ae1-350764deac5e.jpeg" alt class="image--center mx-auto" /></p>
<p>They talked about how to secure your Kubernetes environment using the OPA Gatekeeper while using ArgoCD! Very powerful combo and a good talk!</p>
<p>Eran Bibi from Firefly gave the next talk <strong><em>Enter the Machines: Reducing Friction in Cloud Native using AI</em></strong></p>
<p>He talked about using AI for code and config generation! A very good talk and fits very well the current trend on LLM and GPT!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681679112408/4213f568-e869-43f7-8546-97d307c9cfbc.jpeg" alt class="image--center mx-auto" /></p>
<p>The talk from Boris Cherkasky about <strong><em>Stop The Kuberspendes - Getting Your K8s Cost Under Control</em></strong> I did not visit, as it was in Hebrew. I used the time with talking with people in the exhibition area! Was also very good and fruitful for me!</p>
<p>And now the last talk of the event comes from Viktor Farcic called <strong><em>Shifting Left Stateful Applications In Kubernetes!</em></strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681679388572/680b8a94-cf09-410e-a421-f920e0d0e40e.jpeg" alt class="image--center mx-auto" /></p>
<p>Always enjoy the talks of Viktor and this one was no exception. He explained on how to run stateful apps in Kubernetes in a self-service approach without the need to open a JIRA ticket and wait that others to perform the task for you!</p>
<p>And with this talk, the KCD Israel 2023 ends. Last time I hit the exhibition area and experienced this unique atmosphere!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681679590134/469a433d-3cef-4a1f-833e-6da96154e7bb.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion-and-flight-back-home">Conclusion and flight back home</h2>
<p>KCD Israel's inaugural edition was a fantastic experience, and I thoroughly enjoyed myself! I heard that tech workers in Tel Aviv work hard and play hard, and this event seemed to embody that spirit!</p>
<p>You could feel how much the people enjoyed the event. Everyone had a smile on their face, and the talks were always full. Unlike some other events I've attended, people were genuinely present, not working on their laptops during presentations.</p>
<p><mark>KCD Israel is definitely an event I'll revisit!</mark></p>
<p>Before I knew it, time had flown by, and I found myself waiting for the train to whisk me back to the airport.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1681679865961/a88428f9-13e0-415f-b503-a5721a1ef501.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-links">Links</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/playlist?list=PLj6h78yzYM2OqwftYNyxgf_F-zmIVAUGD">https://www.youtube.com/playlist?list=PLj6h78yzYM2OqwftYNyxgf_F-zmIVAUGD</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://community.cncf.io/events/details/cncf-kcd-israel-presents-kcd-israel-2023/">https://community.cncf.io/events/details/cncf-kcd-israel-presents-kcd-israel-2023/</a></div>
]]></content:encoded></item><item><title><![CDATA[Fiber (Go) vs. Nickel.rs (Rust): A Performance Showdown in 'Hello World']]></title><description><![CDATA[Introduction
In this article, I want to compare the performance of two different web frameworks for Rust and Go. Both frameworks are very similar in their design (all are inspired by Express.js) and both claim to be the fastest web framework (blazing...]]></description><link>https://blog.ediri.io/fiber-go-vs-nickelrs-rust-a-performance-showdown-in-hello-world</link><guid isPermaLink="true">https://blog.ediri.io/fiber-go-vs-nickelrs-rust-a-performance-showdown-in-hello-world</guid><category><![CDATA[Rust]]></category><category><![CDATA[golang]]></category><category><![CDATA[Developer]]></category><category><![CDATA[development]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sat, 08 Apr 2023 13:54:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1680962413285/9f515723-47fe-4f31-b053-44fc950fa1ee.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In this article, I want to compare the <mark>performance</mark> of two different web frameworks for <mark>Rust</mark> and <mark>Go</mark>. Both frameworks are very similar in their design (all are inspired by Express.js) and both claim to be the fastest web framework (blazing fast). Both frameworks are easy to use, which is a big plus for me (<em>I am not the smartest guy in the world, so easy is good)</em>.</p>
<h2 id="heading-the-competitors-in-detail">The Competitors in detail</h2>
<h3 id="heading-nickelers-rust">Nickele.rs (Rust)</h3>
<p>For Rust, I have chosen the <a target="_blank" href="https://nickel-org.github.io/">Nickel.rs</a> framework. It is a minimal and lightweight framework for web apps in Rust. It is inspired by Express.js and provides a lot of features like flexible routing, middleware, JSON handling, and more. And it's <mark>blazingly fast</mark>!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/nickel-org/nickel.rs">https://github.com/nickel-org/nickel.rs</a></div>
<p> </p>
<h3 id="heading-fiber-go">Fiber (Go)</h3>
<p>As a contender for Go, I have chosen the <a target="_blank" href="https://gofiber.io/">Fiber</a> framework. This framework is also inspired by Express.js and is built on top of <a target="_blank" href="https://github.com/valyala/fasthttp">Fasthttp</a>. It has a lot of features like middleware, routing, WebSockets, and more. And it claims to have <mark>extreme performance </mark> and a small memory footprint.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/gofiber/fiber">https://github.com/gofiber/fiber</a></div>
<p> </p>
<h2 id="heading-the-battle">The Battle 🥊</h2>
<p>The specs of my machine are Apple M1 Max (10 Core CPU) with 32GB of RAM.</p>
<p>The Tests will be written in <a target="_blank" href="https://github.com/codesenberg/bombardier">bombardier</a> and will be executed for 50, 100 and 500 concurrent users with executing 5M requests.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/codesenberg/bombardier">https://github.com/codesenberg/bombardier</a></div>
<p> </p>
<p>I use the following versions:</p>
<ul>
<li><p>Go: go1.20.3 darwin/arm64</p>
</li>
<li><p>Rust: rustc 1.65.0 (897e37553 2022-11-02)</p>
</li>
</ul>
<h3 id="heading-the-test-code">The Test Code</h3>
<h4 id="heading-nickelrs-rust"><code>Nickel.rs (Rust)</code></h4>
<pre><code class="lang-rust"><span class="hljs-meta">#[macro_use]</span>
<span class="hljs-keyword">extern</span> <span class="hljs-keyword">crate</span> nickel;

<span class="hljs-keyword">use</span> nickel::Nickel;

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> server = Nickel::new();

    server.utilize(router! {
        get <span class="hljs-string">"**"</span> =&gt; |_req, _res| {
            <span class="hljs-string">"Hello world!"</span>
        }
    });

    server.listen(<span class="hljs-string">"127.0.0.1:6767"</span>).unwrap();
}
</code></pre>
<h4 id="heading-fiber-go-1"><code>Fiber (Go)</code></h4>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> <span class="hljs-string">"github.com/gofiber/fiber/v2"</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    app := fiber.New()

    app.Get(<span class="hljs-string">"/"</span>, <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(c *fiber.Ctx)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-keyword">return</span> c.SendString(<span class="hljs-string">"Hello, World 🐹!"</span>)
    })

    app.Listen(<span class="hljs-string">":3000"</span>)
}
</code></pre>
<h2 id="heading-the-results">The Results</h2>
<h4 id="heading-50-concurrent-users">50 concurrent users</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td><code>Fiber (Go)</code></td><td><code>Nickel.rs (Rust)</code></td></tr>
</thead>
<tbody>
<tr>
<td>Time taken</td><td>140s</td><td>47s</td></tr>
<tr>
<td>Request per second</td><td>35378.09</td><td>106293.29</td></tr>
<tr>
<td>Mean response time</td><td>1.41 ms</td><td>0.39396 ms</td></tr>
<tr>
<td>Median response time</td><td>0.845 ms</td><td>0.049 ms</td></tr>
<tr>
<td>90th percentile</td><td>3.44 ms</td><td>0.110 ms</td></tr>
<tr>
<td>Max response time</td><td>107.07 ms</td><td>33.91 s</td></tr>
<tr>
<td>CPU</td><td>35%</td><td>18%</td></tr>
<tr>
<td>Memory</td><td>11.102 MB</td><td>4 MB</td></tr>
</tbody>
</table>
</div><h4 id="heading-100-concurrent-users">100 concurrent users</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td><code>Fiber (Go)</code></td><td><code>Nickel.rs (Rust)</code></td></tr>
</thead>
<tbody>
<tr>
<td>Time taken</td><td>184s</td><td>51s</td></tr>
<tr>
<td>Request per second</td><td>27073.55</td><td>97200.03</td></tr>
<tr>
<td>Mean response time</td><td>3.69 ms</td><td>0.92 ms</td></tr>
<tr>
<td>Median response time</td><td>2.90 ms</td><td>0.04 ms</td></tr>
<tr>
<td>90th percentile</td><td>7.94 ms</td><td>0.09400 ms</td></tr>
<tr>
<td>Max response time</td><td>136.02 ms</td><td>29.91 s</td></tr>
<tr>
<td>CPU</td><td>35%</td><td>18%</td></tr>
<tr>
<td>Memory</td><td>13 MB</td><td>4 MB</td></tr>
</tbody>
</table>
</div><h4 id="heading-500-concurrent-users">500 concurrent users</h4>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td><code>Fiber (Go)</code></td><td><code>Nickel.rs (Rust)</code></td></tr>
</thead>
<tbody>
<tr>
<td>Time taken</td><td>189s</td><td>1m</td></tr>
<tr>
<td>Request per second</td><td>26359.05</td><td>83084.80</td></tr>
<tr>
<td>Mean response time</td><td>18.97 ms</td><td>5.00 ms</td></tr>
<tr>
<td>Median response time</td><td>18.27 ms</td><td>0.04 ms</td></tr>
<tr>
<td>90th percentile</td><td>31.78 ms</td><td>0.085 ms</td></tr>
<tr>
<td>Max response time</td><td>185.04 ms</td><td>32.25 s</td></tr>
<tr>
<td>CPU</td><td>35%</td><td>17%</td></tr>
<tr>
<td>Memory</td><td>29 MB</td><td>4 MB</td></tr>
</tbody>
</table>
</div><h2 id="heading-conclusion">Conclusion 🎉</h2>
<p>Based on the data provided, <code>Nickel.rs (Rust)</code> is the winner. There are several reasons for this:</p>
<ol>
<li><p>🚀 Faster response times: Nickel.rs (Rust) has lower mean, median, and 90th percentile response times across all levels of concurrent users compared to Fiber (Go).</p>
</li>
<li><p>⚡ Higher request per second: Nickel.rs (Rust) can handle more requests per second than Fiber (Go) in each test.</p>
</li>
<li><p>🌡️ Lower CPU usage: Nickel.rs (Rust) uses less CPU (about half) compared to Fiber (Go) in all tests.</p>
</li>
<li><p>🧠 Lower memory usage: Nickel.rs (Rust) uses significantly less memory (about 3 to 7 times less) compared to Fiber ( Go) in all tests.</p>
</li>
</ol>
<p>In conclusion, 🏆 Rust (specifically <code>Nickel.rs</code>) outperforms Go (<code>Fiber</code>) in terms of response times, request handling, CPU usage, and memory consumption, making it the winner in this comparison.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p>Gopher from cover: <a target="_blank" href="https://dribbble.com/shots/14749679-Golang-gopher-master-karate-ninja-cool-sticker-shirt-print">https://dribbble.com/shots/14749679-Golang-gopher-master-karate-ninja-cool-sticker-shirt-print</a></p>
</li>
<li><p>Rust crab from cover: <a target="_blank" href="https://ih1.redbubble.net/image.1001647428.6797/st,small,507x507-pad,600x600,f8f8f8.jpg">https://ih1.redbubble.net/image.1001647428.6797/st,small,507x507-pad,600x600,f8f8f8.jpg</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Leveraging Pulumi to Incorporate AWS Cognito as an Identity Provider for ArgoCD]]></title><description><![CDATA[TL;DR: Le Code:
https://github.com/dirien/quick-bites/tree/main/pulumi-cognito-gitops-ui/argocd
 
Introduction
In this blog post, I want to show you how to create and use AWS Cognito as an OAuth2 provider for ArgoCD. And this will be all done by usin...]]></description><link>https://blog.ediri.io/leveraging-pulumi-to-incorporate-aws-cognito-as-an-identity-provider-for-argocd</link><guid isPermaLink="true">https://blog.ediri.io/leveraging-pulumi-to-incorporate-aws-cognito-as-an-identity-provider-for-argocd</guid><category><![CDATA[Pulumi]]></category><category><![CDATA[AWS]]></category><category><![CDATA[ArgoCD]]></category><category><![CDATA[gitops]]></category><category><![CDATA[oauth]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Wed, 05 Apr 2023 20:33:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1680726712561/fe0a49e1-e608-4d1d-b386-fced0d148175.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-le-code">TL;DR: Le Code:</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/pulumi-cognito-gitops-ui/argocd">https://github.com/dirien/quick-bites/tree/main/pulumi-cognito-gitops-ui/argocd</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>In this blog post, I want to show you how to create and use <a target="_blank" href="https://aws.amazon.com/cognito/">AWS Cognito</a> as an OAuth2 provider for ArgoCD. And this will be all done by using <a target="_blank" href="https://www.pulumi.com/">Pulumi</a>. This is very convenient, as you can do not need any manual steps to configure the ArgCD deployment with properties of the Cognito service. The Pulumi code will do all the work for you.</p>
<p>The demo infrastructure will be deployed to AWS and will look like this:</p>
<ul>
<li><p>AWS EKS Cluster</p>
</li>
<li><p>AWS Cognito User Pool with a Client and Cognito User.</p>
</li>
<li><p>AWS Load Balancer Controller</p>
</li>
<li><p>External DNS (as I host my domains on <a target="_blank" href="https://www.digitalocean.com/">DigitalOcean</a>)</p>
</li>
<li><p>ArgoCD</p>
</li>
</ul>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/kubernetes-sigs/external-dns">https://github.com/kubernetes-sigs/external-dns</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/argoproj/argo-cd/">https://github.com/argoproj/argo-cd/</a></div>
<p> </p>
<p>So let's get started!</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow this article, you will need the following:</p>
<ul>
<li><p><a target="_blank" href="https://www.pulumi.com/docs/get-started/install/">Pulumi CLI</a> installed.</p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">kubectl</a> installed.</p>
</li>
<li><p>optional <a target="_blank" href="https://k9scli.io/topics/install/">K9s</a>, if you want to quickly interact with your cluster.</p>
</li>
<li><p>AWS: <a target="_blank" href="https://aws.amazon.com/">AWS account</a></p>
</li>
</ul>
<p>I will not go into detail on how to install the prerequisites. If you need help, please refer to the links above.</p>
<h2 id="heading-creating-the-infrastructure-with-pulumi">Creating the infrastructure with Pulumi</h2>
<h3 id="heading-create-your-pulumi-project">Create your Pulumi project</h3>
<p><code>Pulumi</code> is a multi-language infrastructure as Code tool using imperative languages to create a declarative infrastructure description.</p>
<p>You have a wide range of programming languages available, and you can use the one you and your team are the most comfortable with. Currently, (11/2022) <code>Pulumi</code> supports the following languages:</p>
<ul>
<li><p>Node.js (JavaScript / TypeScript)</p>
</li>
<li><p>Python</p>
</li>
<li><p>Go</p>
</li>
<li><p>Java</p>
</li>
<li><p>.NET (C#, VB, F#)</p>
</li>
<li><p>YAML</p>
</li>
</ul>
<p>In this article, we will use <code>Go</code> as our programming language. You can of course use any other language supported by <code>Pulumi</code>.</p>
<p>Create a project folder (for example <code>pulumi-cognito-argocd</code>) and navigate into the newly created directory:</p>
<pre><code class="lang-bash">mkdir pulumi-cognito-argocd
<span class="hljs-built_in">cd</span> pulumi-cognito-argocd
</code></pre>
<p>Now, we need to initialize our project. We will use the <code>pulumi new</code> command to do this. This command will create a new Pulumi project in the current directory. We will use the <code>aws-go</code> template, which will create a new project with a <code>Go</code> template for AWS.</p>
<pre><code class="lang-bash">pulumi new aws-go --force
</code></pre>
<p>You can leave the default values in the prompt but maybe adjust the AWS region to your preference. I chose <code>eu-central-1</code> as my region for this demo.</p>
<p>As we're going to deploy several Helm charts, we need to install the <code>pulumi-kubernetes</code> provider. To install providers, we need to add the following go libarie to our <code>go.mod</code> file:</p>
<pre><code class="lang-bash">go get github.com/pulumi/pulumi-kubernetes/sdk/v3
</code></pre>
<p>And, as I don't want to create an EKS cluster from scratch, we will use the <code>pulumi-eks</code> provider to create the EKS cluster for us. We can configure the EKS cluster with some convenient options, like the Kubernetes version, OIDC support and more.</p>
<pre><code class="lang-bash">go get github.com/pulumi/pulumi-eks/sdk
</code></pre>
<h3 id="heading-creating-the-network-infrastructure">Creating the network infrastructure</h3>
<p>With all libraries installed, we can start to create our infrastructure. First, we need to create the network infrastructure. To do this, head over to the <code>main.go</code> file and add the following code:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        vpc, err := ec2.NewVpc(ctx, <span class="hljs-string">"cognito-argocd-vpc"</span>, &amp;ec2.VpcArgs{
            CidrBlock: pulumi.String(<span class="hljs-string">"172.31.0.0/16"</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        igw, err := ec2.NewInternetGateway(ctx, <span class="hljs-string">"cognito-argocd-igw"</span>, &amp;ec2.InternetGatewayArgs{
            VpcId: vpc.ID(),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        rt, err := ec2.NewRouteTable(ctx, <span class="hljs-string">"cognito-argocd-rt"</span>, &amp;ec2.RouteTableArgs{
            VpcId: vpc.ID(),
            Routes: ec2.RouteTableRouteArray{
                &amp;ec2.RouteTableRouteArgs{
                    CidrBlock: pulumi.String(<span class="hljs-string">"0.0.0.0/0"</span>),
                    GatewayId: igw.ID(),
                },
            },
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        <span class="hljs-keyword">var</span> publicSubnetIDs pulumi.StringArray

        <span class="hljs-comment">// Create a subnet for each availability zone</span>
        <span class="hljs-keyword">for</span> i, az := <span class="hljs-keyword">range</span> availabilityZones {
            publicSubnet, err := ec2.NewSubnet(ctx, fmt.Sprintf(<span class="hljs-string">"cognito-argocd-subnet-%d"</span>, i), &amp;ec2.SubnetArgs{
                VpcId:                       vpc.ID(),
                CidrBlock:                   pulumi.String(publicSubnetCidrs[i]),
                MapPublicIpOnLaunch:         pulumi.Bool(<span class="hljs-literal">true</span>),
                AssignIpv6AddressOnCreation: pulumi.Bool(<span class="hljs-literal">false</span>),
                AvailabilityZone:            pulumi.String(az),
                Tags: pulumi.StringMap{
                    <span class="hljs-string">"Name"</span>:                   pulumi.Sprintf(<span class="hljs-string">"eks-public-subnet-%d"</span>, az),
                    clusterTag:               pulumi.String(<span class="hljs-string">"owned"</span>),
                    <span class="hljs-string">"kubernetes.io/role/elb"</span>: pulumi.String(<span class="hljs-string">"1"</span>),
                },
            })
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">return</span> err
            }
            _, err = ec2.NewRouteTableAssociation(ctx, fmt.Sprintf(<span class="hljs-string">"cognito-argocd-rt-association-%s"</span>, az), &amp;ec2.RouteTableAssociationArgs{
                RouteTableId: rt.ID(),
                SubnetId:     publicSubnet.ID(),
            })
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">return</span> err
            }
            publicSubnetIDs = <span class="hljs-built_in">append</span>(publicSubnetIDs, publicSubnet.ID())
        }

        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>This code will create a new VPC with a public subnet in each availability zone. An internet gateway and a route table will be created as well. The route table will be associated with the public subnets.</p>
<h3 id="heading-creating-the-aws-cognito-infrastructure">Creating the AWS Cognito infrastructure</h3>
<p>With the network infrastructure created, we can work on the creation of the AWS Cognito infrastructure. Add the following code to the <code>main.go</code> file:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitted for brevity</span>
        userPool, err := cognito.NewUserPool(ctx, <span class="hljs-string">"cognito-argocd-user-pool"</span>, &amp;cognito.UserPoolArgs{
            AliasAttributes: pulumi.StringArray{
                pulumi.String(<span class="hljs-string">"email"</span>),
                pulumi.String(<span class="hljs-string">"preferred_username"</span>),
            },
            AutoVerifiedAttributes: pulumi.StringArray{
                pulumi.String(<span class="hljs-string">"email"</span>),
            },
            Schemas: cognito.UserPoolSchemaArray{
                &amp;cognito.UserPoolSchemaArgs{
                    AttributeDataType:      pulumi.String(<span class="hljs-string">"String"</span>),
                    DeveloperOnlyAttribute: pulumi.Bool(<span class="hljs-literal">false</span>),
                    Mutable:                pulumi.Bool(<span class="hljs-literal">true</span>),
                    Name:                   pulumi.String(<span class="hljs-string">"name"</span>),
                    Required:               pulumi.Bool(<span class="hljs-literal">true</span>),
                    StringAttributeConstraints: &amp;cognito.UserPoolSchemaStringAttributeConstraintsArgs{
                        MinLength: pulumi.String(<span class="hljs-string">"3"</span>),
                        MaxLength: pulumi.String(<span class="hljs-string">"70"</span>),
                    },
                },
                &amp;cognito.UserPoolSchemaArgs{
                    AttributeDataType:      pulumi.String(<span class="hljs-string">"String"</span>),
                    DeveloperOnlyAttribute: pulumi.Bool(<span class="hljs-literal">false</span>),
                    Mutable:                pulumi.Bool(<span class="hljs-literal">true</span>),
                    Name:                   pulumi.String(<span class="hljs-string">"email"</span>),
                    Required:               pulumi.Bool(<span class="hljs-literal">true</span>),
                    StringAttributeConstraints: &amp;cognito.UserPoolSchemaStringAttributeConstraintsArgs{
                        MinLength: pulumi.String(<span class="hljs-string">"3"</span>),
                        MaxLength: pulumi.String(<span class="hljs-string">"70"</span>),
                    },
                },
            },
            AdminCreateUserConfig: &amp;cognito.UserPoolAdminCreateUserConfigArgs{
                AllowAdminCreateUserOnly: pulumi.Bool(<span class="hljs-literal">true</span>),
            },
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        _, err = cognito.NewUser(ctx, <span class="hljs-string">"cognito-argocd-admin"</span>, &amp;cognito.UserArgs{
            UserPoolId:        userPool.ID(),
            Username:          pulumi.String(<span class="hljs-string">"admin"</span>),
            TemporaryPassword: pulumi.String(<span class="hljs-string">"Admin123!"</span>),
            Attributes: pulumi.StringMap{
                <span class="hljs-string">"email"</span>:          pulumi.String(adminUserEmail),
                <span class="hljs-string">"email_verified"</span>: pulumi.String(<span class="hljs-string">"true"</span>),
            },
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        _, err = cognito.NewUserPoolDomain(ctx, <span class="hljs-string">"cognito-argocd-user-pool-domain"</span>, &amp;cognito.UserPoolDomainArgs{
            Domain:     pulumi.String(<span class="hljs-string">"argocd-ui"</span>),
            UserPoolId: userPool.ID(),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        userPoolClient, err := cognito.NewUserPoolClient(ctx, <span class="hljs-string">"cognito-argocd-user-pool-client"</span>, &amp;cognito.UserPoolClientArgs{
            UserPoolId: userPool.ID(),
            AllowedOauthFlows: pulumi.StringArray{
                pulumi.String(<span class="hljs-string">"code"</span>),
                pulumi.String(<span class="hljs-string">"implicit"</span>),
            },
            AllowedOauthFlowsUserPoolClient: pulumi.Bool(<span class="hljs-literal">true</span>),
            AllowedOauthScopes: pulumi.StringArray{
                pulumi.String(<span class="hljs-string">"openid"</span>),
                pulumi.String(<span class="hljs-string">"email"</span>),
                pulumi.String(<span class="hljs-string">"profile"</span>),
            },
            SupportedIdentityProviders: pulumi.StringArray{
                pulumi.String(<span class="hljs-string">"COGNITO"</span>),
            },
            GenerateSecret: pulumi.Bool(<span class="hljs-literal">true</span>),
            CallbackUrls: pulumi.StringArray{
                pulumi.String(<span class="hljs-string">"https://argocd.ediri.online/auth/callback"</span>),
            },
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>This code will create the AWS Cognito user pool, an admin user, and a user pool client. Some important things to note here:</p>
<ul>
<li><p>The <code>adminUser</code> resource is created with a temporary password. This will be used to log in for the first time to the Argo CD UI. You will need to change the password after the first login.</p>
</li>
<li><p>The domain name for the Argo CD UI is set to <code>argocd-ui</code>. This is a must to get OAuth2 working with Argo CD. You will need to change this to match your domain name or you can use one that is provided by AWS Cognito, which will be in the format <code>https://&lt;domain_name&gt;.auth.&lt;region&gt;.amazoncognito.com</code>.</p>
</li>
<li><p>The <code>userPoolClient</code> resource is created with a callback URL. This is the URL that the user will be redirected to after they have authenticated with AWS Cognito. This URL will be used by the Argo CD UI to authenticate the user. The URL is set to <code>https://argocd.ediri.online/auth/callback</code> in this example, but you will need to change it to match your domain name.</p>
</li>
</ul>
<p>When we later create the whole infrastructure, you will end up with a whole lot of endpoints:</p>
<ul>
<li><p>The AWS Cognito Auth endpoint: <code>https://&lt;domain_name&gt;.auth.&lt;region&gt;.amazoncognito.com/oauth2/authorize</code></p>
</li>
<li><p>The AWS Cognito Token endpoint: <code>https://&lt;domain_name&gt;.auth.&lt;region&gt;.amazoncognito.com/oauth2/token</code></p>
</li>
<li><p>The AWS Cognito User info endpoint: <code>https://&lt;domain_name&gt;.auth.&lt;region&gt;.amazoncognito.com/oauth2/userInfo</code></p>
</li>
<li><p>The AWS Cognito End session endpoint: <code>https://&lt;domain_name&gt;.auth.&lt;region&gt;.amazoncognito.com/logout</code></p>
</li>
<li><p>The AWS Cognito JWKS endpoint: <code>https://&lt;domain_name&gt;.auth.&lt;region&gt;.amazoncognito.com/.well-known/jwks.json</code></p>
</li>
<li><p>The AWS Cognito Issuer: <code>https://cognito-idp.&lt;region&gt;.amazonaws.com/&lt;user_pool_id&gt;</code></p>
</li>
</ul>
<p>The issuer, we will use later when we create the Argo CD configuration.</p>
<h3 id="heading-create-the-eks-cluster">Create the EKS cluster</h3>
<p>After creating the network infrastructure and the AWS Cognito infrastructure, we can now create the EKS cluster. Add the following code to the <code>main.go</code> file:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitted for brevity</span>
        cluster, err := eks.NewCluster(ctx, clusterName, &amp;eks.ClusterArgs{
            Name:            pulumi.String(clusterName),
            VpcId:           vpc.ID(),
            SubnetIds:       publicSubnetIDs,
            InstanceType:    pulumi.String(<span class="hljs-string">"t3.medium"</span>),
            DesiredCapacity: pulumi.Int(<span class="hljs-number">2</span>),
            MinSize:         pulumi.Int(<span class="hljs-number">1</span>),
            MaxSize:         pulumi.Int(<span class="hljs-number">3</span>),
            ProviderCredentialOpts: eks.KubeconfigOptionsArgs{
                ProfileName: pulumi.String(<span class="hljs-string">"default"</span>),
            },
            CreateOidcProvider: pulumi.Bool(<span class="hljs-literal">true</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        ctx.Export(<span class="hljs-string">"kubeconfig"</span>, pulumi.ToSecret(cluster.Kubeconfig))

        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>That's it! This very short code will create a full-blown EKS cluster with 2 worker nodes. The <code>kubeconfig</code> output will be made available as a secret in the Pulumi stack. We will use this later to connect to the cluster via <code>kubectl</code> or <code>k9s</code>.</p>
<p>But now comes the tricky part! We need to deploy several services to the cluster, to get Argo CD up and running. These services are:</p>
<ul>
<li><p>AWS Load Balancer Controller</p>
</li>
<li><p>External DNS</p>
</li>
<li><p>Argo CD</p>
</li>
</ul>
<blockquote>
<p>For the TLS certificate, we will use the AWS Certificate Manager. I created everything beforehand, and this will be not covered in this post. In short, I created a CNAME record and my DNS provider, and the AWS Certificate Manager created a certificate for me. I use the <code>arn</code> of the certificate in the annotations of the Argo CD ingress.</p>
</blockquote>
<h3 id="heading-deploy-argo-cd">Deploy Argo CD</h3>
<p>We reach the final part of this blog post! Add this code to the <code>main.go</code> file:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitted for brevity</span>
        albRole, err := iam.NewRole(ctx, <span class="hljs-string">"cognito-argocd-alb-role"</span>, &amp;iam.RoleArgs{
            AssumeRolePolicy: pulumi.All(cluster.Core.OidcProvider().Arn(), cluster.Core.OidcProvider().Url()).ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args []<span class="hljs-keyword">interface</span>{})</span> <span class="hljs-params">(<span class="hljs-keyword">string</span>, error)</span></span> {
                arn := args[<span class="hljs-number">0</span>].(<span class="hljs-keyword">string</span>)
                url := args[<span class="hljs-number">1</span>].(<span class="hljs-keyword">string</span>)
                <span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">`{
                        "Version": "2012-10-17",
                        "Statement": [
                            {
                                "Effect": "Allow",
                                "Principal": {
                                    "Federated": "%s"
                                },
                                "Action": "sts:AssumeRoleWithWebIdentity",
                                "Condition": {
                                    "StringEquals": {
                                        "%s:sub": "%s"
                                    }
                                }
                            }
                        ]
                    }`</span>, arn, url, albServiceAccount), <span class="hljs-literal">nil</span>
            }).(pulumi.StringOutput),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        albPolicyFile, _ := os.ReadFile(<span class="hljs-string">"../policies/alb-iam-policy.json"</span>)
        albIAMPolicy, err := iam.NewPolicy(ctx, <span class="hljs-string">"cognito-argocd-alb-policy"</span>, &amp;iam.PolicyArgs{
            Policy: pulumi.String(<span class="hljs-keyword">string</span>(albPolicyFile)),
        }, pulumi.DependsOn([]pulumi.Resource{albRole}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        _, err = iam.NewRolePolicyAttachment(ctx, <span class="hljs-string">"cognito-argocd-alb-role-attachment"</span>, &amp;iam.RolePolicyAttachmentArgs{
            PolicyArn: albIAMPolicy.Arn,
            Role:      albRole.Name,
        }, pulumi.DependsOn([]pulumi.Resource{albIAMPolicy}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        provider, err := kubernetes.NewProvider(ctx, <span class="hljs-string">"my-provider"</span>, &amp;kubernetes.ProviderArgs{
            Kubeconfig:            cluster.KubeconfigJson,
            EnableServerSideApply: pulumi.Bool(<span class="hljs-literal">true</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        ns, err := corev1.NewNamespace(ctx, albNamespace, &amp;corev1.NamespaceArgs{
            Metadata: &amp;metav1.ObjectMetaArgs{
                Name: pulumi.String(albNamespace),
                Labels: pulumi.StringMap{
                    <span class="hljs-string">"app.kubernetes.io/name"</span>: pulumi.String(<span class="hljs-string">"aws-load-balancer-controller"</span>),
                },
            },
        }, pulumi.Provider(provider))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        sa, err := corev1.NewServiceAccount(ctx, <span class="hljs-string">"aws-lb-controller-sa"</span>, &amp;corev1.ServiceAccountArgs{
            Metadata: &amp;metav1.ObjectMetaArgs{
                Name:      pulumi.String(<span class="hljs-string">"aws-lb-controller-serviceaccount"</span>),
                Namespace: ns.Metadata.Name(),
                Annotations: pulumi.StringMap{
                    <span class="hljs-string">"eks.amazonaws.com/role-arn"</span>: albRole.Arn,
                },
            },
        }, pulumi.Provider(provider), pulumi.DependsOn([]pulumi.Resource{ns}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        _, err = helm.NewRelease(ctx, <span class="hljs-string">"aws-load-balancer-controller"</span>, &amp;helm.ReleaseArgs{
            Chart:     pulumi.String(<span class="hljs-string">"aws-load-balancer-controller"</span>),
            Version:   pulumi.String(<span class="hljs-string">"1.4.8"</span>),
            Namespace: ns.Metadata.Name(),
            RepositoryOpts: helm.RepositoryOptsArgs{
                Repo: pulumi.String(<span class="hljs-string">"https://aws.github.io/eks-charts"</span>),
            },
            Values: pulumi.Map{
                <span class="hljs-string">"clusterName"</span>: cluster.EksCluster.ToClusterOutput().Name(),
                <span class="hljs-string">"region"</span>:      pulumi.String(<span class="hljs-string">"eu-central-1"</span>),
                <span class="hljs-string">"serviceAccount"</span>: pulumi.Map{
                    <span class="hljs-string">"create"</span>: pulumi.Bool(<span class="hljs-literal">false</span>),
                    <span class="hljs-string">"name"</span>:   sa.Metadata.Name(),
                },
                <span class="hljs-string">"vpcId"</span>: cluster.EksCluster.VpcConfig().VpcId(),
                <span class="hljs-string">"podLabels"</span>: pulumi.Map{
                    <span class="hljs-string">"stack"</span>: pulumi.String(<span class="hljs-string">"eks"</span>),
                    <span class="hljs-string">"app"</span>:   pulumi.String(<span class="hljs-string">"aws-lb-controller"</span>),
                },
            },
        }, pulumi.Provider(provider), pulumi.DependsOn([]pulumi.Resource{ns, sa}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        externalDNS, err := helm.NewRelease(ctx, <span class="hljs-string">"external-dns"</span>, &amp;helm.ReleaseArgs{
            Chart:           pulumi.String(<span class="hljs-string">"external-dns"</span>),
            Version:         pulumi.String(<span class="hljs-string">"1.12.2"</span>),
            Namespace:       pulumi.String(<span class="hljs-string">"external-dns"</span>),
            CreateNamespace: pulumi.Bool(<span class="hljs-literal">true</span>),
            RepositoryOpts: helm.RepositoryOptsArgs{
                Repo: pulumi.String(<span class="hljs-string">"https://kubernetes-sigs.github.io/external-dns/"</span>),
            },
            Values: pulumi.Map{
                <span class="hljs-string">"provider"</span>: pulumi.String(<span class="hljs-string">"digitalocean"</span>),
                <span class="hljs-string">"sources"</span>: pulumi.Array{
                    pulumi.String(<span class="hljs-string">"ingress"</span>),
                },
                <span class="hljs-string">"env"</span>: pulumi.Array{
                    pulumi.Map{
                        <span class="hljs-string">"name"</span>:  pulumi.String(<span class="hljs-string">"DO_TOKEN"</span>),
                        <span class="hljs-string">"value"</span>: config.GetSecret(ctx, <span class="hljs-string">"do"</span>),
                    },
                },
            },
        }, pulumi.Provider(provider))

        _, err = helm.NewRelease(ctx, <span class="hljs-string">"argocd"</span>, &amp;helm.ReleaseArgs{
            Chart:           pulumi.String(<span class="hljs-string">"argo-cd"</span>),
            Version:         pulumi.String(<span class="hljs-string">"5.28.0"</span>),
            Namespace:       pulumi.String(<span class="hljs-string">"argocd"</span>),
            CreateNamespace: pulumi.Bool(<span class="hljs-literal">true</span>),
            RepositoryOpts: helm.RepositoryOptsArgs{
                Repo: pulumi.String(<span class="hljs-string">"https://argoproj.github.io/argo-helm"</span>),
            },
            Values: pulumi.Map{
                <span class="hljs-string">"dex"</span>: pulumi.Map{
                    <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">false</span>),
                },
                <span class="hljs-string">"server"</span>: pulumi.Map{
                    <span class="hljs-string">"extraArgs"</span>: pulumi.Array{
                        pulumi.String(<span class="hljs-string">"--insecure"</span>),
                        pulumi.String(<span class="hljs-string">"--enable-gzip"</span>),
                    },
                    <span class="hljs-string">"ingress"</span>: pulumi.Map{
                        <span class="hljs-string">"enabled"</span>:          pulumi.Bool(<span class="hljs-literal">true</span>),
                        <span class="hljs-string">"hosts"</span>:            pulumi.Array{pulumi.String(<span class="hljs-string">"argocd.ediri.online"</span>)},
                        <span class="hljs-string">"ingressClassName"</span>: pulumi.String(<span class="hljs-string">"alb"</span>),
                        <span class="hljs-string">"annotations"</span>: pulumi.Map{
                            <span class="hljs-string">"alb.ingress.kubernetes.io/target-type"</span>:        pulumi.String(<span class="hljs-string">"ip"</span>),
                            <span class="hljs-string">"alb.ingress.kubernetes.io/scheme"</span>:             pulumi.String(<span class="hljs-string">"internet-facing"</span>),
                            <span class="hljs-string">"alb.ingress.kubernetes.io/load-balancer-name"</span>: pulumi.String(<span class="hljs-string">"argocd"</span>),
                            <span class="hljs-string">"alb.ingress.kubernetes.io/certificate-arn"</span>:    config.GetSecret(ctx, <span class="hljs-string">"cert_arn"</span>),
                        },
                    },
                },
                <span class="hljs-string">"configs"</span>: pulumi.Map{
                    <span class="hljs-string">"rbac"</span>: pulumi.Map{
                        <span class="hljs-string">"policy.default"</span>: pulumi.String(<span class="hljs-string">"role:readonly"</span>),
                        <span class="hljs-string">"policy.csv"</span>:     pulumi.Sprintf(<span class="hljs-string">`g, %s, role:admin`</span>, adminUserEmail),
                        <span class="hljs-string">"scopes"</span>:         pulumi.String(<span class="hljs-string">`[email]`</span>),
                    },
                    <span class="hljs-string">"cm"</span>: pulumi.Map{
                        <span class="hljs-string">"admin.enabled"</span>: pulumi.Bool(<span class="hljs-literal">false</span>),
                        <span class="hljs-string">"url"</span>:           pulumi.String(<span class="hljs-string">"https://argocd.ediri.online"</span>),
                        <span class="hljs-string">"oidc.config"</span>: pulumi.Sprintf(<span class="hljs-string">`name: Cognito
issuer: https://cognito-idp.eu-central-1.amazonaws.com/%s
clientID: %s
clientSecret: %s
requestedScopes: ["openid", "profile", "email"]
requestedIDTokenClaims: {"email": {"essential": true}}`</span>, userPool.ID(), userPoolClient.ID(), userPoolClient.ClientSecret),
                    },
                },
            },
        }, pulumi.Provider(provider), pulumi.DependsOn([]pulumi.Resource{ns, externalDNS}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>There is a lot, and I mean really a lot going on here. I will try to explain it as best as I can. Let's start with the ALB Controller part:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    albRole, err := iam.NewRole(ctx, <span class="hljs-string">"cognito-argocd-alb-role"</span>, &amp;iam.RoleArgs{
        AssumeRolePolicy: pulumi.All(cluster.Core.OidcProvider().Arn(), cluster.Core.OidcProvider().Url()).ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args []<span class="hljs-keyword">interface</span>{})</span> <span class="hljs-params">(<span class="hljs-keyword">string</span>, error)</span></span> {
            arn := args[<span class="hljs-number">0</span>].(<span class="hljs-keyword">string</span>)
            url := args[<span class="hljs-number">1</span>].(<span class="hljs-keyword">string</span>)
            <span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">`{
                        "Version": "2012-10-17",
                        "Statement": [
                            {
                                "Effect": "Allow",
                                "Principal": {
                                    "Federated": "%s"
                                },
                                "Action": "sts:AssumeRoleWithWebIdentity",
                                "Condition": {
                                    "StringEquals": {
                                        "%s:sub": "%s"
                                    }
                                }
                            }
                        ]
                    }`</span>, arn, url, albServiceAccount), <span class="hljs-literal">nil</span>
        }).(pulumi.StringOutput),
    })
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    albPolicyFile, _ := os.ReadFile(<span class="hljs-string">"../policies/alb-iam-policy.json"</span>)
    albIAMPolicy, err := iam.NewPolicy(ctx, <span class="hljs-string">"cognito-argocd-alb-policy"</span>, &amp;iam.PolicyArgs{
        Policy: pulumi.String(<span class="hljs-keyword">string</span>(albPolicyFile)),
    }, pulumi.DependsOn([]pulumi.Resource{albRole}))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    _, err = iam.NewRolePolicyAttachment(ctx, <span class="hljs-string">"cognito-argocd-alb-role-attachment"</span>, &amp;iam.RolePolicyAttachmentArgs{
        PolicyArn: albIAMPolicy.Arn,
        Role:      albRole.Name,
    }, pulumi.DependsOn([]pulumi.Resource{albIAMPolicy}))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
}
</code></pre>
<p>We define the role that will be assumed by the ALB Controller. The <code>AssumeRolePolicy</code> field specifies the trust policy for the role, which allows the ALB to assume the role using the OpenID Connect (OIDC) provider for the EKS cluster.</p>
<p>The code then creates an AWS IAM policy for the ALB to use with the <code>iam.NewPolicy</code> function. The Policy field specifies the contents of the policy, which is read from a JSON file using the <code>os.ReadFile</code> function. The role is then attached to the policy using the <code>iam.NewRolePolicyAttachment</code> function.</p>
<p>Next, we create the ALB Controller itself:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// omitted for brevity</span>
    provider, err := kubernetes.NewProvider(ctx, <span class="hljs-string">"my-provider"</span>, &amp;kubernetes.ProviderArgs{
        Kubeconfig:            cluster.KubeconfigJson,
        EnableServerSideApply: pulumi.Bool(<span class="hljs-literal">true</span>),
    })
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    ns, err := corev1.NewNamespace(ctx, albNamespace, &amp;corev1.NamespaceArgs{
        Metadata: &amp;metav1.ObjectMetaArgs{
            Name: pulumi.String(albNamespace),
            Labels: pulumi.StringMap{
                <span class="hljs-string">"app.kubernetes.io/name"</span>: pulumi.String(<span class="hljs-string">"aws-load-balancer-controller"</span>),
            },
        },
    }, pulumi.Provider(provider))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    sa, err := corev1.NewServiceAccount(ctx, <span class="hljs-string">"aws-lb-controller-sa"</span>, &amp;corev1.ServiceAccountArgs{
        Metadata: &amp;metav1.ObjectMetaArgs{
            Name:      pulumi.String(<span class="hljs-string">"aws-lb-controller-serviceaccount"</span>),
            Namespace: ns.Metadata.Name(),
            Annotations: pulumi.StringMap{
                <span class="hljs-string">"eks.amazonaws.com/role-arn"</span>: albRole.Arn,
            },
        },
    }, pulumi.Provider(provider), pulumi.DependsOn([]pulumi.Resource{ns}))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }

    _, err = helm.NewRelease(ctx, <span class="hljs-string">"aws-load-balancer-controller"</span>, &amp;helm.ReleaseArgs{
        Chart:     pulumi.String(<span class="hljs-string">"aws-load-balancer-controller"</span>),
        Version:   pulumi.String(<span class="hljs-string">"1.4.8"</span>),
        Namespace: ns.Metadata.Name(),
        RepositoryOpts: helm.RepositoryOptsArgs{
            Repo: pulumi.String(<span class="hljs-string">"https://aws.github.io/eks-charts"</span>),
        },
        Values: pulumi.Map{
            <span class="hljs-string">"clusterName"</span>: cluster.EksCluster.ToClusterOutput().Name(),
            <span class="hljs-string">"region"</span>:      pulumi.String(<span class="hljs-string">"eu-central-1"</span>),
            <span class="hljs-string">"serviceAccount"</span>: pulumi.Map{
                <span class="hljs-string">"create"</span>: pulumi.Bool(<span class="hljs-literal">false</span>),
                <span class="hljs-string">"name"</span>:   sa.Metadata.Name(),
            },
            <span class="hljs-string">"vpcId"</span>: cluster.EksCluster.VpcConfig().VpcId(),
            <span class="hljs-string">"podLabels"</span>: pulumi.Map{
                <span class="hljs-string">"stack"</span>: pulumi.String(<span class="hljs-string">"eks"</span>),
                <span class="hljs-string">"app"</span>:   pulumi.String(<span class="hljs-string">"aws-lb-controller"</span>),
            },
        },
    }, pulumi.Provider(provider), pulumi.DependsOn([]pulumi.Resource{ns, sa}))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    externalDNS, err := helm.NewRelease(ctx, <span class="hljs-string">"external-dns"</span>, &amp;helm.ReleaseArgs{
        Chart:           pulumi.String(<span class="hljs-string">"external-dns"</span>),
        Version:         pulumi.String(<span class="hljs-string">"1.12.2"</span>),
        Namespace:       pulumi.String(<span class="hljs-string">"external-dns"</span>),
        CreateNamespace: pulumi.Bool(<span class="hljs-literal">true</span>),
        RepositoryOpts: helm.RepositoryOptsArgs{
            Repo: pulumi.String(<span class="hljs-string">"https://kubernetes-sigs.github.io/external-dns/"</span>),
        },
        Values: pulumi.Map{
            <span class="hljs-string">"provider"</span>: pulumi.String(<span class="hljs-string">"digitalocean"</span>),
            <span class="hljs-string">"sources"</span>: pulumi.Array{
                pulumi.String(<span class="hljs-string">"ingress"</span>),
            },
            <span class="hljs-string">"env"</span>: pulumi.Array{
                pulumi.Map{
                    <span class="hljs-string">"name"</span>:  pulumi.String(<span class="hljs-string">"DO_TOKEN"</span>),
                    <span class="hljs-string">"value"</span>: config.GetSecret(ctx, <span class="hljs-string">"do"</span>),
                },
            },
        },
    }, pulumi.Provider(provider))
}
</code></pre>
<p>The <code>kubernetes.NewProvider()</code> function is creating a new provider that will manage Kubernetes resources, we are using the server-side apply feature.</p>
<p>We then create a new namespace for the ALB Controller using the <code>corev1.NewNamespace</code> function. Next, we create a new service account for the ALB Controller using the <code>corev1.NewServiceAccount</code> function. The <code>Annotations</code> field specifies the ARN of the role that the ALB Controller will assume.</p>
<p>Finally, we create the ALB Controller using the <code>helm.NewRelease</code> function. Important to note here is that we are passing the already created service account as a value to the <code>serviceAccount.name</code> field. This is important because the ALB Controller will use the service account to assume the role that we created earlier.</p>
<p>The <code>helm.NewRelease</code> function is also used to create the External DNS controller. This is really because of me, as I want that the address of the ingress will be added as an A record in my DigitalOcean DNS. You can skip this part if you do use for example AWS Route53.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitted for brevity</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// omitted for brevity</span>
    _, err = helm.NewRelease(ctx, <span class="hljs-string">"argocd"</span>, &amp;helm.ReleaseArgs{
        Chart:           pulumi.String(<span class="hljs-string">"argo-cd"</span>),
        Version:         pulumi.String(<span class="hljs-string">"5.28.0"</span>),
        Namespace:       pulumi.String(<span class="hljs-string">"argocd"</span>),
        CreateNamespace: pulumi.Bool(<span class="hljs-literal">true</span>),
        RepositoryOpts: helm.RepositoryOptsArgs{
            Repo: pulumi.String(<span class="hljs-string">"https://argoproj.github.io/argo-helm"</span>),
        },
        Values: pulumi.Map{
            <span class="hljs-string">"dex"</span>: pulumi.Map{
                <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">false</span>),
            },
            <span class="hljs-string">"server"</span>: pulumi.Map{
                <span class="hljs-string">"extraArgs"</span>: pulumi.Array{
                    pulumi.String(<span class="hljs-string">"--insecure"</span>),
                    pulumi.String(<span class="hljs-string">"--enable-gzip"</span>),
                },
                <span class="hljs-string">"ingress"</span>: pulumi.Map{
                    <span class="hljs-string">"enabled"</span>:          pulumi.Bool(<span class="hljs-literal">true</span>),
                    <span class="hljs-string">"hosts"</span>:            pulumi.Array{pulumi.String(<span class="hljs-string">"argocd.ediri.online"</span>)},
                    <span class="hljs-string">"ingressClassName"</span>: pulumi.String(<span class="hljs-string">"alb"</span>),
                    <span class="hljs-string">"annotations"</span>: pulumi.Map{
                        <span class="hljs-string">"alb.ingress.kubernetes.io/target-type"</span>:        pulumi.String(<span class="hljs-string">"ip"</span>),
                        <span class="hljs-string">"alb.ingress.kubernetes.io/scheme"</span>:             pulumi.String(<span class="hljs-string">"internet-facing"</span>),
                        <span class="hljs-string">"alb.ingress.kubernetes.io/load-balancer-name"</span>: pulumi.String(<span class="hljs-string">"argocd"</span>),
                        <span class="hljs-string">"alb.ingress.kubernetes.io/certificate-arn"</span>:    config.GetSecret(ctx, <span class="hljs-string">"cert_arn"</span>),
                    },
                },
            },
            <span class="hljs-string">"configs"</span>: pulumi.Map{
                <span class="hljs-string">"rbac"</span>: pulumi.Map{
                    <span class="hljs-string">"policy.default"</span>: pulumi.String(<span class="hljs-string">"role:readonly"</span>),
                    <span class="hljs-string">"policy.csv"</span>:     pulumi.Sprintf(<span class="hljs-string">`g, %s, role:admin`</span>, adminUserEmail),
                    <span class="hljs-string">"scopes"</span>:         pulumi.String(<span class="hljs-string">`[email]`</span>),
                },
                <span class="hljs-string">"cm"</span>: pulumi.Map{
                    <span class="hljs-string">"admin.enabled"</span>: pulumi.Bool(<span class="hljs-literal">false</span>),
                    <span class="hljs-string">"url"</span>:           pulumi.String(<span class="hljs-string">"https://argocd.ediri.online"</span>),
                    <span class="hljs-string">"oidc.config"</span>: pulumi.Sprintf(<span class="hljs-string">`name: Cognito
issuer: https://cognito-idp.eu-central-1.amazonaws.com/%s
clientID: %s
clientSecret: %s
requestedScopes: ["openid", "profile", "email"]
requestedIDTokenClaims: {"email": {"essential": true}}`</span>, userPool.ID(), userPoolClient.ID(), userPoolClient.ClientSecret),
                },
            },
        },
    }, pulumi.Provider(provider), pulumi.DependsOn([]pulumi.Resource{ns, externalDNS}))
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
}
</code></pre>
<p>The last part of the code is creating the ArgoCD deployment. We are using the <code>helm.NewRelease</code> function again to create the deployment. The <code>Values</code> field contains the configuration for the deployment. The <code>server.ingress</code> field contains the now some interesting parts. We need to set the <code>ingressClassName</code> to <code>alb</code> so that the ingress will be managed by the ALB Controller. We also need to set the <code>alb.ingress.kubernetes.io/certificate-arn</code> annotation to the ARN of the certificate in our AWS Certificate Manager. We also need to set the <code>alb.ingress.kubernetes.io/scheme</code> and the <code>alb.ingress.kubernetes.io/target-type</code> annotations to <code>internet-facing</code> and <code>ip</code> respectively.</p>
<p>The <code>configs.cm.oidc.config</code> field contains the configuration for the OIDC provider. We are using Cognito as the OIDC provider set the <code>issuer</code> field to the URL of the Cognito user pool. The <code>clientID</code> and <code>clientSecret</code> fields are the ID and secret of the Cognito user pool client. The <code>requestedScopes</code> and <code>requestedIDTokenClaims</code> fields are the scopes and claims that we want to request from the OIDC provider.</p>
<p>Don't forget to set the <code>url</code> field to your domain name, in my case <code>argocd.ediri.online</code>.</p>
<h2 id="heading-deploying-the-stack">Deploying the stack</h2>
<p>Finally, we can deploy the stack. We can do this by running the following command:</p>
<pre><code class="lang-bash">pulumi up
</code></pre>
<p>This will create the stack and deploy the resources. This will take a few minutes and after that, you should be able to access the ArgoCD dashboard at the URL that you specified! In my case, this is <code>https://argocd.ediri.online</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680725205287/3e91be85-18a2-4a2b-b616-ff5604db5926.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680725231629/07692133-d09d-40b1-b7a4-7a9f66ca16f8.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680725266127/099a53d9-493e-41f3-abf3-3d18b5c62eb6.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680725287097/6d2b5402-0e66-4fa6-848a-233773f42f36.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-housekeeping">Housekeeping</h2>
<p>After you are done with the stack, you can destroy it by running the following command:</p>
<pre><code class="lang-bash">pulumi destroy
</code></pre>
<p>This will destroy all the resources that were created by the stack.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Using AWS Cognito as an OIDC provider for ArgoCD is a great way to secure your ArgoCD deployment. If you already run your infrastructure on AWS it is very easy to set Cognito up and configure it to use it as an OAuth2 provider for ArgoCD.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://argo-cd.readthedocs.io/en/stable/">ArgoCD</a></p>
</li>
<li><p><a target="_blank" href="https://kubernetes-sigs.github.io/aws-load-balancer-controller">AWS Load Balancer Controller</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/cognito/">AWS Cognito</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/certificate-manager/">AWS Certificate Manager</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/iam/">AWS IAM</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Observability Made Easy: Building a RESTful API with Actix Web and OpenTelemetry]]></title><description><![CDATA[TL;DR: Le Code
https://github.com/dirien/quick-bites/tree/main/rust-actix-web-rest-api-opentelemetry
 
Introduction
We keep reading about the importance of Observability in our applications. Charity Majors CEO of Honeycomb made a very good tweet thre...]]></description><link>https://blog.ediri.io/observability-made-easy-building-a-restful-api-with-actix-web-and-opentelemetry</link><guid isPermaLink="true">https://blog.ediri.io/observability-made-easy-building-a-restful-api-with-actix-web-and-opentelemetry</guid><category><![CDATA[Rust]]></category><category><![CDATA[OpenTelemetry]]></category><category><![CDATA[Developer]]></category><category><![CDATA[REST API]]></category><category><![CDATA[observability]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Tue, 04 Apr 2023 14:45:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1680618688300/e7c42490-bb0f-421f-9958-d6051a8b0a28.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-le-code">TL;DR: Le Code</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/rust-actix-web-rest-api-opentelemetry">https://github.com/dirien/quick-bites/tree/main/rust-actix-web-rest-api-opentelemetry</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>We keep reading about the importance of Observability in our applications. <a target="_blank" href="https://twitter.com/mipsytipsy">Charity Majors</a> CEO of Honeycomb made a very good tweet thread in 2018(!!)</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/mipsytipsy/status/1071228841618661376?s=20">https://twitter.com/mipsytipsy/status/1071228841618661376?s=20</a></div>
<p> </p>
<p>And this great blog post from <a target="_blank" href="https://twitter.com/horovits">Dotan Horovits</a>:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://logz.io/learn/opentelemetry-guide/">https://logz.io/learn/opentelemetry-guide/</a></div>
<p> </p>
<p>Or this page created by <a target="_blank" href="https://twitter.com/dnsmichi">Michael Friedrich</a></p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://o11y.love/">https://o11y.love/</a></div>
<p> </p>
<p><a target="_blank" href="https://twitter.com/aurelievache">Aurélie Vache</a> made a good blog article for Golang!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://dev.to/aurelievache/learning-go-by-examples-part-10-instrument-your-go-app-with-opentelemetry-and-send-traces-to-jaeger-distributed-tracing-1p4a">https://dev.to/aurelievache/learning-go-by-examples-part-10-instrument-your-go-app-with-opentelemetry-and-send-traces-to-jaeger-distributed-tracing-1p4a</a></div>
<p> </p>
<p>There are numerous tools and frameworks that we can use during development to offer an Observability layer. In this article, we will add <a target="_blank" href="https://opentelemetry.io/">OpenTelemetry</a> to our REST API with <a target="_blank" href="https://actix.rs/">Actix Web</a> application we developed in the previous article. If you haven't read the <a target="_blank" href="https://blog.ediri.io/building-a-restful-api-with-actix-web-and-diesel-for-persistent-data-storage">previous article</a>:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.ediri.io/building-a-restful-api-with-actix-web-and-diesel-for-persistent-data-storage">https://blog.ediri.io/building-a-restful-api-with-actix-web-and-diesel-for-persistent-data-storage</a></div>
<p> </p>
<p>Let's talk first about Observability and OpenTelemetry to build a basic understanding before we head over to the code part.</p>
<h2 id="heading-observability">Observability?</h2>
<p>When we talk about Observability, we are referring <mark>to the ability to monitor, understand, and troubleshoot the internal state of a system based on its external outputs</mark>. It is a crucial aspect of modern software development and operations (DevOps) that helps ensure the reliability, stability, and performance of applications. Observability is achieved through the collection and analysis of various types of telemetry data, such as logs, metrics, and traces.</p>
<h3 id="heading-why-is-observability-important">Why is Observability important?</h3>
<h4 id="heading-complexity">Complexity</h4>
<p>Modern applications are often <mark>complex</mark>, <mark>distributed</mark>, and composed of multiple <mark>microservices</mark>. This makes it difficult to pinpoint the root cause of issues when they arise. Observability helps developers and operators gain insights into the application's behavior, making it easier to identify and resolve problems.</p>
<h4 id="heading-continuous-deployment">Continuous deployment</h4>
<p>In a fast-paced development environment, <mark>frequent changes</mark> are made to applications. Observability ensures that issues introduced by these changes are quickly detected and addressed, reducing downtime and ensuring a smooth user experience.</p>
<h4 id="heading-scalability">Scalability</h4>
<p>As applications <mark>grow</mark> and <mark>scale</mark>, so do the number of components and services that need to be monitored. Observability enables the tracking of these components, ensuring that performance remains consistent even as the system expands.</p>
<h3 id="heading-is-observability-not-the-same-as-monitoring">Is <mark>Observability not the same as Monitoring</mark>?</h3>
<p>The fact is, monitoring (or application performance monitoring) is a subset of Observability and a step towards achieving it. Observability itself uses different types of telemetry data to provide insights into the state of the system and to understand the reason for any issues that may arise.</p>
<p>Here's a simpler version of the difference between Observability and monitoring:</p>
<ul>
<li><p>Monitoring uses set data, dashboards, and alerts to check how well applications and systems work.</p>
</li>
<li><p>Observability lets you understand what's happening inside complex systems that change over time by looking at all the information available right away.</p>
</li>
</ul>
<h3 id="heading-three-pillars-of-observability">Three Pillars of Observability</h3>
<p><img src="https://www.phcppros.com/ext/resources/2021/08/31/TW0921_three-columns.jpg?t=1630423342&amp;width=1080" alt="Three Pillars of Profit | phcppros" /></p>
<p>The three components of Observability (logs, metrics, and traces) are often referred to as the <mark>Three Pillars of Observability</mark>:</p>
<ul>
<li><p><mark>Metrics</mark>: Metrics are numerical values that are used to measure the health and performance of a system.</p>
</li>
<li><p><mark>Logs</mark>: A timestamped sequence of events that provide insight into the behavior of a system. While metrics show the first signs of a problem, logs provide you with the context to understand the root cause.</p>
</li>
<li><p><mark>Traces</mark>: Traces are a set of events that occur in a distributed system, and are used to understand the flow of requests through the system. When a request moves through a distributed system, it is called a span.</p>
</li>
</ul>
<h3 id="heading-the-benefits-of-observability">The Benefits of Observability:</h3>
<h4 id="heading-faster-issue-resolution">Faster issue resolution</h4>
<p>Observability helps to quickly identify the root cause of issues, <mark>reducing</mark> the mean time to resolution (<mark>MTTR</mark>) and minimizing the impact on users.</p>
<h4 id="heading-proactive-problem-detection">Proactive problem detection</h4>
<p>Observability allows for the <mark>early detection</mark> of potential <mark>problems</mark>, enabling teams to take preventive measures before they escalate and affect users.</p>
<h4 id="heading-improved-performance">Improved performance</h4>
<p>By providing visibility into how applications are performing, Observability allows teams to optimize and fine-tune their systems, resulting in <mark>better performance</mark> and a higher-quality user experience.</p>
<h4 id="heading-data-driven-decision-making">Data-driven decision-making</h4>
<p>Observability provides valuable insights into application behavior, which can be used to make <mark>informed decisions</mark> about system architecture, design, and resource allocation.</p>
<h4 id="heading-enhanced-collaboration">Enhanced collaboration</h4>
<p>Observability promotes a shared understanding of the system's state, <mark>fostering</mark> <mark>better collaboration</mark> between development and operations teams, and improving overall software delivery efficiency.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p><a target="_blank" href="https://www.rust-lang.org">Rust</a></p>
</li>
<li><p>An IDE or text editor of your choice</p>
</li>
<li><p><a target="_blank" href="https://www.docker.com">Docker</a> and <code>docker-compose</code> installed</p>
</li>
<li><p>optional: If you want to interact with the PostgreSQL database, you can install psql with <code>brew install postgresql</code></p>
</li>
<li><p>The code from the last blog article, which you can find <a target="_blank" href="https://github.com/dirien/quick-bites/tree/main/rust-actix-web-rest-api-diesel">here</a></p>
<p>  %[https://github.com/dirien/quick-bites/tree/main/rust-actix-web-rest-api-diesel] </p>
</li>
</ul>
<h2 id="heading-setting-up-the-database">Setting up the Database</h2>
<p>We're going to use the same way to set up the database as we did in the previous article. Simply run the following command</p>
<pre><code class="lang-bash">docker-compose -f postgres.yml up -d
</code></pre>
<p>And run the <code>diesel</code> cli to create the database and the tables</p>
<pre><code class="lang-bash">diesel setup
diesel migration run
</code></pre>
<p>As part of the <code>up.sql</code> migration, we create two tables. The first one is the <code>todos</code> table and the second one is the <code>categories</code> table. The <code>todos</code> table has a foreign key <code>category_id</code> to the <code>categories</code> table.</p>
<p>We also pre-populate the <code>categories</code> table with some categories.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SEQUENCE</span> categories_id_seq;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> categories
(
    <span class="hljs-keyword">id</span>          <span class="hljs-built_in">INTEGER</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">nextval</span>(<span class="hljs-string">'categories_id_seq'</span>),
    <span class="hljs-keyword">name</span>        <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    description <span class="hljs-built_in">TEXT</span>
);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> todos
(
    <span class="hljs-keyword">id</span>          <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>) PRIMARY <span class="hljs-keyword">KEY</span>,
    title       <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    description <span class="hljs-built_in">TEXT</span>,
    created_at  <span class="hljs-built_in">TIMESTAMP</span>,
    updated_at  <span class="hljs-built_in">TIMESTAMP</span>,
    category_id <span class="hljs-built_in">INTEGER</span>,
    <span class="hljs-keyword">FOREIGN</span> <span class="hljs-keyword">KEY</span> (category_id) <span class="hljs-keyword">REFERENCES</span> categories (<span class="hljs-keyword">id</span>)
);

<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> categories (<span class="hljs-keyword">name</span>, description)
<span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'Work'</span>, <span class="hljs-string">'Tasks related to work or job responsibilities'</span>),
       (<span class="hljs-string">'Personal'</span>, <span class="hljs-string">'Personal tasks and errands'</span>),
       (<span class="hljs-string">'Health'</span>, <span class="hljs-string">'Health and fitness related tasks'</span>),
       (<span class="hljs-string">'Hobbies'</span>, <span class="hljs-string">'Tasks related to hobbies and interests'</span>),
       (<span class="hljs-string">'Education'</span>, <span class="hljs-string">'Tasks related to learning and education'</span>);
</code></pre>
<p>In the <code>src/repository/database.rs</code> file, I created two new functions to get all categories and all todos with joined categories. I use a new struct called <code>TodoItemData</code> to return all the data I need for API responses rather than calling two separate functions to get the todos and the categories.</p>
<p>In a real-world application, I would probably create a backend for frontend (BFF) service that would call the backend API and return the data in a format that is easier to work with on the frontend.</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_categories</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Vec</span>&lt;Category&gt; {
    categories
        .load::&lt;Category&gt;(&amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.pool.get().unwrap())
        .expect(<span class="hljs-string">"Error loading all categories"</span>)
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_todos_with_category</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Vec</span>&lt;TodoItemData&gt; {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> empty_todo_item_data_list: <span class="hljs-built_in">Vec</span>&lt;TodoItemData&gt; = <span class="hljs-built_in">Vec</span>::new();
    todos
        .inner_join(categories)
        .load::&lt;(Todo, Category)&gt;(&amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>.pool.get().unwrap())
        .expect(<span class="hljs-string">"Error loading all todos"</span>)
        .into_iter()
        .for_each(|(todo, category)| {
            <span class="hljs-built_in">println!</span>(<span class="hljs-string">"todo: {:?}, category: {:?}"</span>, todo, category);
            <span class="hljs-keyword">let</span> todo_item_data = TodoItemData {
                id: todo.id,
                title: todo.title,
                description: todo.description,
                created_at: todo.created_at,
                updated_at: todo.updated_at,
                category: <span class="hljs-literal">Some</span>(CategoryData {
                    id: category.id,
                    name: category.name,
                    description: category.description,
                }),
            };
            empty_todo_item_data_list.push(todo_item_data);
        });
    empty_todo_item_data_list
}
</code></pre>
<h2 id="heading-why-opentelemetry">Why OpenTelemetry?</h2>
<p>Numerous Observability platforms currently exist, offering in-depth insights into your code and displaying traces, such as Dynatrace, NewRelic, DataDog, etc. So, what makes OpenTelemetry a desirable choice?</p>
<p>OpenTelemetry addresses a significant challenge: establishing a standard for reporting and transmitting measurements.</p>
<p>By using OpenTelemetry with Solution A, you can effortlessly switch to Solution B as your Observability platform without losing any trace history.</p>
<p>As a result, OpenTelemetry has emerged as the go-to standard for many organizations implementing Observability in their systems.</p>
<h2 id="heading-setting-up-opentelemetry">Setting up OpenTelemetry</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680613446488/022c53d4-e8d6-4dc5-b05f-199b50db7cdb.png" alt class="image--center mx-auto" /></p>
<p>Before we can start to add OpenTelemetry code to our application, we need to import some necessary crates.</p>
<pre><code class="lang-bash">cargo add actix_web_opentelemetry - - features opentelemetry-prometheus,metrics,metrics-prometheus,prometheus
cargo add opentelemetry - - features=rt-tokio,trace
cargo add opentelemetry-jaeger - - features=collector_client,rt-tokio,isahc_collector_client,isahc_collector_client
cargo add tracing-opentelemetry
cargo add tracing
cargo add tracing-bunyan-formatter
cargo add tracing-subscriber - - features=env-filter,registry
</code></pre>
<p>If you're not going to use <code>jaeger</code> as your tracing backend, you can remove the <code>opentelemetry-jaeger</code> crate and change it to <code>opentelemetry-otlp</code> or <code>opentelemetry-zipkin</code> crate. See the <a target="_blank" href="https://opentelemetry.io/docs/instrumentation/rust/">OpenTelemetry Rust Crates list</a> for more information.</p>
<p>To set up the tracing layers and metrics exporter, we need to create a new file called <code>tracing.rs</code> in the <code>src</code> folder.</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> actix_web_opentelemetry::{PrometheusMetricsHandler, RequestMetricsBuilder};
<span class="hljs-keyword">use</span> dotenv::dotenv;
<span class="hljs-keyword">use</span> opentelemetry::sdk::export::metrics::aggregation;
<span class="hljs-keyword">use</span> opentelemetry::sdk::metrics::{controllers, processors};
<span class="hljs-keyword">use</span> opentelemetry::{global, sdk};
<span class="hljs-keyword">use</span> tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer};
<span class="hljs-keyword">use</span> tracing_subscriber::{EnvFilter, Registry};
<span class="hljs-keyword">use</span> tracing_subscriber::{prelude::*};

<span class="hljs-meta">#[derive(Debug, Clone)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">OpenTelemetryStack</span></span> {
    request_metrics: actix_web_opentelemetry::RequestMetrics,
    metrics_handler: PrometheusMetricsHandler,
}

<span class="hljs-keyword">impl</span> OpenTelemetryStack {
    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">new</span></span>() -&gt; <span class="hljs-keyword">Self</span> {
        dotenv().ok();
        <span class="hljs-keyword">let</span> app_name = std::env::var(<span class="hljs-string">"CARGO_BIN_NAME"</span>).unwrap_or(<span class="hljs-string">"demo"</span>.to_string());

        global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new());
        <span class="hljs-keyword">let</span> tracer = opentelemetry_jaeger::new_agent_pipeline()
            .with_endpoint(std::env::var(<span class="hljs-string">"JAEGER_ENDPOINT"</span>).unwrap_or(<span class="hljs-string">"localhost:6831"</span>.to_string()))
            .with_service_name(app_name.clone())
            .install_batch(opentelemetry::runtime::Tokio)
            .expect(<span class="hljs-string">"Failed to install OpenTelemetry tracer."</span>);

        <span class="hljs-keyword">let</span> telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
        <span class="hljs-keyword">let</span> env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new(<span class="hljs-string">"INFO"</span>));
        <span class="hljs-keyword">let</span> formatting_layer = BunyanFormattingLayer::new(app_name.clone().into(), std::io::stdout);
        <span class="hljs-keyword">let</span> subscriber = Registry::default()
            .with(telemetry)
            .with(JsonStorageLayer)
            .with(formatting_layer)
            .with(env_filter);
        tracing::subscriber::set_global_default(subscriber)
            .expect(<span class="hljs-string">"Failed to install `tracing` subscriber."</span>);

        <span class="hljs-keyword">let</span> controller = controllers::basic(processors::factory(
            sdk::metrics::selectors::simple::histogram([<span class="hljs-number">0.1</span>, <span class="hljs-number">0.5</span>, <span class="hljs-number">1.0</span>, <span class="hljs-number">2.0</span>, <span class="hljs-number">5.0</span>, <span class="hljs-number">10.0</span>]),
            aggregation::cumulative_temporality_selector(),
        )).build();
        <span class="hljs-keyword">let</span> prometheus_exporter = opentelemetry_prometheus::exporter(controller).init();
        <span class="hljs-keyword">let</span> meter = global::meter(<span class="hljs-string">"global"</span>);
        <span class="hljs-keyword">let</span> request_metrics = RequestMetricsBuilder::new().build(meter);
        <span class="hljs-keyword">let</span> metrics_handler = PrometheusMetricsHandler::new(prometheus_exporter.clone());
        <span class="hljs-keyword">Self</span> {
            request_metrics,
            metrics_handler
        }
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">metrics</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; actix_web_opentelemetry::RequestMetrics {
        <span class="hljs-keyword">self</span>.request_metrics.clone()
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">metrics_handler</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; PrometheusMetricsHandler {
        <span class="hljs-keyword">self</span>.metrics_handler.clone()
    }
}
</code></pre>
<p>In the <code>OpenTelemetryStack</code> struct, we create a new <code>RequestMetrics</code> and <code>PrometheusMetricsHandler</code> instance. In the implementation block of the <code>OpenTelemetryStack</code> struct, we initialize the tracing subscriber and the metrics exporter in the <code>new</code> function.</p>
<p>The endpoint of the <code>opentelemetry_jaeger</code> crate is set to <code>localhost:6831</code> by default. If you're using a different endpoint, you can change it by setting the <code>JAEGER_ENDPOINT</code> environment variable. In the next step, we initialize an environment filter (<code>EnvFilter</code>) that determines which trace events to collect based on their log levels. It attempts to create the filter using the default environment configuration. If it fails, it falls back to creating a filter with the <code>INFO</code> log level.</p>
<p>The <code>BunyanFormattingLayer</code> is used to format the logs in a way that is compatible with the Bunyan log format, which is a popular JSON-based logging format. Then we add all these layers to the <code>tracing</code> subscriber.</p>
<p>The <code>opentelemetry_prometheus</code> crate is used to export metrics to Prometheus. The <code>controllers::basic</code> function is used to initialize the metrics controller using a basic controller provided by the OpenTelemetry SDK. <code>sdk::metrics:: selectors::simple::histogram([0.1, 0.5, 1.0, 2.0, 5.0, 10.0]),</code> sets up a simple histogram selector with the specified boundaries for aggregating metric data. The boundaries are [0.1, 0.5, 1.0, 2.0, 5.0, 10.0]. The <code>aggregation::cumulative_temporality_selector(),</code> line configures the aggregation to use <code>cumulative temporality</code>, meaning the metric values will be aggregated over time, rather than being reset periodically.</p>
<p><code>let meter = global::meter("global");</code> retrieves a global meter instance with the name <code>global</code> (creative I know!). <code>Meters</code> are responsible for creating and recording metric instruments.</p>
<p>The next two lines create a new <code>RequestMetrics</code> and <code>PrometheusMetricsHandler</code> instance with the previously created meter and exporter.</p>
<h2 id="heading-wiring-up-opentelemetry-in-actix">Wiring up OpenTelemetry in Actix</h2>
<p>With the <code>tracing.rs</code> file in place and the heavy lifting done, we can now wire up OpenTelemetry in our Actix application.</p>
<p>Head over to the <code>main.rs</code> file and change the main function to the following:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[actix_web::main]</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() -&gt; std::io::<span class="hljs-built_in">Result</span>&lt;()&gt; {
    <span class="hljs-keyword">let</span> todo_db = repository::database::Database::new();
    <span class="hljs-keyword">let</span> app_data = web::Data::new(todo_db);

    <span class="hljs-keyword">let</span> telemetry = telemetry::OpenTelemetryStack::new();
    <span class="hljs-keyword">let</span> telemetry_data = web::Data::new(telemetry.clone());

    HttpServer::new(<span class="hljs-keyword">move</span> || {
        App::new()
            .app_data(app_data.clone())
            .app_data(telemetry_data.clone())
            .configure(api::api::config)
            .service(healthcheck)
            .service(metrics)
            .default_service(web::route().to(not_found))
            .wrap(actix_web::middleware::Logger::default())
            .wrap(RequestTracing::new())
            .wrap(telemetry.metrics())
    })
        .bind((<span class="hljs-string">"127.0.0.1"</span>, <span class="hljs-number">8080</span>))?
        .run()
        .<span class="hljs-keyword">await</span>
}
</code></pre>
<p>The <code>telemetry</code> variable is initialized with the <code>OpenTelemetryStack</code> struct. The <code>telemetry_data</code> variable is used to pass the <code>telemetry</code> variable to the <code>App</code> instance. The <code>telemetry.metrics()</code> function is used to create a new <code>RequestMetrics</code> instance that is used to collect metrics for the Actix application.</p>
<h2 id="heading-adding-the-tracinginstrument-attribute">Adding the <code>#[tracing::instrument]</code> attribute</h2>
<p>The <code>#[tracing::instrument]</code> attribute is used to instrument the code. It adds spans to the code and adds the tracing context to the logs. The <code>#[tracing::instrument]</code> attribute is added to all the functions in the <code>api</code> module.</p>
<p>Here's an example of the <code>create_todo</code> function in the <code>todos</code> module:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[post(<span class="hljs-meta-string">"/todos"</span>)]</span>
<span class="hljs-meta">#[tracing::instrument]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">create_todo</span></span>(db: web::Data&lt;Database&gt;, new_todo: web::Json&lt;Todo&gt;) -&gt; HttpResponse {
    <span class="hljs-keyword">let</span> todo = db.create_todo(new_todo.into_inner());
    <span class="hljs-keyword">match</span> todo {
        <span class="hljs-literal">Ok</span>(todo) =&gt; HttpResponse::<span class="hljs-literal">Ok</span>().json(todo),
        <span class="hljs-literal">Err</span>(err) =&gt; HttpResponse::InternalServerError().body(err.to_string()),
    }
}

<span class="hljs-comment">// rest of the code is omitted for brevity</span>
</code></pre>
<h2 id="heading-the-metrics-endpoint">The <code>metrics</code> endpoint</h2>
<p>The <code>/metrics</code> endpoint is used to expose the metrics to Prometheus. The <code>metrics</code> endpoint is added to the Actix and looks like this:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[get(<span class="hljs-meta-string">"/metrics"</span>)]</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">metrics</span></span>(telemetry: web::Data&lt;telemetry::OpenTelemetryStack&gt;, db: web::Data&lt;Database&gt;, request: HttpRequest) -&gt; <span class="hljs-keyword">impl</span> Responder {
    <span class="hljs-keyword">let</span> categories = db.get_categories();
    <span class="hljs-keyword">let</span> todos = db.get_todos();

    <span class="hljs-keyword">let</span> meter = global::meter(<span class="hljs-string">"global"</span>);
    <span class="hljs-keyword">let</span> todo_count = meter.i64_observable_gauge(<span class="hljs-string">"todo_count"</span>).with_description(<span class="hljs-string">"Number of todos"</span>).init();
    <span class="hljs-keyword">let</span> category_count = meter.i64_observable_gauge(<span class="hljs-string">"category_count"</span>).with_description(<span class="hljs-string">"Number of categories"</span>).init();

    <span class="hljs-keyword">let</span> cx = Context::current();
    todo_count.observe(&amp;cx, todos.len() <span class="hljs-keyword">as</span> <span class="hljs-built_in">i64</span>, &amp;[]);
    category_count.observe(&amp;cx, categories.len() <span class="hljs-keyword">as</span> <span class="hljs-built_in">i64</span>, &amp;[]);
    telemetry.metrics_handler().call(request).<span class="hljs-keyword">await</span>
}
</code></pre>
<p>The <code>telemetry</code> variable is used to retrieve the <code>PrometheusMetricsHandler</code> instance. The <code>db</code> variable is used to get access to the <code>Database</code> instance.</p>
<p>I created two new metrics, <code>todo_count</code> and <code>category_count</code>, that are used to count the number of todos and categories as a gauge.</p>
<blockquote>
<p>A gauge is a metric that represents a single numerical value that can increase or decrease.</p>
</blockquote>
<p>In the end, the <code>PrometheusMetricsHandler</code> instance is called to handle exposing the additional metrics to Prometheus.</p>
<h2 id="heading-testing-the-application">Testing the application</h2>
<p>Now with everything in place, we can start the application and test it. But before we do that, let us take care that we have a Jaeger and Prometheus instance running. For this, I created a new <code>docker-compose</code> file called <code>telemetry.yaml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3.8'</span>

<span class="hljs-attr">services:</span>
  <span class="hljs-attr">jaeger:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">jaegertracing/all-in-one:1.43.0</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"16686:16686"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"14268:14268"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"9411:9411"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5778:5778"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"6831:6831/udp"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"6832:6832/udp"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5775:5775/udp"</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">COLLECTOR_ZIPKIN_HTTP_PORT=9411</span>

  <span class="hljs-attr">prometheus:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">prom/prometheus:v2.43.0</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"9090:9090"</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./prometheus.yaml:/etc/prometheus/prometheus.yml</span>
</code></pre>
<p>And the corresponding <code>prometheus.yaml</code> file, so that Prometheus can scrape the metrics from the Actix application running currently on <code>localhost:8080</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">global:</span>
  <span class="hljs-attr">scrape_interval:</span> <span class="hljs-string">5s</span> <span class="hljs-comment"># How frequently to scrape targets by default</span>
  <span class="hljs-attr">evaluation_interval:</span> <span class="hljs-string">5s</span> <span class="hljs-comment"># How frequently to evaluate rules by default</span>

<span class="hljs-attr">scrape_configs:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">'demo'</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [ <span class="hljs-string">'host.docker.internal:8080'</span> ] <span class="hljs-comment"># The host.docker.internal is a special DNS name that resolves to the internal IP address used by the host.</span>
        <span class="hljs-attr">labels:</span>
          <span class="hljs-attr">group:</span> <span class="hljs-string">'demo'</span>
</code></pre>
<p>Now we can start the application and Prometheus:</p>
<pre><code class="lang-bash">docker-compose -f telemetry.yaml up -d
</code></pre>
<p>And you can access the Jaeger UI at <code>http://localhost:16686</code> and Prometheus at <code>http://localhost:9090</code>.</p>
<p>Finally, we can start the application:</p>
<pre><code class="lang-bash">cargo run
</code></pre>
<p>Check the <code>/metrics</code> endpoint:</p>
<pre><code class="lang-bash">curl http://localhost:8080/metrics
</code></pre>
<p>You should see the following output:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># HELP category_count Number of categories</span>
<span class="hljs-comment"># TYPE category_count gauge</span>
category_count{service_name=<span class="hljs-string">"unknown_service"</span>} 5
<span class="hljs-comment"># HELP http_server_active_requests HTTP concurrent in-flight requests per route</span>
<span class="hljs-comment"># TYPE http_server_active_requests gauge</span>
http_server_active_requests{http_flavor=<span class="hljs-string">"HTTP/1.1"</span>,http_host=<span class="hljs-string">"host.docker.internal:8080"</span>,http_method=<span class="hljs-string">"GET"</span>,http_scheme=<span class="hljs-string">"http"</span>,http_server_name=<span class="hljs-string">"127.0.0.1:8080"</span>,http_target=<span class="hljs-string">"/metrics"</span>,net_host_port=<span class="hljs-string">"8080"</span>,service_name=<span class="hljs-string">"unknown_service"</span>} 1
<span class="hljs-comment"># HELP http_server_duration HTTP inbound request duration per route</span>
<span class="hljs-comment"># TYPE http_server_duration histogram</span>
http_server_duration_bucket{http_flavor=<span class="hljs-string">"HTTP/1.1"</span>,http_host=<span class="hljs-string">"host.docker.internal:8080"</span>,http_method=<span class="hljs-string">"GET"</span>,http_scheme=<span class="hljs-string">"http"</span>,http_server_name=<span class="hljs-string">"127.0.0.1:8080"</span>,http_status_code=<span class="hljs-string">"200"</span>,http_target=<span class="hljs-string">"/metrics"</span>,net_host_port=<span class="hljs-string">"8080"</span>,service_name=<span class="hljs-string">"unknown_service"</span>,le=<span class="hljs-string">"0.1"</span>} 0
http_server_duration_bucket{http_flavor=<span class="hljs-string">"HTTP/1.1"</span>,http_host=<span class="hljs-string">"host.docker.internal:8080"</span>,http_method=<span class="hljs-string">"GET"</span>,http_scheme=<span class="hljs-string">"http"</span>,http_server_name=<span class="hljs-string">"127.0.0.1:8080"</span>,http_status_code=<span class="hljs-string">"200"</span>,http_target=<span class="hljs-string">"/metrics"</span>,net_host_port=<span class="hljs-string">"8080"</span>,service_name=<span class="hljs-string">"unknown_service"</span>,le=<span class="hljs-string">"0.5"</span>} 0
http_server_duration_bucket{http_flavor=<span class="hljs-string">"HTTP/1.1"</span>,http_host=<span class="hljs-string">"host.docker.internal:8080"</span>,http_method=<span class="hljs-string">"GET"</span>,http_scheme=<span class="hljs-string">"http"</span>,http_server_name=<span class="hljs-string">"127.0.0.1:8080"</span>,http_status_code=<span class="hljs-string">"200"</span>,http_target=<span class="hljs-string">"/metrics"</span>,net_host_port=<span class="hljs-string">"8080"</span>,service_name=<span class="hljs-string">"unknown_service"</span>,le=<span class="hljs-string">"1"</span>} 0
http_server_duration_bucket{http_flavor=<span class="hljs-string">"HTTP/1.1"</span>,http_host=<span class="hljs-string">"host.docker.internal:8080"</span>,http_method=<span class="hljs-string">"GET"</span>,http_scheme=<span class="hljs-string">"http"</span>,http_server_name=<span class="hljs-string">"127.0.0.1:8080"</span>,http_status_code=<span class="hljs-string">"200"</span>,http_target=<span class="hljs-string">"/metrics"</span>,net_host_port=<span class="hljs-string">"8080"</span>,service_name=<span class="hljs-string">"unknown_service"</span>,le=<span class="hljs-string">"2"</span>} 0
http_server_duration_bucket{http_flavor=<span class="hljs-string">"HTTP/1.1"</span>,http_host=<span class="hljs-string">"host.docker.internal:8080"</span>,http_method=<span class="hljs-string">"GET"</span>,http_scheme=<span class="hljs-string">"http"</span>,http_server_name=<span class="hljs-string">"127.0.0.1:8080"</span>,http_status_code=<span class="hljs-string">"200"</span>,http_target=<span class="hljs-string">"/metrics"</span>,net_host_port=<span class="hljs-string">"8080"</span>,service_name=<span class="hljs-string">"unknown_service"</span>,le=<span class="hljs-string">"5"</span>} 3
http_server_duration_bucket{http_flavor=<span class="hljs-string">"HTTP/1.1"</span>,http_host=<span class="hljs-string">"host.docker.internal:8080"</span>,http_method=<span class="hljs-string">"GET"</span>,http_scheme=<span class="hljs-string">"http"</span>,http_server_name=<span class="hljs-string">"127.0.0.1:8080"</span>,http_status_code=<span class="hljs-string">"200"</span>,http_target=<span class="hljs-string">"/metrics"</span>,net_host_port=<span class="hljs-string">"8080"</span>,service_name=<span class="hljs-string">"unknown_service"</span>,le=<span class="hljs-string">"10"</span>} 5
http_server_duration_bucket{http_flavor=<span class="hljs-string">"HTTP/1.1"</span>,http_host=<span class="hljs-string">"host.docker.internal:8080"</span>,http_method=<span class="hljs-string">"GET"</span>,http_scheme=<span class="hljs-string">"http"</span>,http_server_name=<span class="hljs-string">"127.0.0.1:8080"</span>,http_status_code=<span class="hljs-string">"200"</span>,http_target=<span class="hljs-string">"/metrics"</span>,net_host_port=<span class="hljs-string">"8080"</span>,service_name=<span class="hljs-string">"unknown_service"</span>,le=<span class="hljs-string">"+Inf"</span>} 6
http_server_duration_sum{http_flavor=<span class="hljs-string">"HTTP/1.1"</span>,http_host=<span class="hljs-string">"host.docker.internal:8080"</span>,http_method=<span class="hljs-string">"GET"</span>,http_scheme=<span class="hljs-string">"http"</span>,http_server_name=<span class="hljs-string">"127.0.0.1:8080"</span>,http_status_code=<span class="hljs-string">"200"</span>,http_target=<span class="hljs-string">"/metrics"</span>,net_host_port=<span class="hljs-string">"8080"</span>,service_name=<span class="hljs-string">"unknown_service"</span>} 36.187
http_server_duration_count{http_flavor=<span class="hljs-string">"HTTP/1.1"</span>,http_host=<span class="hljs-string">"host.docker.internal:8080"</span>,http_method=<span class="hljs-string">"GET"</span>,http_scheme=<span class="hljs-string">"http"</span>,http_server_name=<span class="hljs-string">"127.0.0.1:8080"</span>,http_status_code=<span class="hljs-string">"200"</span>,http_target=<span class="hljs-string">"/metrics"</span>,net_host_port=<span class="hljs-string">"8080"</span>,service_name=<span class="hljs-string">"unknown_service"</span>} 6
<span class="hljs-comment"># HELP todo_count Number of todos</span>
<span class="hljs-comment"># TYPE todo_count gauge</span>
todo_count{service_name=<span class="hljs-string">"unknown_service"</span>} 0
</code></pre>
<p>As you can see, we have a few metrics that are being collected by default. We can also see that the <code>category_count</code> metric is being collected, and it has a value of <code>5</code>. This is because we initialized the database with 5 categories.</p>
<p>the <code>todo_count</code> metric is being collected, and it has a value of <code>0</code>. This is because we haven't created any todos yet.</p>
<p>Let us create some todos, one for each category:</p>
<pre><code class="lang-bash">curl -s -X POST -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"title": "Prepare presentation slides", "description": "Create slides for the upcoming project meeting", "category_id": 1}'</span> http://localhost:8080/api/todos | jq
curl -s -X POST -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"title": "Buy groceries", "description": "Purchase groceries for the week", "category_id": 2}'</span> http://localhost:8080/api/todos | jq
curl -s -X POST -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"title": "Run 5 miles", "description": "Complete a 5-mile run for weekly exercise", "category_id": 3}'</span> http://localhost:8080/api/todos | jq
curl -s -X POST -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"title": "Practice guitar", "description": "Spend 30 minutes practicing guitar chords", "category_id": 4}'</span> http://localhost:8080/api/todos | jq
curl -s -X POST -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"title": "Study for the exam", "description": "Review course material for the upcoming exam", "category_id": 5}'</span> http://localhost:8080/api/todos | jq

{
  <span class="hljs-string">"id"</span>: <span class="hljs-string">"55aff8a6-42c2-4d94-8872-b94a55beae08"</span>,
  <span class="hljs-string">"title"</span>: <span class="hljs-string">"Prepare presentation slides"</span>,
  <span class="hljs-string">"description"</span>: <span class="hljs-string">"Create slides for the upcoming project meeting"</span>,
  <span class="hljs-string">"created_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.581186"</span>,
  <span class="hljs-string">"updated_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.581199"</span>,
  <span class="hljs-string">"category_id"</span>: 1
}
{
  <span class="hljs-string">"id"</span>: <span class="hljs-string">"e10567cb-c1df-413a-8996-6c072e124a0c"</span>,
  <span class="hljs-string">"title"</span>: <span class="hljs-string">"Buy groceries"</span>,
  <span class="hljs-string">"description"</span>: <span class="hljs-string">"Purchase groceries for the week"</span>,
  <span class="hljs-string">"created_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.604805"</span>,
  <span class="hljs-string">"updated_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.604809"</span>,
  <span class="hljs-string">"category_id"</span>: 2
}
{
  <span class="hljs-string">"id"</span>: <span class="hljs-string">"4f0bb9d3-fd24-4a51-b11e-1a12231a4529"</span>,
  <span class="hljs-string">"title"</span>: <span class="hljs-string">"Run 5 miles"</span>,
  <span class="hljs-string">"description"</span>: <span class="hljs-string">"Complete a 5-mile run for weekly exercise"</span>,
  <span class="hljs-string">"created_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.619386"</span>,
  <span class="hljs-string">"updated_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.619389"</span>,
  <span class="hljs-string">"category_id"</span>: 3
}
{
  <span class="hljs-string">"id"</span>: <span class="hljs-string">"055b7ab2-53d9-468b-a4ac-82e70d190a33"</span>,
  <span class="hljs-string">"title"</span>: <span class="hljs-string">"Practice guitar"</span>,
  <span class="hljs-string">"description"</span>: <span class="hljs-string">"Spend 30 minutes practicing guitar chords"</span>,
  <span class="hljs-string">"created_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.633674"</span>,
  <span class="hljs-string">"updated_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.633676"</span>,
  <span class="hljs-string">"category_id"</span>: 4
}
{
  <span class="hljs-string">"id"</span>: <span class="hljs-string">"b422a294-57f7-4c0c-bd67-9d7babf1bd9c"</span>,
  <span class="hljs-string">"title"</span>: <span class="hljs-string">"Study for the exam"</span>,
  <span class="hljs-string">"description"</span>: <span class="hljs-string">"Review course material for the upcoming exam"</span>,
  <span class="hljs-string">"created_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.649398"</span>,
  <span class="hljs-string">"updated_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.649401"</span>,
  <span class="hljs-string">"category_id"</span>: 5
}
</code></pre>
<p>Now, let's get all todos with the <code>/api/todos</code> endpoint:</p>
<pre><code class="lang-bash">curl -s http://127.0.0.1:8080/api/todos | jq                                                                                                                                                                                             
[
  {
    <span class="hljs-string">"id"</span>: <span class="hljs-string">"55aff8a6-42c2-4d94-8872-b94a55beae08"</span>,
    <span class="hljs-string">"title"</span>: <span class="hljs-string">"Prepare presentation slides"</span>,
    <span class="hljs-string">"description"</span>: <span class="hljs-string">"Create slides for the upcoming project meeting"</span>,
    <span class="hljs-string">"created_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.581186"</span>,
    <span class="hljs-string">"updated_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.581199"</span>,
    <span class="hljs-string">"category"</span>: {
      <span class="hljs-string">"id"</span>: 1,
      <span class="hljs-string">"name"</span>: <span class="hljs-string">"Work"</span>,
      <span class="hljs-string">"description"</span>: <span class="hljs-string">"Tasks related to work or job responsibilities"</span>
    }
  },
  {
    <span class="hljs-string">"id"</span>: <span class="hljs-string">"e10567cb-c1df-413a-8996-6c072e124a0c"</span>,
    <span class="hljs-string">"title"</span>: <span class="hljs-string">"Buy groceries"</span>,
    <span class="hljs-string">"description"</span>: <span class="hljs-string">"Purchase groceries for the week"</span>,
    <span class="hljs-string">"created_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.604805"</span>,
    <span class="hljs-string">"updated_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.604809"</span>,
    <span class="hljs-string">"category"</span>: {
      <span class="hljs-string">"id"</span>: 2,
      <span class="hljs-string">"name"</span>: <span class="hljs-string">"Personal"</span>,
      <span class="hljs-string">"description"</span>: <span class="hljs-string">"Personal tasks and errands"</span>
    }
  },
  {
    <span class="hljs-string">"id"</span>: <span class="hljs-string">"4f0bb9d3-fd24-4a51-b11e-1a12231a4529"</span>,
    <span class="hljs-string">"title"</span>: <span class="hljs-string">"Run 5 miles"</span>,
    <span class="hljs-string">"description"</span>: <span class="hljs-string">"Complete a 5-mile run for weekly exercise"</span>,
    <span class="hljs-string">"created_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.619386"</span>,
    <span class="hljs-string">"updated_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.619389"</span>,
    <span class="hljs-string">"category"</span>: {
      <span class="hljs-string">"id"</span>: 3,
      <span class="hljs-string">"name"</span>: <span class="hljs-string">"Health"</span>,
      <span class="hljs-string">"description"</span>: <span class="hljs-string">"Health and fitness related tasks"</span>
    }
  },
  {
    <span class="hljs-string">"id"</span>: <span class="hljs-string">"055b7ab2-53d9-468b-a4ac-82e70d190a33"</span>,
    <span class="hljs-string">"title"</span>: <span class="hljs-string">"Practice guitar"</span>,
    <span class="hljs-string">"description"</span>: <span class="hljs-string">"Spend 30 minutes practicing guitar chords"</span>,
    <span class="hljs-string">"created_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.633674"</span>,
    <span class="hljs-string">"updated_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.633676"</span>,
    <span class="hljs-string">"category"</span>: {
      <span class="hljs-string">"id"</span>: 4,
      <span class="hljs-string">"name"</span>: <span class="hljs-string">"Hobbies"</span>,
      <span class="hljs-string">"description"</span>: <span class="hljs-string">"Tasks related to hobbies and interests"</span>
    }
  },
  {
    <span class="hljs-string">"id"</span>: <span class="hljs-string">"b422a294-57f7-4c0c-bd67-9d7babf1bd9c"</span>,
    <span class="hljs-string">"title"</span>: <span class="hljs-string">"Study for the exam"</span>,
    <span class="hljs-string">"description"</span>: <span class="hljs-string">"Review course material for the upcoming exam"</span>,
    <span class="hljs-string">"created_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.649398"</span>,
    <span class="hljs-string">"updated_at"</span>: <span class="hljs-string">"2023-04-04T09:40:07.649401"</span>,
    <span class="hljs-string">"category"</span>: {
      <span class="hljs-string">"id"</span>: 5,
      <span class="hljs-string">"name"</span>: <span class="hljs-string">"Education"</span>,
      <span class="hljs-string">"description"</span>: <span class="hljs-string">"Tasks related to learning and education"</span>
    }
  }
]
</code></pre>
<p>And check the <code>/metrics</code> endpoint:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># HELP category_count Number of categories</span>
<span class="hljs-comment"># TYPE category_count gauge</span>
category_count{service_name=<span class="hljs-string">"unknown_service"</span>} 5

<span class="hljs-comment"># omitted for brevity</span>

<span class="hljs-comment"># HELP todo_count Number of todos</span>
<span class="hljs-comment"># TYPE todo_count gauge</span>
todo_count{service_name=<span class="hljs-string">"unknown_service"</span>} 5
</code></pre>
<p>The <code>todo_count</code> metric is now showing the correct value of 5, as expected.</p>
<p>Head over to the UI of Prometheus and check the <code>todo_count</code> metric</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680612458585/d27a22b2-1490-412b-bd53-c618ceb3f695.png" alt class="image--center mx-auto" /></p>
<p>And same for the traces in the Jaeger UI</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680612475813/5ea1efd3-5127-41e4-bcd7-e0b930d18074.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-housekeeping">Housekeeping</h2>
<p>To stop and remove the <code>postgres</code> container and volume, run the following commands:</p>
<pre><code class="lang-bash">docker-compose -f postgres.yaml down
docker volume rm rust-actix-web-rest-api-diesel_postgres-data
</code></pre>
<p>To stop the <code>telemetry</code> containers, run the following commands:</p>
<pre><code class="lang-bash">docker-compose -f telemetry.yaml down
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congrats! We, again, successfully added telemetry to our Rust Actix Web REST API demo application. We also configured Prometheus and Jaeger to collect and visualize the metrics and traces. And everything on top of our existing application code.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p><a target="_blank" href="https://www.jaegertracing.io/">Jaeger</a></p>
</li>
<li><p><a target="_blank" href="https://prometheus.io/">Prometheus</a></p>
</li>
<li><p><a target="_blank" href="https://opentelemetry.io/">OpenTelemetry</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/OutThereLabs/actix-web-opentelemetry">Actix Web OpenTelemetry</a></p>
</li>
<li><p><a target="_blank" href="https://hub.docker.com/_/postgres">postgres</a></p>
</li>
<li><p><a target="_blank" href="https://hub.docker.com/r/prom/prometheus">prom/prometheus</a></p>
</li>
<li><p><a target="_blank" href="https://hub.docker.com/r/jaegertracing/all-in-one">jaegertracing/all-in-one</a></p>
</li>
<li><p><a target="_blank" href="https://docs.docker.com/compose/">Docker Compose</a></p>
</li>
<li><p><a target="_blank" href="https://actix.rs/">actix.rs</a></p>
</li>
<li><p><a target="_blank" href="https://o11y.love/">o11y.love</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Step-by-Step Guide: Setting Up WASI Node Pools for AKS and Running WASM Spin Applications all with Pulumi]]></title><description><![CDATA[TL;DR: Code
https://github.com/dirien/pulumi-aks-wasm-spin
 
Introduction
In this blog post, I am going to show you how to run a Fermyon Spin WebAssembly (WASM) application on a WebAssembly System Interface (WASI) node pool.
Everything will be powere...]]></description><link>https://blog.ediri.io/step-by-step-guide-setting-up-wasi-node-pools-for-aks-and-running-wasm-spin-applications-all-with-pulumi</link><guid isPermaLink="true">https://blog.ediri.io/step-by-step-guide-setting-up-wasi-node-pools-for-aks-and-running-wasm-spin-applications-all-with-pulumi</guid><category><![CDATA[wasm]]></category><category><![CDATA[Rust]]></category><category><![CDATA[Pulumi]]></category><category><![CDATA[Azure]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Sun, 26 Mar 2023 21:18:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679865231025/560a8d0e-7d2e-4d61-bb40-5aaf26ede1a9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-code">TL;DR: Code</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/pulumi-aks-wasm-spin">https://github.com/dirien/pulumi-aks-wasm-spin</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>In this blog post, I am going to show you how to run a <a target="_blank" href="https://www.fermyon.com/">Fermyon</a> <a target="_blank" href="https://www.fermyon.com/spin">Spin</a> WebAssembly (WASM) application on a WebAssembly System Interface (WASI) node pool.</p>
<p><strong>Everything will be powered by using</strong> <a target="_blank" href="https://www.pulumi.com/"><strong>Pulumi</strong></a><strong>.</strong></p>
<p>Microsoft has revealed their decision to transition from krustlet and adopt <a target="_blank" href="https://github.com/deislabs/containerd-wasm-shims">containerd shims</a> for operating WASM workloads in their WASI node pools within AKS.</p>
<p>The objective of <a target="_blank" href="https://github.com/deislabs/containerd-wasm-shims">containerd shims</a> is to offer implementations capable of executing WASM/WASI workloads utilizing the <a target="_blank" href="https://github.com/deislabs/runwasi">runwasi</a> library. By installing these shims on Kubernetes nodes, a runtime class can be added to Kubernetes, enabling the scheduling of Wasm workloads on the respective nodes. This allows your Wasm pods and deployments to function similarly to container workloads.</p>
<p><a target="_blank" href="https://github.com/deislabs/runwasi">Runwasi</a> is a project focused on operating wasm workloads on Wasmtime, a rapid and secure WebAssembly runtime managed by containerd.</p>
<p>We will focus this article on the Spin shim which is powered by the <a target="_blank" href="https://github.com/fermyon/spin">Fermyon Spin</a> engine.</p>
<h2 id="heading-what-is-wasm">What is WASM?</h2>
<p>WebAssembly (WASM) is a binary format that is designed for maximum execution speed and portability using a WASM runtime. The WASM runtime is designed to run on a target architecture and execute WebAssemblies in a sandboxed environment to ensure security at near-native speed.</p>
<p>The WebAssembly System Interface (WASI) establishes a standardized connection between the WASM runtime and the host system, granting access to system resources like the file system or network.</p>
<h2 id="heading-what-is-fermyon-spin">What is Fermyon Spin?</h2>
<p>Fermyon <a target="_blank" href="https://www.fermyon.com/spin">Spin</a> is a framework for building cloud-native applications with WebAssembly components. It is created by Fermyon and is fully open source. You can find the source code on <a target="_blank" href="https://github.com/fermyon/spin">GitHub</a>.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p><a target="_blank" href="https://www.pulumi.com/docs/get-started/install/">Pulumi</a></p>
</li>
<li><p><a target="_blank" href="https://docs.microsoft.com/en-us/cli/azure/install-azure-cli">Azure CLI</a></p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">kubectl</a></p>
</li>
<li><p><a target="_blank" href="https://www.rust-lang.org/tools/install">Rust installed</a></p>
</li>
<li><p><a target="_blank" href="https://golang.org/doc/install">Go installed</a></p>
</li>
<li><p>IDE of your choice (VS Code, IntelliJ, etc.)</p>
</li>
</ul>
<h2 id="heading-activate-preview-features-for-aks">Activate Preview Features for AKS</h2>
<p>To install the <code>aks-preview</code> extension for Azure CLI, run the following command:</p>
<pre><code class="lang-bash">az extension add --name aks-preview
</code></pre>
<p>or update your existing <code>aks-preview</code> extension to the latest version:</p>
<pre><code class="lang-bash">az extension update --name aks-preview
</code></pre>
<h2 id="heading-register-the-wasmnodepoolpreview-feature">Register the <code>WasmNodePoolPreview</code> Feature</h2>
<p>You may need to register the <code>WasmNodePoolPreview</code> feature for your subscription by simply running the following command:</p>
<pre><code class="lang-bash">az feature register --namespace <span class="hljs-string">"Microsoft.ContainerService"</span> --name <span class="hljs-string">"WasmNodePoolPreview"</span>
</code></pre>
<p>This will take a few minutes to complete. You can check the status of the feature registration by running the following command:</p>
<pre><code class="lang-bash">az feature show --namespace <span class="hljs-string">"Microsoft.ContainerService"</span> --name <span class="hljs-string">"WasmNodePoolPreview"</span>
</code></pre>
<p>Once the <code>state</code> property of the feature is <code>Registered</code>, you can create a WASI node pool for your AKS cluster.</p>
<h2 id="heading-set-up-your-aks-cluster">Set Up Your AKS Cluster</h2>
<p>Let's start a new Pulumi project with the following command, using the <code>pulumi-azure-native</code> provider and <code>Go</code> as the language of choice:</p>
<pre><code class="lang-bash">mkdir pulumi-aks-wasm-spin &amp;&amp; <span class="hljs-built_in">cd</span> pulumi-aks-wasm-spin
pulumi new azure-go --force
</code></pre>
<p>You will need to provide some details about your project. You can use the default values for all questions for this demo.</p>
<p>We can now generate our AKS cluster using the subsequent code:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"encoding/base64"</span>

    containerservice <span class="hljs-string">"github.com/pulumi/pulumi-azure-native-sdk/containerservice/v20230101"</span>
    resources <span class="hljs-string">"github.com/pulumi/pulumi-azure-native-sdk/resources/v20220901"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi/sdk/v3/go/pulumi"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// Create an Azure Resource Group</span>
        resourceGroup, err := resources.NewResourceGroup(ctx, <span class="hljs-string">"wasm-aks-rg"</span>, &amp;resources.ResourceGroupArgs{
            ResourceGroupName: pulumi.String(<span class="hljs-string">"wasm-aks-rg"</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        wasmCluster, err := containerservice.NewManagedCluster(ctx, <span class="hljs-string">"wasm-aks-cluster"</span>, &amp;containerservice.ManagedClusterArgs{
            ResourceGroupName: resourceGroup.Name,
            KubernetesVersion: pulumi.String(<span class="hljs-string">"1.25.5"</span>),
            ResourceName:      pulumi.String(<span class="hljs-string">"wasm-aks-cluster"</span>),
            Identity: &amp;containerservice.ManagedClusterIdentityArgs{
                Type: containerservice.ResourceIdentityTypeSystemAssigned,
            },
            DnsPrefix: pulumi.String(<span class="hljs-string">"wasm-aks-cluster"</span>),
            AgentPoolProfiles: containerservice.ManagedClusterAgentPoolProfileArray{
                &amp;containerservice.ManagedClusterAgentPoolProfileArgs{
                    Name:         pulumi.String(<span class="hljs-string">"agentpool"</span>),
                    Mode:         pulumi.String(<span class="hljs-string">"System"</span>),
                    OsDiskSizeGB: pulumi.Int(<span class="hljs-number">30</span>),
                    OsType:       pulumi.String(<span class="hljs-string">"Linux"</span>),
                    Count:        pulumi.Int(<span class="hljs-number">1</span>),
                    VmSize:       pulumi.String(<span class="hljs-string">"Standard_B4ms"</span>),
                },
            },
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        wasmPool, err := containerservice.NewAgentPool(ctx, <span class="hljs-string">"wasm-aks-agentpool"</span>, &amp;containerservice.AgentPoolArgs{
            AgentPoolName:     pulumi.String(<span class="hljs-string">"wasmpool"</span>),
            ResourceGroupName: resourceGroup.Name,
            ResourceName:      wasmCluster.Name,
            WorkloadRuntime:   pulumi.String(<span class="hljs-string">"WasmWasi"</span>),
            Count:             pulumi.Int(<span class="hljs-number">1</span>),
            VmSize:            pulumi.String(<span class="hljs-string">"Standard_B4ms"</span>),
            OsType:            pulumi.String(<span class="hljs-string">"Linux"</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        kubeconfig := pulumi.All(wasmCluster.Name, resourceGroup.Name).ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args []<span class="hljs-keyword">interface</span>{})</span> <span class="hljs-params">(*<span class="hljs-keyword">string</span>, error)</span></span> {
            clusterName := args[<span class="hljs-number">0</span>].(<span class="hljs-keyword">string</span>)
            resourceGroupName := args[<span class="hljs-number">1</span>].(<span class="hljs-keyword">string</span>)
            creds, err := containerservice.ListManagedClusterUserCredentials(ctx, &amp;containerservice.ListManagedClusterUserCredentialsArgs{
                ResourceGroupName: resourceGroupName,
                ResourceName:      clusterName,
            })
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
            }
            decoded, err := base64.StdEncoding.DecodeString(creds.Kubeconfigs[<span class="hljs-number">0</span>].Value)
            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
            }
            <span class="hljs-keyword">return</span> pulumi.StringRef(<span class="hljs-keyword">string</span>(decoded)), <span class="hljs-literal">nil</span>
        }).(pulumi.StringPtrOutput)

        ctx.Export(<span class="hljs-string">"resourceGroupName"</span>, resourceGroup.Name)
        ctx.Export(<span class="hljs-string">"wasmClusterName"</span>, wasmCluster.Name)
        ctx.Export(<span class="hljs-string">"wasmAgentPoolName"</span>, wasmPool.Name)
        ctx.Export(<span class="hljs-string">"kubeconfig"</span>, pulumi.ToSecret(kubeconfig))

        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>The code above will create a basic AKS cluster with a single node pool.</p>
<p>Then we will add a WASI node pool using the <code>containerservice.NewAgentPool</code> function. It is very important that the <code>WorkloadRuntime</code> property is set to <code>WasmWasi</code> to enable the <code>containerd-wasm-shims</code> on the node pool. Also, the <code>OsType</code> property must be set to <code>Linux</code> and the <code>WASM/WASI</code> node pool can't be used as a system node pool.</p>
<p>The last part of the code above is exporting the <code>kubeconfig</code> of the cluster. This is needed to connect to the cluster if you want to use the <code>kubectl</code> CLI or <code>k9s</code> to inspect the cluster.</p>
<p>The <code>ctx.Export</code> statements are exporting for example the <code>kubeconfig</code> as a secret and some other information about the infrastructure. You can run the following Pulumi command to see all the exported values.</p>
<pre><code class="lang-bash">pulumi stack output
</code></pre>
<p>Before I continue with the next steps regarding the infrastructure, I think it is a good time to switch to <code>Spin</code> to create the workload for our WASI node pool.</p>
<h2 id="heading-developing-a-wasm-application-using-fermyon-spin">Developing a WASM Application Using Fermyon Spin</h2>
<h3 id="heading-installation-of-the-spin-cli">Installation of the Spin CLI</h3>
<p>Before we can start building our application, we need to install the Spin CLI. To do so, we can run the following command:</p>
<pre><code class="lang-bash">curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
sudo mv spin /usr/<span class="hljs-built_in">local</span>/bin
</code></pre>
<p>There are more installation options available on the <a target="_blank" href="https://developer.fermyon.com/spin/install">install Spin</a> documentation page depending on your operating system. I am using macOS, so I will use the above command.</p>
<h3 id="heading-start-a-new-spin-project">Start a New Spin Project</h3>
<p>Now with the Spin CLI installed, we can create our project. Spin supports multiple languages, but not all features are available in every language. Please check the <a target="_blank" href="https://developer.fermyon.com/spin/language-support-overview">language Support</a> page for more detailed information.</p>
<p>In this blog post, I will be using the Rust language. I start by installing the Spin templates.</p>
<blockquote>
<p>Note: You do not need any templates to create a Spin application, but they will help you to get started quicker as everything is already configured.</p>
</blockquote>
<pre><code class="lang-bash">spin templates install --git https://github.com/fermyon/spin --update
Copying remote template <span class="hljs-built_in">source</span>
Installing template redis-rust...
Installing template static-fileserver...
Installing template http-grain...
Installing template http-swift...
Installing template http-php...
Installing template http-c...
Installing template redirect...
Installing template http-rust...
Installing template http-go...
Installing template http-zig...
Installing template http-empty...
Installing template redis-go...
Installed 12 template(s)

+------------------------------------------------------------------------+
| Name                Description                                        |
+========================================================================+
| http-c              HTTP request handler using C and the Zig toolchain |
| http-empty          HTTP application with no components                |
| http-go             HTTP request handler using (Tiny)Go                |
| http-grain          HTTP request handler using Grain                   |
| http-php            HTTP request handler using PHP                     |
| http-rust           HTTP request handler using Rust                    |
| http-swift          HTTP request handler using SwiftWasm               |
| http-zig            HTTP request handler using Zig                     |
| redirect            Redirects a HTTP route                             |
| redis-go            Redis message handler using (Tiny)Go               |
| redis-rust          Redis message handler using Rust                   |
| static-fileserver   Serves static files from an asset directory        |
+------------------------------------------------------------------------+
</code></pre>
<p>To build in Rust Spin components, we need to install the <code>wasm32-wasi</code> target for Rust. To install the target, run following command:</p>
<pre><code class="lang-bash">rustup target add wasm32-wasi
</code></pre>
<p>Now we can call the <code>spin new</code> command to create a new Spin application:</p>
<pre><code class="lang-bash">spin new http-rust 
Enter a name <span class="hljs-keyword">for</span> your new application: aks-spin-demo
Description: Demo Spin application <span class="hljs-keyword">for</span> AKS WASI node pool
HTTP base: /
HTTP path: /api/figlet
</code></pre>
<p>This should generate all the files we need and a directory called <code>aks-spin-demo</code>. The <code>spin.toml</code> file contains the configuration for the application.</p>
<p>Let's take a look at the <code>spin.toml</code> file:</p>
<pre><code class="lang-toml"><span class="hljs-attr">spin_manifest_version</span> = <span class="hljs-string">"1"</span>
<span class="hljs-attr">authors</span> = [<span class="hljs-string">"Engin Diri"</span>]
<span class="hljs-attr">description</span> = <span class="hljs-string">"Demo Spin application for AKS WASI node pool"</span>
<span class="hljs-attr">name</span> = <span class="hljs-string">"aks-spin-demo"</span>
<span class="hljs-attr">trigger</span> = { type = <span class="hljs-string">"http"</span>, base = <span class="hljs-string">"/"</span> }
<span class="hljs-attr">version</span> = <span class="hljs-string">"0.1.0"</span>

<span class="hljs-section">[[component]]</span>
<span class="hljs-attr">id</span> = <span class="hljs-string">"aks-spin-demo"</span>
<span class="hljs-attr">source</span> = <span class="hljs-string">"target/wasm32-wasi/release/aks_spin_demo.wasm"</span>
<span class="hljs-attr">allowed_http_hosts</span> = []
<span class="hljs-section">[component.trigger]</span>
<span class="hljs-attr">route</span> = <span class="hljs-string">"/api/figlet"</span>
<span class="hljs-section">[component.build]</span>
<span class="hljs-attr">command</span> = <span class="hljs-string">"cargo build --target wasm32-wasi --release"</span>
</code></pre>
<p>As we want to build a nice Figlet application, we need to add the following dependencies to the <code>Cargo.toml</code> file:</p>
<pre><code class="lang-toml"><span class="hljs-attr">figlet-rs</span> = <span class="hljs-string">"0.1.5"</span>
</code></pre>
<p>We then need to change the code in the <code>src/lib.rs</code> file to the following:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> anyhow::<span class="hljs-built_in">Result</span>;
<span class="hljs-keyword">use</span> spin_sdk::{
    http::{Request, Response},
    http_component,
};
<span class="hljs-keyword">use</span> figlet_rs::FIGfont;

<span class="hljs-comment">/// A simple Spin HTTP component.</span>
<span class="hljs-meta">#[http_component]</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">handle_aks_spin_demo</span></span>(_: Request) -&gt; <span class="hljs-built_in">Result</span>&lt;Response&gt; {
    <span class="hljs-keyword">let</span> standard_font = FIGfont::standard().unwrap();
    <span class="hljs-keyword">let</span> figure = standard_font.convert(<span class="hljs-string">"Hello, Fermyon on Azure AKS!"</span>);
    <span class="hljs-literal">Ok</span>(http::Response::builder()
        .status(<span class="hljs-number">200</span>).body(<span class="hljs-literal">Some</span>(figure.unwrap().to_string().into()))?)
}
</code></pre>
<h3 id="heading-build-and-run-the-application-locally">Build and Run the Application Locally</h3>
<p>You can try out the application locally by running the following command:</p>
<pre><code class="lang-bash">spin build -u -f spin-local.toml
</code></pre>
<p>This will build the application and start a local web server. You can run a curl command to test the application:</p>
<pre><code class="lang-bash">curl http://127.0.0.1:3000/api/figlet
</code></pre>
<p>You should see the following output:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679863779234/c940dc85-df3a-40ea-8d41-9841dd760ab0.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-publish-the-application-to-azure-container-registry-acr">Publish the Application to Azure Container Registry (ACR)</h3>
<p>Before we can publish the application to the ACR, we need to create the ACR first. We will do this by extending the existing Pulumi program.</p>
<p>Add the <code>containerregistry</code> package to our <code>go.mod</code> file, by running the following command:</p>
<pre><code class="lang-bash">go get -u github.com/pulumi/pulumi-azure-native-sdk/containerregistry
go get -u github.com/pulumi/pulumi-azure-native-sdk/authorization
go get -u github.com/pulumi/pulumi-docker/sdk/v4
</code></pre>
<p>Now we can add the following code to the <code>main.go</code> file:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// ... Omited code</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// ... Omited code</span>

        registry, err := v20230101preview.NewRegistry(ctx, <span class="hljs-string">"wasm-aks-registry"</span>, &amp;v20230101preview.RegistryArgs{
            ResourceGroupName: resourceGroup.Name,
            Location:          resourceGroup.Location,
            RegistryName:      pulumi.String(<span class="hljs-string">"wasmaksregistry"</span>),
            AdminUserEnabled:  pulumi.Bool(<span class="hljs-literal">true</span>),
            Sku: &amp;v20230101preview.SkuArgs{
                Name: pulumi.String(<span class="hljs-string">"Standard"</span>),
            },
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        credentials := pulumi.All(resourceGroup.Name, registry.Name).ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args []<span class="hljs-keyword">interface</span>{})</span> <span class="hljs-params">(*v20230101preview.ListRegistryCredentialsResult, error)</span></span> {
            <span class="hljs-keyword">return</span> v20230101preview.ListRegistryCredentials(ctx, &amp;v20230101preview.ListRegistryCredentialsArgs{
                ResourceGroupName: args[<span class="hljs-number">0</span>].(<span class="hljs-keyword">string</span>),
                RegistryName:      args[<span class="hljs-number">1</span>].(<span class="hljs-keyword">string</span>),
            })
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        adminUsername := credentials.ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(result <span class="hljs-keyword">interface</span>{})</span> <span class="hljs-params">(<span class="hljs-keyword">string</span>, error)</span></span> {
            credentials := result.(*v20230101preview.ListRegistryCredentialsResult)
            <span class="hljs-keyword">return</span> *credentials.Username, <span class="hljs-literal">nil</span>
        }).(pulumi.StringOutput)
        adminPassword := credentials.ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(result <span class="hljs-keyword">interface</span>{})</span> <span class="hljs-params">(<span class="hljs-keyword">string</span>, error)</span></span> {
            credentials := result.(*v20230101preview.ListRegistryCredentialsResult)
            <span class="hljs-keyword">return</span> *credentials.Passwords[<span class="hljs-number">0</span>].Value, <span class="hljs-literal">nil</span>
        }).(pulumi.StringOutput)

        definition, err := v20220401.LookupRoleDefinition(ctx, &amp;v20220401.LookupRoleDefinitionArgs{
            RoleDefinitionId: <span class="hljs-string">"7f951dda-4ed3-4680-a7ca-43fe172d538d"</span>,
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        _, err = v20220401.NewRoleAssignment(ctx, <span class="hljs-string">"wasm-aks-role-assignment"</span>, &amp;v20220401.RoleAssignmentArgs{
            PrincipalId:      wasmCluster.IdentityProfile.MapIndex(pulumi.String(<span class="hljs-string">"kubeletidentity"</span>)).ObjectId().Elem(),
            PrincipalType:    pulumi.String(v20220401.PrincipalTypeServicePrincipal),
            RoleDefinitionId: pulumi.String(definition.Id),
            Scope:            registry.ID(),
        }, pulumi.DependsOn([]pulumi.Resource{registry}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        image, err := docker.NewImage(ctx, <span class="hljs-string">"wasm-spin-demo-image"</span>, &amp;docker.ImageArgs{
            ImageName: pulumi.Sprintf(<span class="hljs-string">"%s.azurecr.io/aks-wasm-spin-demo:latest"</span>, registry.Name),
            Build: &amp;docker.DockerBuildArgs{
                Dockerfile:     pulumi.String(<span class="hljs-string">"aks-spin-demo/Dockerfile"</span>),
                Context:        pulumi.String(<span class="hljs-string">"aks-spin-demo"</span>),
                BuilderVersion: docker.BuilderVersionBuilderBuildKit,
                Platform:       pulumi.String(<span class="hljs-string">"linux/amd64"</span>),
            },
            Registry: &amp;docker.RegistryArgs{
                Server:   pulumi.Sprintf(<span class="hljs-string">"%s.azurecr.io"</span>, registry.Name),
                Username: adminUsername,
                Password: adminPassword,
            },
        }, pulumi.DependsOn([]pulumi.Resource{wasmCluster, wasmPool, registry}))

        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>This code will create the ACR resource with admin user enabled. We will use the admin user to push our Spin image to the ACR. We also create a role assignment to allow the AKS cluster to pull images from the newly created ACR. With this role assignment in place, we don't need to create a pull secret for the AKS cluster or a specific service account.</p>
<p>The last part of the code will build the image using the <code>pulumi-docker</code> provider. I have created a Dockerfile in the <code>aks-spin-demo</code> folder which is a multi-stage build. The first stage will build the Rust application and the second stage will create a minimal image with the compiled binary and the <code>spin.toml</code> file.</p>
<blockquote>
<p><strong>Attention</strong>: The tag <code>spin_manifest_version</code> has to be renamed to <code>spin_version</code>, otherwise the shim will not work!</p>
</blockquote>
<p>The minimal image is created by using the Chainguard <code>cgr.dev/chainguard/static</code> image. The <code>cgr.dev/chainguard/static</code> image is a base image with just enough files to run static binaries!</p>
<pre><code class="lang-bash">FROM --platform=<span class="hljs-variable">${BUILDPLATFORM}</span> rust:1.68.1 AS build
WORKDIR /opt/build
COPY . .
RUN rustup target add wasm32-wasi &amp;&amp; cargo build --target wasm32-wasi --release

FROM cgr.dev/chainguard/static:latest
COPY --from=build /opt/build/target/wasm32-wasi/release/aks_spin_demo.wasm .
COPY --from=build /opt/build/spin.toml .
</code></pre>
<h3 id="heading-deploy-the-application-to-the-aks-cluster">Deploy the Application to the AKS Cluster</h3>
<p>Now we can head over to the deployment of the Spin application on our AKS cluster. For this step, we will use the <code>pulumi-kubernetes</code> provider. With this provider, we can use <code>go</code> to create the Kubernetes resources.</p>
<p>The resources we will create are:</p>
<ul>
<li><p>A <code>namespace</code> for the application, we name it <code>wasm-demo</code></p>
</li>
<li><p>A <code>deployment</code> for the application, important is here to set the <code>command</code> to <code>/</code></p>
</li>
<li><p>A <code>service</code> for the application of type <code>LoadBalancer</code></p>
</li>
</ul>
<p>Add the <code>pulumi-kubernetes</code> provider to your <code>go.mod</code> file:</p>
<pre><code class="lang-bash">go get -u github.com/pulumi/pulumi-kubernetes/sdk/v3
</code></pre>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// ... Omited code</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// ... Omited code</span>
        k8s, err := kubernetes.NewProvider(ctx, <span class="hljs-string">"wasm-aks-provider"</span>, &amp;kubernetes.ProviderArgs{
            Kubeconfig:            kubeconfig,
            EnableServerSideApply: pulumi.Bool(<span class="hljs-literal">true</span>),
        }, pulumi.DependsOn([]pulumi.Resource{wasmCluster, wasmPool, registry}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        _, err = core.NewNamespace(ctx, <span class="hljs-string">"wasm-aks-namespace"</span>, &amp;core.NamespaceArgs{
            Metadata: &amp;meta.ObjectMetaArgs{
                Name: pulumi.String(<span class="hljs-string">"wasm-demo"</span>),
            },
        }, pulumi.Provider(k8s))

        deployment, err := apps.NewDeployment(ctx, <span class="hljs-string">"wasm-aks-deployment"</span>, &amp;apps.DeploymentArgs{
            Metadata: &amp;meta.ObjectMetaArgs{
                Name:      pulumi.String(<span class="hljs-string">"wasm-demo"</span>),
                Namespace: pulumi.String(<span class="hljs-string">"wasm-demo"</span>),
                Annotations: pulumi.StringMap{
                    <span class="hljs-string">"pulumi.com/skipAwait"</span>: pulumi.String(<span class="hljs-string">"true"</span>),
                },
            },
            Spec: &amp;apps.DeploymentSpecArgs{
                Selector: &amp;meta.LabelSelectorArgs{
                    MatchLabels: pulumi.StringMap{
                        <span class="hljs-string">"app"</span>: pulumi.String(<span class="hljs-string">"wasm-demo"</span>),
                    },
                },
                Replicas: pulumi.Int(<span class="hljs-number">1</span>),
                Template: &amp;core.PodTemplateSpecArgs{
                    Metadata: &amp;meta.ObjectMetaArgs{
                        Labels: pulumi.StringMap{
                            <span class="hljs-string">"app"</span>: pulumi.String(<span class="hljs-string">"wasm-demo"</span>),
                        },
                    },
                    Spec: &amp;core.PodSpecArgs{
                        RuntimeClassName: pulumi.String(<span class="hljs-string">"wasmtime-spin-v1"</span>),
                        Containers: core.ContainerArray{
                            &amp;core.ContainerArgs{
                                Name:  pulumi.String(<span class="hljs-string">"wasm-demo"</span>),
                                Image: image.ImageName,
                                Command: pulumi.StringArray{
                                    pulumi.String(<span class="hljs-string">"/"</span>),
                                },
                                Resources: &amp;core.ResourceRequirementsArgs{
                                    Requests: pulumi.StringMap{
                                        <span class="hljs-string">"cpu"</span>:    pulumi.String(<span class="hljs-string">"10m"</span>),
                                        <span class="hljs-string">"memory"</span>: pulumi.String(<span class="hljs-string">"10Mi"</span>),
                                    },
                                    Limits: pulumi.StringMap{
                                        <span class="hljs-string">"cpu"</span>:    pulumi.String(<span class="hljs-string">"500m"</span>),
                                        <span class="hljs-string">"memory"</span>: pulumi.String(<span class="hljs-string">"64Mi"</span>),
                                    },
                                },
                            },
                        },
                    },
                },
            },
        }, pulumi.Provider(k8s), pulumi.DependsOn([]pulumi.Resource{wasmCluster, wasmPool, registry, image}))

        _, err = core.NewService(ctx, <span class="hljs-string">"wasm-aks-service"</span>, &amp;core.ServiceArgs{
            Metadata: &amp;meta.ObjectMetaArgs{
                Name:      pulumi.String(<span class="hljs-string">"wasm-demo"</span>),
                Namespace: pulumi.String(<span class="hljs-string">"wasm-demo"</span>),
                Annotations: pulumi.StringMap{
                    <span class="hljs-string">"pulumi.com/skipAwait"</span>: pulumi.String(<span class="hljs-string">"true"</span>),
                },
            },
            Spec: &amp;core.ServiceSpecArgs{
                Type: core.ServiceSpecTypeLoadBalancer,
                Ports: core.ServicePortArray{
                    &amp;core.ServicePortArgs{
                        Name:       pulumi.String(<span class="hljs-string">"http"</span>),
                        Protocol:   pulumi.String(<span class="hljs-string">"TCP"</span>),
                        Port:       pulumi.Int(<span class="hljs-number">8080</span>),
                        TargetPort: pulumi.Int(<span class="hljs-number">80</span>),
                    },
                },
                Selector: pulumi.StringMap{
                    <span class="hljs-string">"app"</span>: deployment.Spec.Selector().MatchLabels().MapIndex(pulumi.String(<span class="hljs-string">"app"</span>)),
                },
            },
        }, pulumi.Provider(k8s))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<h3 id="heading-test-the-application">Test the Application</h3>
<p>Now we can run the <code>pulumi up</code> command to deploy the application to the AKS cluster.</p>
<pre><code class="lang-bash">pulumi up
</code></pre>
<p>After the deployment is finished, we can get the public IP of the service with the following command:</p>
<pre><code class="lang-bash">kubectl get svc -n wasm-demo wasm-demo -o jsonpath=<span class="hljs-string">'{.status.loadBalancer.ingress[0].ip}'</span>
20.101.12.51
</code></pre>
<p>With this IP address, we can now use <code>curl</code> to test the application:</p>
<pre><code class="lang-bash">curl http://20.101.12.51:8080/api/figlet
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679863898454/2cfb57ba-2134-44a0-8568-01da5d09294e.png" alt class="image--center mx-auto" /></p>
<p>And it works!</p>
<h2 id="heading-housekeeping">Housekeeping</h2>
<p>To clean up the resources, we can run the <code>pulumi destroy</code> command. This will delete all the resources that we just created.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>AKS support for WASM remains in its preview stage, yet deploying WASM applications to AKS is feasible by activating the <code>WasmNodePoolPreview</code> feature flag.</p>
<p>I think that WASM is a very interesting technology and seeing that major cloud providers like Azure are already starting to support it is very exciting and a step in the right direction.</p>
<p>I eagerly anticipate the advancements in store for WASM and am confident that we will observe an increasing number of WASM integrations from all the major cloud providers.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p>https://www.pulumi.com/docs/get-started/install/</p>
</li>
<li><p>https://developer.fermyon.com/spin/index</p>
</li>
<li><p>https://www.pulumi.com/registry/packages/azure-native/</p>
</li>
<li><p>https://www.pulumi.com/registry/packages/kubernetes/</p>
</li>
<li><p>https://www.pulumi.com/registry/packages/docker/</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Minecraft Server: Secrets, Observability, Kubernetes and more with Pulumi and Scaleway]]></title><description><![CDATA[TL;DR: Code
https://github.com/dirien/quick-bites/tree/main/pulumi-scaleway-kapsule
 
Introduction
In this tutorial, I want to use some new Beta services from Scaleway to deploy a Minecraft server on Kubernetes. To deploy the infrastructure I will be...]]></description><link>https://blog.ediri.io/minecraft-server-secrets-observability-kubernetes-and-more-with-pulumi-and-scaleway</link><guid isPermaLink="true">https://blog.ediri.io/minecraft-server-secrets-observability-kubernetes-and-more-with-pulumi-and-scaleway</guid><category><![CDATA[Pulumi]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[#prometheus]]></category><category><![CDATA[loki]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Engin Diri]]></dc:creator><pubDate>Wed, 22 Mar 2023 15:27:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679498771689/31a96ae1-35fb-47bf-b0b9-267ff0c0291a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-tldr-code">TL;DR: Code</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/quick-bites/tree/main/pulumi-scaleway-kapsule">https://github.com/dirien/quick-bites/tree/main/pulumi-scaleway-kapsule</a></div>
<p> </p>
<h2 id="heading-introduction">Introduction</h2>
<p>In this tutorial, I want to use some new Beta services from Scaleway to deploy a Minecraft server on Kubernetes. To deploy the infrastructure I will be using Pulumi, a modern infrastructure as code tool. Because nobody wants to manage infrastructure by hand, right?</p>
<p>So what are the new Scaleway services we will be using? For the whole observability part, we're going to use <a target="_blank" href="https://www.scaleway.com/en/docs/observability/cockpit/">Cockpit</a>. Cockpit is the new monitoring and logging tool from Scaleway. To handle the secrets we will use <code>external-secrets</code> Kubernetes operator and as backed we will use the new <a target="_blank" href="https://www.scaleway.com/en/docs/identity-and-access-management/secret-manager/">Secret Manager</a> from Scaleway.</p>
<p>And yes, we will use the new Scaleway Managed Kubernetes Service called <a target="_blank" href="https://www.scaleway.com/en/docs/containers/kubernetes/">Kapsule</a> as a runtime for all of this.</p>
<p>So without further ado, let's get started!</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>If you want to follow along, you need to have the following installed:</p>
<ul>
<li><p><a target="_blank" href="https://www.pulumi.com/docs/get-started/install/">Pulumi</a></p>
</li>
<li><p>A Scaleway account with a valid access key and secret key</p>
</li>
<li><p><a target="_blank" href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">kubectl</a> if you want to interact with the Kubernetes cluster</p>
</li>
</ul>
<h2 id="heading-setup-your-pulumi-project">Setup your Pulumi project</h2>
<p>To get things started, we need to create a new Pulumi project. Create a new directory and run <code>pulumi new</code> inside of it, as I am going to use <a target="_blank" href="https://go.dev/">Go</a> for this tutorial, I will select <code>go</code> to use a predefined template for this blog post.</p>
<blockquote>
<p>You can find more about Pulumi templates <a target="_blank" href="https://www.pulumi.com/templates/">here</a>.</p>
</blockquote>
<pre><code class="lang-bash">mkdir pulumi-scaleway-kapsule &amp;&amp; <span class="hljs-built_in">cd</span> pulumi-scaleway-kapsule
pulumi new go --force
</code></pre>
<p>I left all the default values as they are, no need to change anything here.</p>
<pre><code class="lang-bash">This <span class="hljs-built_in">command</span> will walk you through creating a new Pulumi project.

Enter a value or leave blank to accept the (default), and press &lt;ENTER&gt;.
Press ^C at any time to quit.

project name: (pulumi-scaleway-kapsule) 
project description: (A minimal Go Pulumi program) 
Created project <span class="hljs-string">'pulumi-scaleway-kapsule'</span>

Please enter your desired stack name.
To create a stack <span class="hljs-keyword">in</span> an organization, use the format &lt;org-name&gt;/&lt;stack-name&gt; (e.g. `acmecorp/dev`).
stack name: (dev) 
Created stack <span class="hljs-string">'dev'</span>

Installing dependencies...

Finished installing dependencies

Your new project is ready to go! ✨

To perform an initial deployment, run `pulumi up`
</code></pre>
<p>To use the Scaleway provider, we can add it using the <code>go get</code> command:</p>
<pre><code class="lang-bash">go get github.com/dirien/pulumi-scaleway/sdk/v2
</code></pre>
<p>And as we deploy several <code>Helm</code> charts, we need to add the Kubernetes provider too:</p>
<pre><code class="lang-bash">go get github.com/pulumi/pulumi-kubernetes/sdk/v3
</code></pre>
<p>And that's it from a Go dependency perspective. Your <code>go.mod</code> should look like this:</p>
<pre><code class="lang-go">module pulumi-scaleway-kapsule

<span class="hljs-keyword">go</span> <span class="hljs-number">1.20</span>

require (
github.com/dirien/pulumi-scaleway/sdk/v2 v2<span class="hljs-number">.13</span><span class="hljs-number">.1</span>
github.com/pulumi/pulumi-kubernetes/sdk/v3 v3<span class="hljs-number">.24</span><span class="hljs-number">.2</span>
github.com/pulumi/pulumi/sdk/v3 v3<span class="hljs-number">.58</span><span class="hljs-number">.0</span>
)
</code></pre>
<h2 id="heading-setup-your-pulumi-stack">Setup your Pulumi stack</h2>
<p>Now with the project setup done, we can start to create our infrastructure. Head over to the <code>main.go</code> file and add the following code:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"github.com/dirien/pulumi-scaleway/sdk/v2/go/scaleway"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/apiextensions"</span>
    v1 <span class="hljs-string">"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/core/v1"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/helm/v3"</span>
    metav1 <span class="hljs-string">"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/meta/v1"</span>
    <span class="hljs-string">"github.com/pulumi/pulumi/sdk/v3/go/pulumi"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>This is the basic structure of a Pulumi program. We have a <code>main</code> function which will be called by Pulumi and inside of this function we can create our infrastructure.</p>
<p>We start by creating a new Scaleway <code>project</code>. Scaleway has a concept of projects, which are basically a groupings of different resources. This is useful if you want to separate different environments like <code>dev</code>, <code>staging</code> and <code>prod</code> and makes it easier to manage with <code>IAM</code>.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitting imports</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        project, err := scaleway.NewAccountProject(ctx, <span class="hljs-string">"scaleway-project"</span>, &amp;scaleway.AccountProjectArgs{
            Name: pulumi.String(<span class="hljs-string">"pulumi-scaleway-kapsule"</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>Now we have a Scaleway <code>project</code> created, we can create our <code>Cockpit</code> instance. We create also a <code>Cockpit</code> token, which allows you to authenticate against the <code>Cockpit</code> API. We can select the token permissions on creation too.</p>
<p>The following permissions are available:</p>
<ul>
<li><p>Push: this allows you to send your metrics and logs to your Cockpit.</p>
</li>
<li><p>Query: this allows you to fetch your metrics and logs from your Cockpit.</p>
</li>
<li><p>Rules: allow you to configure alerting and recording rules.</p>
</li>
<li><p>Alerts: allow you to set up the alert manager.</p>
</li>
</ul>
<p>Cockpit uses under-the-hood Cortex (Metrics and Alertmanager) and Loki. You get all of them dedicated API URLs:</p>
<ul>
<li><p>Cortex: https://metrics.prd.obs.fr-par.scw.cloud/api/v1/push</p>
</li>
<li><p>Loki: https://logs.prd.obs.fr-par.scw.cloud/loki/api/v1/push</p>
</li>
<li><p>Alertmanager: https://alertmanager.prd.obs.fr-par.scw.cloud</p>
</li>
</ul>
<p>The final resource, we create in the <code>Cockpit</code> context is a local Grafana user. We use this user to authenticate against the managed Grafana instance from Scaleway. There are two roles you can select:</p>
<ul>
<li><p>Editor: this allows you to edit dashboards and create new ones.</p>
</li>
<li><p>Viewer: allows you to only view dashboards.</p>
</li>
</ul>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitting imports</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        project, err := scaleway.NewAccountProject(ctx, <span class="hljs-string">"scaleway-project"</span>, &amp;scaleway.AccountProjectArgs{
            Name: pulumi.String(<span class="hljs-string">"pulumi-scaleway-kapsule"</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        cockpit, err := scaleway.NewCockpit(ctx, <span class="hljs-string">"scaleway-cockpit"</span>, &amp;scaleway.CockpitArgs{
            ProjectId: project.ID(),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        cockpitToken, err := scaleway.NewCockpitToken(ctx, <span class="hljs-string">"scaleway-cockpit-token"</span>, &amp;scaleway.CockpitTokenArgs{
            Name:      pulumi.String(<span class="hljs-string">"cockpit-token"</span>),
            ProjectId: cockpit.ProjectId,
            Scopes: scaleway.CockpitTokenScopesArgs{
                QueryLogs:    pulumi.Bool(<span class="hljs-literal">true</span>),
                WriteLogs:    pulumi.Bool(<span class="hljs-literal">true</span>),
                QueryMetrics: pulumi.Bool(<span class="hljs-literal">true</span>),
                WriteMetrics: pulumi.Bool(<span class="hljs-literal">true</span>),
            },
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        user, err := scaleway.NewCockpitGrafanaUser(ctx, <span class="hljs-string">"scaleway-cockpit-grafana-user"</span>, &amp;scaleway.CockpitGrafanaUserArgs{
            ProjectId: cockpit.ProjectId,
            Role:      pulumi.String(<span class="hljs-string">"editor"</span>),
            Login:     pulumi.String(<span class="hljs-string">"pulumi"</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        ctx.Export(<span class="hljs-string">"grafana-password"</span>, pulumi.ToSecret(user.Password))
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>With the Scaleway <code>Cockpit</code> service defined, we can now create the Scaleway Secret Manager. We will use this service to store our Minecraft RCON password. To retrieve the password in our Kubernetes cluster, we will use later the <code>external-secrets</code> Operator. We will also create a dedicated IAM user and a dedicated IAM policy for fetching the secret.</p>
<p>Keep in mind to change the <code>please-change-me</code> password later on in the Console, I just need this to get the example working as the Helm chart to deploy the Minecraft server requires a Kubernetes secret with the RCON password.</p>
<p>The IAM API key will be used by the <code>external-secrets</code> Operator to authenticate against the Scaleway Secret Manager API.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitting imports</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitting previous code</span>

        secret, err := scaleway.NewSecret(ctx, <span class="hljs-string">"scaleway-secret"</span>, &amp;scaleway.SecretArgs{
            Name:      pulumi.String(<span class="hljs-string">"scaleway-secret"</span>),
            ProjectId: project.ID(),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        _, err = scaleway.NewSecretVersion(ctx, <span class="hljs-string">"scaleway-secret-version"</span>, &amp;scaleway.SecretVersionArgs{
            SecretId: secret.ID(),
            Data:     pulumi.String(<span class="hljs-string">"please-change-me"</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        iamApplication, err := scaleway.NewIamApplication(ctx, <span class="hljs-string">"scaleway-iam-application"</span>, &amp;scaleway.IamApplicationArgs{
            Name: pulumi.String(<span class="hljs-string">"pulumi-application"</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        _, err = scaleway.NewIamPolicy(ctx, <span class="hljs-string">"scaleway-iam-policy"</span>, &amp;scaleway.IamPolicyArgs{
            Name:          pulumi.String(<span class="hljs-string">"pulumi-scaleway-iam-policy"</span>),
            ApplicationId: iamApplication.ID(),
            Rules: scaleway.IamPolicyRuleArray{
                &amp;scaleway.IamPolicyRuleArgs{
                    ProjectIds: pulumi.StringArray{
                        project.ID(),
                    },
                    PermissionSetNames: pulumi.StringArray{
                        pulumi.String(<span class="hljs-string">"SecretManagerFullAccess"</span>),
                    },
                },
            },
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        key, err := scaleway.NewIamApiKey(ctx, <span class="hljs-string">"scaleway-iam-api-key"</span>, &amp;scaleway.IamApiKeyArgs{
            ApplicationId: iamApplication.ID(),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<blockquote>
<p><strong>Note:</strong> The IAM Policy permission is set to <code>SecretManagerFullAccess</code></p>
</blockquote>
<p>Finally, we can create our Kubernetes cluster. We will use the Scaleway Kapsule service to create a managed Kubernetes cluster and add a node pool to it. There are a lot of options available to configure the cluster and the node pool. I tried to select the most important ones.</p>
<p>Feel free to change them to your needs.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitting imports</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitting previous code</span>

        k8sCluster, err := scaleway.NewK8sCluster(ctx, <span class="hljs-string">"k8s-cluster"</span>, &amp;scaleway.K8sClusterArgs{
            Name:    pulumi.String(<span class="hljs-string">"pulumi-scaleway-kapsule"</span>),
            Version: pulumi.String(<span class="hljs-string">"1.26"</span>),
            Cni:     pulumi.String(<span class="hljs-string">"cilium"</span>),
            AutoUpgrade: scaleway.K8sClusterAutoUpgradeArgs{
                Enable:                     pulumi.Bool(<span class="hljs-literal">true</span>),
                MaintenanceWindowDay:       pulumi.String(<span class="hljs-string">"sunday"</span>),
                MaintenanceWindowStartHour: pulumi.Int(<span class="hljs-number">3</span>),
            },
            AdmissionPlugins: pulumi.StringArray{
                pulumi.String(<span class="hljs-string">"AlwaysPullImages"</span>),
            },
            DeleteAdditionalResources: pulumi.Bool(<span class="hljs-literal">true</span>),
            ProjectId:                 project.ID(),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        pool, err := scaleway.NewK8sPool(ctx, <span class="hljs-string">"k8s-pool"</span>, &amp;scaleway.K8sPoolArgs{
            Name:        pulumi.String(<span class="hljs-string">"pulumi-scaleway-kapsule-pool"</span>),
            ClusterId:   k8sCluster.ID(),
            NodeType:    pulumi.String(<span class="hljs-string">"PLAY2-MICRO"</span>),
            Autoscaling: pulumi.BoolPtr(<span class="hljs-literal">true</span>),
            MinSize:     pulumi.Int(<span class="hljs-number">1</span>),
            MaxSize:     pulumi.Int(<span class="hljs-number">3</span>),
            Size:        pulumi.Int(<span class="hljs-number">1</span>),
            Autohealing: pulumi.BoolPtr(<span class="hljs-literal">true</span>),
        })
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<h2 id="heading-deploying-the-observability-stack">Deploying the Observability Stack</h2>
<p>Now with the infrastructure in place, we can deploy the observability stack onto our recently created Kubernetes cluster. For this, we will use the <code>pulumi-kubernetes</code> provider as it offers us a handy way to deploy Helm charts. The <code>helm.Release</code> resource is our key component here.</p>
<p>Our observability stack will consist of the following components:</p>
<p><code>kube-prometheus-stack</code>, but without deploying Grafana. We will use the Scaleway Cockpit Grafana instance instead.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack">https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack</a></div>
<p> </p>
<p><code>promtail</code>, to collect logs from our Kubernetes cluster.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://artifacthub.io/packages/helm/grafana/promtail">https://artifacthub.io/packages/helm/grafana/promtail</a></div>
<p> </p>
<p>Both stacks are configured to use the <code>remote write</code> feature to forward metrics and logs to the Scaleway Cockpit service. And as both endpoints are protected, we have to use the IAM API key we created earlier to authenticate against it.</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitting imports</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitting previous code</span>

        kubernetesProvider, err := kubernetes.NewProvider(ctx, <span class="hljs-string">"k8s-provider"</span>, &amp;kubernetes.ProviderArgs{
            Kubeconfig:            k8sCluster.Kubeconfigs.Index(pulumi.Int(<span class="hljs-number">0</span>)).ConfigFile(),
            EnableServerSideApply: pulumi.Bool(<span class="hljs-literal">true</span>),
        }, pulumi.DependsOn([]pulumi.Resource{k8sCluster, pool}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        kubePrometheusStack, err := helm.NewRelease(ctx, <span class="hljs-string">"kube-prometheus-stack"</span>, &amp;helm.ReleaseArgs{
            Name:  pulumi.String(<span class="hljs-string">"kube-prometheus-stack"</span>),
            Chart: pulumi.String(<span class="hljs-string">"kube-prometheus-stack"</span>),
            RepositoryOpts: helm.RepositoryOptsArgs{
                Repo: pulumi.String(<span class="hljs-string">"https://prometheus-community.github.io/helm-charts"</span>),
            },
            Namespace:       pulumi.String(<span class="hljs-string">"monitoring"</span>),
            Version:         pulumi.String(<span class="hljs-string">"45.7.1"</span>),
            CreateNamespace: pulumi.BoolPtr(<span class="hljs-literal">true</span>),
            Values: pulumi.Map{
                <span class="hljs-string">"grafana"</span>: pulumi.Map{
                    <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">false</span>),
                },
                <span class="hljs-string">"kube-state-metrics"</span>: pulumi.Map{
                    <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                },
                <span class="hljs-string">"prometheus-node-exporter"</span>: pulumi.Map{
                    <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                    <span class="hljs-string">"prometheus"</span>: pulumi.Map{
                        <span class="hljs-string">"monitor"</span>: pulumi.Map{
                            <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                        },
                    },
                },
                <span class="hljs-string">"prometheus"</span>: pulumi.Map{
                    <span class="hljs-string">"prometheusSpec"</span>: pulumi.Map{
                        <span class="hljs-string">"remoteWrite"</span>: pulumi.Array{
                            pulumi.Map{
                                <span class="hljs-string">"url"</span>:         pulumi.String(<span class="hljs-string">"https://metrics.prd.obs.fr-par.scw.cloud/api/v1/push"</span>),
                                <span class="hljs-string">"bearerToken"</span>: cockpitToken.SecretKey,
                            },
                        },
                        <span class="hljs-string">"ruleSelectorNilUsesHelmValues"</span>:           pulumi.Bool(<span class="hljs-literal">false</span>),
                        <span class="hljs-string">"serviceMonitorSelectorNilUsesHelmValues"</span>: pulumi.Bool(<span class="hljs-literal">false</span>),
                    },
                },
            },
        }, pulumi.Provider(kubernetesProvider))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        _, err = helm.NewRelease(ctx, <span class="hljs-string">"promtail"</span>, &amp;helm.ReleaseArgs{
            Name:  pulumi.String(<span class="hljs-string">"promtail"</span>),
            Chart: pulumi.String(<span class="hljs-string">"promtail"</span>),
            RepositoryOpts: helm.RepositoryOptsArgs{
                Repo: pulumi.String(<span class="hljs-string">"https://grafana.github.io/helm-charts"</span>),
            },
            Namespace:       pulumi.String(<span class="hljs-string">"monitoring"</span>),
            Version:         pulumi.String(<span class="hljs-string">"6.9.3"</span>),
            CreateNamespace: pulumi.BoolPtr(<span class="hljs-literal">true</span>),
            Values: pulumi.Map{
                <span class="hljs-string">"config"</span>: pulumi.Map{
                    <span class="hljs-string">"clients"</span>: pulumi.Array{
                        pulumi.Map{
                            <span class="hljs-string">"url"</span>:          pulumi.String(<span class="hljs-string">"https://logs.prd.obs.fr-par.scw.cloud/loki/api/v1/push"</span>),
                            <span class="hljs-string">"bearer_token"</span>: cockpitToken.SecretKey,
                        },
                    },
                },
                <span class="hljs-string">"serviceMonitor"</span>: pulumi.Map{
                    <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                },
            },
        }, pulumi.Provider(kubernetesProvider), pulumi.DependsOn([]pulumi.Resource{kubePrometheusStack}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<h2 id="heading-deploying-the-external-secrets-operator">Deploying the external-secrets operator</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://artifacthub.io/packages/helm/external-secrets-operator/external-secrets">https://artifacthub.io/packages/helm/external-secrets-operator/external-secrets</a></div>
<p> </p>
<p>Deploying the <code>external-secrets</code> operator is a piece of cake. We just need to deploy the Helm chart and leave it much to the defaults. I just activated the <code>prometheus</code> service monitor to get some metrics about the operator itself.</p>
<blockquote>
<p>I will create for the <code>external-secrets</code> operator a dedicated blog article, as part of my <strong>Advanced Secret Management on Kubernetes With Pulumi</strong> series!</p>
</blockquote>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitting imports</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitting previous code</span>

        externalSecrets, err := helm.NewRelease(ctx, <span class="hljs-string">"external-secrets"</span>, &amp;helm.ReleaseArgs{
            Name:  pulumi.String(<span class="hljs-string">"external-secrets"</span>),
            Chart: pulumi.String(<span class="hljs-string">"external-secrets"</span>),
            RepositoryOpts: helm.RepositoryOptsArgs{
                Repo: pulumi.String(<span class="hljs-string">"https://charts.external-secrets.io"</span>),
            },
            Namespace:       pulumi.String(<span class="hljs-string">"external-secrets"</span>),
            Version:         pulumi.String(<span class="hljs-string">"0.8.1"</span>),
            CreateNamespace: pulumi.BoolPtr(<span class="hljs-literal">true</span>),
            Values: pulumi.Map{
                <span class="hljs-string">"installCRDs"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                <span class="hljs-string">"serviceMonitor"</span>: pulumi.Map{
                    <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                },
                <span class="hljs-string">"webhook"</span>: pulumi.Map{
                    <span class="hljs-string">"serviceMonitor"</span>: pulumi.Map{
                        <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                    },
                },
                <span class="hljs-string">"certController"</span>: pulumi.Map{
                    <span class="hljs-string">"serviceMonitor"</span>: pulumi.Map{
                        <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                    },
                },
            },
        }, pulumi.Provider(kubernetesProvider), pulumi.DependsOn([]pulumi.Resource{secret, kubePrometheusStack}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<h2 id="heading-deploying-the-minecraft-server">Deploying the Minecraft server</h2>
<p>Now we are going to deploy all components required to run a Minecraft server. This consists of the following parts:</p>
<ul>
<li><p>The Namespace <code>minecraft</code>, where all components will be deployed.</p>
</li>
<li><p>The Helm chart for the Minecraft server. Important is here that we will use a sidecar container to run our <code>minecraft-exporter</code>.</p>
</li>
<li><p>The <code>SecretStore</code> CR, which will be used by the <code>external-secrets</code> operator to fetch the secrets from the Scaleway Secrets Manager.</p>
</li>
<li><p>The <code>ExternalSecret</code> CR, which will be used by the <code>external-secrets</code> operator to create the Kubernetes Secret.</p>
</li>
<li><p>The <code>ServiceMonitor</code> CR, which will be used by Prometheus to scrape the metrics from the <code>minecraft-exporter</code>.</p>
</li>
</ul>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://artifacthub.io/packages/helm/minecraft-server-charts/minecraft">https://artifacthub.io/packages/helm/minecraft-server-charts/minecraft</a></div>
<p> </p>
<p>For the <code>prometheus-minecraft-expoter</code> we use this project of mine:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/dirien/minecraft-prometheus-exporter">https://github.com/dirien/minecraft-prometheus-exporter</a></div>
<p> </p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-comment">// omitting imports</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    pulumi.Run(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx *pulumi.Context)</span> <span class="hljs-title">error</span></span> {
        <span class="hljs-comment">// omitting previous code</span>
        mcNamespace, err := v1.NewNamespace(ctx, <span class="hljs-string">"minecraft"</span>, &amp;v1.NamespaceArgs{
            Metadata: &amp;metav1.ObjectMetaArgs{
                Name: pulumi.String(<span class="hljs-string">"minecraft"</span>),
            },
        }, pulumi.Provider(kubernetesProvider))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        mc, err := helm.NewRelease(ctx, <span class="hljs-string">"minecraft"</span>, &amp;helm.ReleaseArgs{
            Chart:   pulumi.String(<span class="hljs-string">"minecraft"</span>),
            Version: pulumi.String(<span class="hljs-string">"4.6.0"</span>),
            RepositoryOpts: &amp;helm.RepositoryOptsArgs{
                Repo: pulumi.String(<span class="hljs-string">"https://itzg.github.io/minecraft-server-charts"</span>),
            },
            Namespace: mcNamespace.Metadata.Name(),
            Values: pulumi.Map{
                <span class="hljs-string">"minecraftServer"</span>: pulumi.Map{
                    <span class="hljs-string">"eula"</span>:        pulumi.Bool(<span class="hljs-literal">true</span>),
                    <span class="hljs-string">"motd"</span>:        pulumi.String(<span class="hljs-string">"Scaleway and Pulumi: Minecraft Server"</span>),
                    <span class="hljs-string">"serviceType"</span>: pulumi.String(<span class="hljs-string">"LoadBalancer"</span>),
                    <span class="hljs-string">"rcon"</span>: pulumi.Map{
                        <span class="hljs-string">"enabled"</span>:        pulumi.Bool(<span class="hljs-literal">true</span>),
                        <span class="hljs-string">"existingSecret"</span>: pulumi.String(<span class="hljs-string">"minecraft-rcon"</span>),
                    },
                    <span class="hljs-string">"extraPorts"</span>: pulumi.Array{
                        pulumi.Map{
                            <span class="hljs-string">"name"</span>:          pulumi.String(<span class="hljs-string">"prom"</span>),
                            <span class="hljs-string">"containerPort"</span>: pulumi.Int(<span class="hljs-number">9150</span>),
                            <span class="hljs-string">"protocol"</span>:      pulumi.String(<span class="hljs-string">"TCP"</span>),
                            <span class="hljs-string">"service"</span>: pulumi.Map{
                                <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                                <span class="hljs-string">"port"</span>:    pulumi.Int(<span class="hljs-number">9150</span>),
                            },
                            <span class="hljs-string">"ingress"</span>: pulumi.Map{
                                <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">false</span>),
                            },
                        },
                    },
                },
                <span class="hljs-string">"persistence"</span>: pulumi.Map{
                    <span class="hljs-string">"dataDir"</span>: pulumi.Map{
                        <span class="hljs-string">"enabled"</span>: pulumi.Bool(<span class="hljs-literal">true</span>),
                    },
                },
                <span class="hljs-string">"sidecarContainers"</span>: pulumi.Array{
                    pulumi.Map{
                        <span class="hljs-string">"name"</span>:  pulumi.String(<span class="hljs-string">"minecraft-exporter"</span>),
                        <span class="hljs-string">"image"</span>: pulumi.String(<span class="hljs-string">"ghcr.io/dirien/minecraft-exporter:0.18.0"</span>),
                        <span class="hljs-string">"volumeMounts"</span>: pulumi.Array{
                            pulumi.Map{
                                <span class="hljs-string">"name"</span>:      pulumi.String(<span class="hljs-string">"datadir"</span>),
                                <span class="hljs-string">"mountPath"</span>: pulumi.String(<span class="hljs-string">"/data"</span>),
                            },
                        },
                        <span class="hljs-string">"env"</span>: pulumi.Array{
                            pulumi.Map{
                                <span class="hljs-string">"name"</span>:  pulumi.String(<span class="hljs-string">"MC_WORLD"</span>),
                                <span class="hljs-string">"value"</span>: pulumi.String(<span class="hljs-string">"/data/world"</span>),
                            },
                            pulumi.Map{
                                <span class="hljs-string">"name"</span>:  pulumi.String(<span class="hljs-string">"MC_RCON_ADDRESS"</span>),
                                <span class="hljs-string">"value"</span>: pulumi.String(<span class="hljs-string">"localhost:25575"</span>),
                            },
                            pulumi.Map{
                                <span class="hljs-string">"name"</span>: pulumi.String(<span class="hljs-string">"MC_RCON_PASSWORD"</span>),
                                <span class="hljs-string">"valueFrom"</span>: pulumi.Map{
                                    <span class="hljs-string">"secretKeyRef"</span>: pulumi.Map{
                                        <span class="hljs-string">"name"</span>: pulumi.String(<span class="hljs-string">"minecraft-rcon"</span>),
                                        <span class="hljs-string">"key"</span>:  pulumi.String(<span class="hljs-string">"rcon-password"</span>),
                                    },
                                },
                            },
                        },
                    },
                },
            },
        }, pulumi.Provider(kubernetesProvider), pulumi.DependsOn([]pulumi.Resource{mcNamespace, kubePrometheusStack}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        mcSecretStore, err := apiextensions.NewCustomResource(ctx, <span class="hljs-string">"secret-store"</span>, &amp;apiextensions.CustomResourceArgs{
            ApiVersion: pulumi.String(<span class="hljs-string">"external-secrets.io/v1beta1"</span>),
            Kind:       pulumi.String(<span class="hljs-string">"SecretStore"</span>),
            Metadata: &amp;metav1.ObjectMetaArgs{
                Name:      pulumi.String(<span class="hljs-string">"secret-store"</span>),
                Namespace: mcNamespace.Metadata.Name(),
            },
            OtherFields: kubernetes.UntypedArgs{
                <span class="hljs-string">"spec"</span>: pulumi.Map{
                    <span class="hljs-string">"provider"</span>: pulumi.Map{
                        <span class="hljs-string">"scaleway"</span>: pulumi.Map{
                            <span class="hljs-string">"region"</span>:    pulumi.String(<span class="hljs-string">"fr-par"</span>),
                            <span class="hljs-string">"projectId"</span>: project.ID(),
                            <span class="hljs-string">"accessKey"</span>: pulumi.Map{
                                <span class="hljs-string">"value"</span>: key.AccessKey,
                            },
                            <span class="hljs-string">"secretKey"</span>: pulumi.Map{
                                <span class="hljs-string">"value"</span>: key.SecretKey,
                            },
                        },
                    },
                },
            },
        }, pulumi.Provider(kubernetesProvider), pulumi.DependsOn([]pulumi.Resource{mcNamespace, externalSecrets}))
        <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
            <span class="hljs-keyword">return</span> err
        }

        apiextensions.NewCustomResource(ctx, <span class="hljs-string">"minecraft-servicemonitor"</span>, &amp;apiextensions.CustomResourceArgs{
            ApiVersion: pulumi.String(<span class="hljs-string">"monitoring.coreos.com/v1"</span>),
            Kind:       pulumi.String(<span class="hljs-string">"ServiceMonitor"</span>),
            Metadata: &amp;metav1.ObjectMetaArgs{
                Name:      pulumi.String(<span class="hljs-string">"minecraft"</span>),
                Namespace: mcNamespace.Metadata.Name(),
            },
            OtherFields: kubernetes.UntypedArgs{
                <span class="hljs-string">"spec"</span>: pulumi.Map{
                    <span class="hljs-string">"selector"</span>: pulumi.Map{
                        <span class="hljs-string">"matchLabels"</span>: pulumi.Map{
                            <span class="hljs-string">"app"</span>: pulumi.All(mc.Name, mc.Namespace).ApplyT(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(args []<span class="hljs-keyword">interface</span>{})</span> <span class="hljs-title">string</span></span> {
                                <span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">"%s-%s"</span>, *args[<span class="hljs-number">0</span>].(*<span class="hljs-keyword">string</span>), *args[<span class="hljs-number">1</span>].(*<span class="hljs-keyword">string</span>))
                            }),
                        },
                    },
                    <span class="hljs-string">"endpoints"</span>: pulumi.Array{
                        pulumi.Map{
                            <span class="hljs-string">"targetPort"</span>: pulumi.Int(<span class="hljs-number">9150</span>),
                        },
                    },
                    <span class="hljs-string">"namespaceSelector"</span>: pulumi.Map{
                        <span class="hljs-string">"matchNames"</span>: pulumi.Array{
                            mcNamespace.Metadata.Name(),
                        },
                    },
                    <span class="hljs-string">"jobLabel"</span>: pulumi.String(<span class="hljs-string">"minecraft-exporter"</span>),
                },
            },
        }, pulumi.Provider(kubernetesProvider), pulumi.DependsOn([]pulumi.Resource{mcNamespace, mc, kubePrometheusStack}))

        apiextensions.NewCustomResource(ctx, <span class="hljs-string">"external-secrets"</span>, &amp;apiextensions.CustomResourceArgs{
            ApiVersion: pulumi.String(<span class="hljs-string">"external-secrets.io/v1beta1"</span>),
            Kind:       pulumi.String(<span class="hljs-string">"ExternalSecret"</span>),
            Metadata: &amp;metav1.ObjectMetaArgs{
                Name:      pulumi.String(<span class="hljs-string">"minecraft-rcon"</span>),
                Namespace: mcNamespace.Metadata.Name(),
            },
            OtherFields: kubernetes.UntypedArgs{
                <span class="hljs-string">"spec"</span>: pulumi.Map{
                    <span class="hljs-string">"secretStoreRef"</span>: pulumi.Map{
                        <span class="hljs-string">"name"</span>: mcSecretStore.Metadata.Name(),
                        <span class="hljs-string">"kind"</span>: mcSecretStore.Kind,
                    },
                    <span class="hljs-string">"target"</span>: pulumi.Map{
                        <span class="hljs-string">"name"</span>: pulumi.String(<span class="hljs-string">"minecraft-rcon"</span>),
                    },
                    <span class="hljs-string">"refreshInterval"</span>: pulumi.String(<span class="hljs-string">"20s"</span>),
                    <span class="hljs-string">"data"</span>: pulumi.Array{
                        pulumi.Map{
                            <span class="hljs-string">"secretKey"</span>: pulumi.String(<span class="hljs-string">"rcon-password"</span>),
                            <span class="hljs-string">"remoteRef"</span>: pulumi.Map{
                                <span class="hljs-string">"key"</span>:     pulumi.Sprintf(<span class="hljs-string">"name:%s"</span>, secret.Name),
                                <span class="hljs-string">"version"</span>: pulumi.String(<span class="hljs-string">"latest_enabled"</span>),
                            },
                        },
                    },
                },
            },
        }, pulumi.Provider(kubernetesProvider), pulumi.DependsOn([]pulumi.Resource{mcNamespace, externalSecrets}))
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>
    })
}
</code></pre>
<p>With everything in place, we can now run <code>pulumi up</code> to deploy our application. This can take a few minutes, so go grab a coffee or something.</p>
<pre><code class="lang-bash">➜ pulumi up -y -f        
Updating (dev)

View <span class="hljs-keyword">in</span> Browser (Ctrl+O): https://app.pulumi.com/dirien/pulumi-scaleway-kapsule/dev/updates/1

     Type                                                      Name                           Status              
 +   pulumi:pulumi:Stack                                       pulumi-scaleway-kapsule-dev    created (51s)       
 +   ├─ scaleway:index:AccountProject                          scaleway-project               created (0.65s)     
 +   ├─ scaleway:index:IamApplication                          scaleway-iam-application       created (0.73s)     
 +   ├─ scaleway:index:Secret                                  scaleway-secret                created (0.92s)     
 +   ├─ scaleway:index:K8sCluster                              k8s-cluster                    created (6s)        
 +   ├─ scaleway:index:Cockpit                                 scaleway-cockpit               created (47s)       
 +   ├─ scaleway:index:IamApiKey                               scaleway-iam-api-key           created (1s)        
 +   ├─ scaleway:index:IamPolicy                               scaleway-iam-policy            created (1s)        
 +   ├─ scaleway:index:SecretVersion                           scaleway-secret-version        created (1s)        
 +   ├─ scaleway:index:K8sPool                                 k8s-pool                       created (335s)      
 +   ├─ scaleway:index:CockpitGrafanaUser                      scaleway-cockpit-grafana-user  created (1s)        
 +   ├─ scaleway:index:CockpitToken                            scaleway-cockpit-token         created (0.83s)     
 +   ├─ pulumi:providers:kubernetes                            k8s-provider                   created (0.50s)     
 +   ├─ kubernetes:core/v1:Namespace                           minecraft                      created (2s)        
 +   ├─ kubernetes:helm.sh/v3:Release                          kube-prometheus-stack          created (76s)       
 +   ├─ kubernetes:helm.sh/v3:Release                          promtail                       created (3s)        
 +   ├─ kubernetes:helm.sh/v3:Release                          external-secrets               created (93s)       
 +   ├─ kubernetes:external-secrets.io/v1beta1:SecretStore     secret-store                   created (2s)        
 +   ├─ kubernetes:external-secrets.io/v1beta1:ExternalSecret  external-secrets               created (0.63s)     
 +   ├─ kubernetes:helm.sh/v3:Release                          minecraft                      created (2s)        
 +   └─ kubernetes:monitoring.coreos.com/v1:ServiceMonitor     minecraft-servicemonitor       created (0.67s)     


Outputs:
    grafana-password: [secret]
    kubeconfig      : [secret]

Resources:
    + 21 created

Duration: 9m4s
</code></pre>
<p>With the deployment finished, we can now access our Minecraft server. To do so, we need to get the IP address of the LoadBalancer that was created for us. First we need to get our <code>kubeconfig</code> file. To do so, we can run the following command:</p>
<pre><code class="lang-bash">pulumi stack output kubeconfig --show-secrets -s dev &gt; kubeconfig.yaml
</code></pre>
<p>After that, we can use the <code>kubectl</code> command to get the IP address of the LoadBalancer:</p>
<pre><code class="lang-bash">kubectl get services --all-namespaces | grep LoadBalancer | awk <span class="hljs-string">'{print $5}'</span>
</code></pre>
<h2 id="heading-testing">Testing</h2>
<p>After having the IP address, we can connect now to our Minecraft server. To do so, we can use the Minecraft client and add a new Server with the IP we got from the Pulumi stack output!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679497487499/8f7a1c56-000b-4f28-a839-19fd50011c4f.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679497722603/e69e0270-861b-4653-bd0b-1f14e0054da6.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679497738141/4b902dd7-de31-4501-b2b1-04022216c237.png" alt class="image--center mx-auto" /></p>
<p>Awesome, that worked fine! Now we come to the fun part!</p>
<p>Let us check the metrics of our Minecraft game server in the Scaleway Grafana. We need to get the password of the Grafana user. To do so, we can run the following command:</p>
<pre><code class="lang-bash">pulumi stack output grafana-password --show-secrets -s dev
</code></pre>
<p>Note this password. The login we set to <code>pulumi</code> in our <code>NewCockpitGrafanaUser</code> resource. Head over to the Scaleway console for Cockpit and click on the <code>"Open your dashboards"</code> button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679497803833/79cbc117-c57f-4d26-b6ea-38f4350174ad.png" alt class="image--center mx-auto" /></p>
<p>This will open Grafana for you. Enter the username and password we just got and press "Log in". You should now be in the Grafana dashboard.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679497887084/4652a6f7-c54e-4bba-a997-9b119d937420.png" alt class="image--center mx-auto" /></p>
<p>Here is an example dashboard I made for my Minecraft server:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"annotations"</span>: {
    <span class="hljs-attr">"list"</span>: [
      {
        <span class="hljs-attr">"builtIn"</span>: <span class="hljs-number">1</span>,
        <span class="hljs-attr">"datasource"</span>: {
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"datasource"</span>,
          <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"grafana"</span>
        },
        <span class="hljs-attr">"enable"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"hide"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"iconColor"</span>: <span class="hljs-string">"rgba(0, 211, 255, 1)"</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Annotations &amp; Alerts"</span>,
        <span class="hljs-attr">"target"</span>: {
          <span class="hljs-attr">"limit"</span>: <span class="hljs-number">100</span>,
          <span class="hljs-attr">"matchAny"</span>: <span class="hljs-literal">false</span>,
          <span class="hljs-attr">"tags"</span>: [],
          <span class="hljs-attr">"type"</span>: <span class="hljs-string">"dashboard"</span>
        },
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"dashboard"</span>
      }
    ]
  },
  <span class="hljs-attr">"editable"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"fiscalYearStartMonth"</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">"graphTooltip"</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">"id"</span>: <span class="hljs-number">19</span>,
  <span class="hljs-attr">"links"</span>: [],
  <span class="hljs-attr">"liveNow"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"panels"</span>: [
    {
      <span class="hljs-attr">"datasource"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"prometheus"</span>,
        <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"ag1UWWfVk"</span>
      },
      <span class="hljs-attr">"fieldConfig"</span>: {
        <span class="hljs-attr">"defaults"</span>: {
          <span class="hljs-attr">"color"</span>: {
            <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"palette-classic"</span>
          },
          <span class="hljs-attr">"custom"</span>: {
            <span class="hljs-attr">"axisCenteredZero"</span>: <span class="hljs-literal">false</span>,
            <span class="hljs-attr">"axisColorMode"</span>: <span class="hljs-string">"text"</span>,
            <span class="hljs-attr">"axisLabel"</span>: <span class="hljs-string">""</span>,
            <span class="hljs-attr">"axisPlacement"</span>: <span class="hljs-string">"auto"</span>,
            <span class="hljs-attr">"barAlignment"</span>: <span class="hljs-number">0</span>,
            <span class="hljs-attr">"drawStyle"</span>: <span class="hljs-string">"line"</span>,
            <span class="hljs-attr">"fillOpacity"</span>: <span class="hljs-number">0</span>,
            <span class="hljs-attr">"gradientMode"</span>: <span class="hljs-string">"none"</span>,
            <span class="hljs-attr">"hideFrom"</span>: {
              <span class="hljs-attr">"legend"</span>: <span class="hljs-literal">false</span>,
              <span class="hljs-attr">"tooltip"</span>: <span class="hljs-literal">false</span>,
              <span class="hljs-attr">"viz"</span>: <span class="hljs-literal">false</span>
            },
            <span class="hljs-attr">"lineInterpolation"</span>: <span class="hljs-string">"linear"</span>,
            <span class="hljs-attr">"lineWidth"</span>: <span class="hljs-number">1</span>,
            <span class="hljs-attr">"pointSize"</span>: <span class="hljs-number">5</span>,
            <span class="hljs-attr">"scaleDistribution"</span>: {
              <span class="hljs-attr">"type"</span>: <span class="hljs-string">"linear"</span>
            },
            <span class="hljs-attr">"showPoints"</span>: <span class="hljs-string">"auto"</span>,
            <span class="hljs-attr">"spanNulls"</span>: <span class="hljs-literal">false</span>,
            <span class="hljs-attr">"stacking"</span>: {
              <span class="hljs-attr">"group"</span>: <span class="hljs-string">"A"</span>,
              <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"none"</span>
            },
            <span class="hljs-attr">"thresholdsStyle"</span>: {
              <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"off"</span>
            }
          },
          <span class="hljs-attr">"mappings"</span>: [],
          <span class="hljs-attr">"thresholds"</span>: {
            <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"absolute"</span>,
            <span class="hljs-attr">"steps"</span>: [
              {
                <span class="hljs-attr">"color"</span>: <span class="hljs-string">"green"</span>,
                <span class="hljs-attr">"value"</span>: <span class="hljs-literal">null</span>
              },
              {
                <span class="hljs-attr">"color"</span>: <span class="hljs-string">"red"</span>,
                <span class="hljs-attr">"value"</span>: <span class="hljs-number">80</span>
              }
            ]
          }
        },
        <span class="hljs-attr">"overrides"</span>: []
      },
      <span class="hljs-attr">"gridPos"</span>: {
        <span class="hljs-attr">"h"</span>: <span class="hljs-number">8</span>,
        <span class="hljs-attr">"w"</span>: <span class="hljs-number">12</span>,
        <span class="hljs-attr">"x"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"y"</span>: <span class="hljs-number">0</span>
      },
      <span class="hljs-attr">"id"</span>: <span class="hljs-number">10</span>,
      <span class="hljs-attr">"options"</span>: {
        <span class="hljs-attr">"legend"</span>: {
          <span class="hljs-attr">"calcs"</span>: [],
          <span class="hljs-attr">"displayMode"</span>: <span class="hljs-string">"list"</span>,
          <span class="hljs-attr">"placement"</span>: <span class="hljs-string">"bottom"</span>,
          <span class="hljs-attr">"showLegend"</span>: <span class="hljs-literal">true</span>
        },
        <span class="hljs-attr">"tooltip"</span>: {
          <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"single"</span>,
          <span class="hljs-attr">"sort"</span>: <span class="hljs-string">"none"</span>
        }
      },
      <span class="hljs-attr">"targets"</span>: [
        {
          <span class="hljs-attr">"datasource"</span>: {
            <span class="hljs-attr">"type"</span>: <span class="hljs-string">"prometheus"</span>,
            <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"ag1UWWfVk"</span>
          },
          <span class="hljs-attr">"editorMode"</span>: <span class="hljs-string">"code"</span>,
          <span class="hljs-attr">"expr"</span>: <span class="hljs-string">"sum(minecraft_movement_meters_total{player=\"$player\"}) by (means)"</span>,
          <span class="hljs-attr">"legendFormat"</span>: <span class="hljs-string">"__auto"</span>,
          <span class="hljs-attr">"range"</span>: <span class="hljs-literal">true</span>,
          <span class="hljs-attr">"refId"</span>: <span class="hljs-string">"A"</span>
        }
      ],
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Movement"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"timeseries"</span>
    },
    {
      <span class="hljs-attr">"datasource"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"prometheus"</span>,
        <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"ag1UWWfVk"</span>
      },
      <span class="hljs-attr">"fieldConfig"</span>: {
        <span class="hljs-attr">"defaults"</span>: {
          <span class="hljs-attr">"color"</span>: {
            <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"thresholds"</span>
          },
          <span class="hljs-attr">"mappings"</span>: [
            {
              <span class="hljs-attr">"options"</span>: {
                <span class="hljs-attr">"match"</span>: <span class="hljs-string">"null"</span>,
                <span class="hljs-attr">"result"</span>: {
                  <span class="hljs-attr">"text"</span>: <span class="hljs-string">"N/A"</span>
                }
              },
              <span class="hljs-attr">"type"</span>: <span class="hljs-string">"special"</span>
            }
          ],
          <span class="hljs-attr">"thresholds"</span>: {
            <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"absolute"</span>,
            <span class="hljs-attr">"steps"</span>: [
              {
                <span class="hljs-attr">"color"</span>: <span class="hljs-string">"green"</span>,
                <span class="hljs-attr">"value"</span>: <span class="hljs-literal">null</span>
              },
              {
                <span class="hljs-attr">"color"</span>: <span class="hljs-string">"red"</span>,
                <span class="hljs-attr">"value"</span>: <span class="hljs-number">80</span>
              }
            ]
          },
          <span class="hljs-attr">"unit"</span>: <span class="hljs-string">"none"</span>
        },
        <span class="hljs-attr">"overrides"</span>: []
      },
      <span class="hljs-attr">"gridPos"</span>: {
        <span class="hljs-attr">"h"</span>: <span class="hljs-number">8</span>,
        <span class="hljs-attr">"w"</span>: <span class="hljs-number">4</span>,
        <span class="hljs-attr">"x"</span>: <span class="hljs-number">12</span>,
        <span class="hljs-attr">"y"</span>: <span class="hljs-number">0</span>
      },
      <span class="hljs-attr">"id"</span>: <span class="hljs-number">5</span>,
      <span class="hljs-attr">"links"</span>: [],
      <span class="hljs-attr">"maxDataPoints"</span>: <span class="hljs-number">100</span>,
      <span class="hljs-attr">"options"</span>: {
        <span class="hljs-attr">"colorMode"</span>: <span class="hljs-string">"none"</span>,
        <span class="hljs-attr">"graphMode"</span>: <span class="hljs-string">"none"</span>,
        <span class="hljs-attr">"justifyMode"</span>: <span class="hljs-string">"auto"</span>,
        <span class="hljs-attr">"orientation"</span>: <span class="hljs-string">"horizontal"</span>,
        <span class="hljs-attr">"reduceOptions"</span>: {
          <span class="hljs-attr">"calcs"</span>: [
            <span class="hljs-string">"mean"</span>
          ],
          <span class="hljs-attr">"fields"</span>: <span class="hljs-string">""</span>,
          <span class="hljs-attr">"values"</span>: <span class="hljs-literal">false</span>
        },
        <span class="hljs-attr">"textMode"</span>: <span class="hljs-string">"auto"</span>
      },
      <span class="hljs-attr">"pluginVersion"</span>: <span class="hljs-string">"9.3.1"</span>,
      <span class="hljs-attr">"targets"</span>: [
        {
          <span class="hljs-attr">"datasource"</span>: {
            <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"${DS_PROMETHEUS}"</span>
          },
          <span class="hljs-attr">"editorMode"</span>: <span class="hljs-string">"code"</span>,
          <span class="hljs-attr">"expr"</span>: <span class="hljs-string">"sum(minecraft_deaths_total{player=\"$player\"})"</span>,
          <span class="hljs-attr">"instant"</span>: <span class="hljs-literal">true</span>,
          <span class="hljs-attr">"legendFormat"</span>: <span class="hljs-string">""</span>,
          <span class="hljs-attr">"refId"</span>: <span class="hljs-string">"A"</span>
        }
      ],
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Deaths"</span>,
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"stat"</span>
    },
    {
      <span class="hljs-attr">"aliasColors"</span>: {},
      <span class="hljs-attr">"bars"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"dashLength"</span>: <span class="hljs-number">10</span>,
      <span class="hljs-attr">"dashes"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"datasource"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"prometheus"</span>,
        <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"ag1UWWfVk"</span>
      },
      <span class="hljs-attr">"fieldConfig"</span>: {
        <span class="hljs-attr">"defaults"</span>: {
          <span class="hljs-attr">"links"</span>: []
        },
        <span class="hljs-attr">"overrides"</span>: []
      },
      <span class="hljs-attr">"fill"</span>: <span class="hljs-number">1</span>,
      <span class="hljs-attr">"fillGradient"</span>: <span class="hljs-number">0</span>,
      <span class="hljs-attr">"gridPos"</span>: {
        <span class="hljs-attr">"h"</span>: <span class="hljs-number">8</span>,
        <span class="hljs-attr">"w"</span>: <span class="hljs-number">7</span>,
        <span class="hljs-attr">"x"</span>: <span class="hljs-number">16</span>,
        <span class="hljs-attr">"y"</span>: <span class="hljs-number">0</span>
      },
      <span class="hljs-attr">"hiddenSeries"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"id"</span>: <span class="hljs-number">4</span>,
      <span class="hljs-attr">"legend"</span>: {
        <span class="hljs-attr">"avg"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"current"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"max"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"min"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"show"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"total"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"values"</span>: <span class="hljs-literal">false</span>
      },
      <span class="hljs-attr">"lines"</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">"linewidth"</span>: <span class="hljs-number">1</span>,
      <span class="hljs-attr">"nullPointMode"</span>: <span class="hljs-string">"null"</span>,
      <span class="hljs-attr">"options"</span>: {
        <span class="hljs-attr">"alertThreshold"</span>: <span class="hljs-literal">true</span>
      },
      <span class="hljs-attr">"percentage"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"pluginVersion"</span>: <span class="hljs-string">"9.3.1"</span>,
      <span class="hljs-attr">"pointradius"</span>: <span class="hljs-number">2</span>,
      <span class="hljs-attr">"points"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"renderer"</span>: <span class="hljs-string">"flot"</span>,
      <span class="hljs-attr">"seriesOverrides"</span>: [],
      <span class="hljs-attr">"spaceLength"</span>: <span class="hljs-number">10</span>,
      <span class="hljs-attr">"stack"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"steppedLine"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"targets"</span>: [
        {
          <span class="hljs-attr">"datasource"</span>: {
            <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"${DS_PROMETHEUS}"</span>
          },
          <span class="hljs-attr">"editorMode"</span>: <span class="hljs-string">"code"</span>,
          <span class="hljs-attr">"expr"</span>: <span class="hljs-string">"sum(minecraft_item_actions_total{player=\"$player\", action=\"picked_up\"}) by (entity)"</span>,
          <span class="hljs-attr">"legendFormat"</span>: <span class="hljs-string">"{{block}}"</span>,
          <span class="hljs-attr">"range"</span>: <span class="hljs-literal">true</span>,
          <span class="hljs-attr">"refId"</span>: <span class="hljs-string">"A"</span>
        }
      ],
      <span class="hljs-attr">"thresholds"</span>: [],
      <span class="hljs-attr">"timeRegions"</span>: [],
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Blocks collected"</span>,
      <span class="hljs-attr">"tooltip"</span>: {
        <span class="hljs-attr">"shared"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"sort"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"value_type"</span>: <span class="hljs-string">"individual"</span>
      },
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"graph"</span>,
      <span class="hljs-attr">"xaxis"</span>: {
        <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"time"</span>,
        <span class="hljs-attr">"show"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"values"</span>: []
      },
      <span class="hljs-attr">"yaxes"</span>: [
        {
          <span class="hljs-attr">"format"</span>: <span class="hljs-string">"short"</span>,
          <span class="hljs-attr">"logBase"</span>: <span class="hljs-number">1</span>,
          <span class="hljs-attr">"show"</span>: <span class="hljs-literal">true</span>
        },
        {
          <span class="hljs-attr">"format"</span>: <span class="hljs-string">"short"</span>,
          <span class="hljs-attr">"logBase"</span>: <span class="hljs-number">1</span>,
          <span class="hljs-attr">"show"</span>: <span class="hljs-literal">true</span>
        }
      ],
      <span class="hljs-attr">"yaxis"</span>: {
        <span class="hljs-attr">"align"</span>: <span class="hljs-literal">false</span>
      }
    },
    {
      <span class="hljs-attr">"aliasColors"</span>: {},
      <span class="hljs-attr">"bars"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"dashLength"</span>: <span class="hljs-number">10</span>,
      <span class="hljs-attr">"dashes"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"datasource"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"prometheus"</span>,
        <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"ag1UWWfVk"</span>
      },
      <span class="hljs-attr">"fieldConfig"</span>: {
        <span class="hljs-attr">"defaults"</span>: {
          <span class="hljs-attr">"links"</span>: []
        },
        <span class="hljs-attr">"overrides"</span>: []
      },
      <span class="hljs-attr">"fill"</span>: <span class="hljs-number">1</span>,
      <span class="hljs-attr">"fillGradient"</span>: <span class="hljs-number">0</span>,
      <span class="hljs-attr">"gridPos"</span>: {
        <span class="hljs-attr">"h"</span>: <span class="hljs-number">7</span>,
        <span class="hljs-attr">"w"</span>: <span class="hljs-number">23</span>,
        <span class="hljs-attr">"x"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"y"</span>: <span class="hljs-number">8</span>
      },
      <span class="hljs-attr">"hiddenSeries"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"id"</span>: <span class="hljs-number">2</span>,
      <span class="hljs-attr">"legend"</span>: {
        <span class="hljs-attr">"avg"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"current"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"max"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"min"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"show"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"total"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"values"</span>: <span class="hljs-literal">false</span>
      },
      <span class="hljs-attr">"lines"</span>: <span class="hljs-literal">true</span>,
      <span class="hljs-attr">"linewidth"</span>: <span class="hljs-number">1</span>,
      <span class="hljs-attr">"nullPointMode"</span>: <span class="hljs-string">"null"</span>,
      <span class="hljs-attr">"options"</span>: {
        <span class="hljs-attr">"alertThreshold"</span>: <span class="hljs-literal">true</span>
      },
      <span class="hljs-attr">"percentage"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"pluginVersion"</span>: <span class="hljs-string">"9.3.1"</span>,
      <span class="hljs-attr">"pointradius"</span>: <span class="hljs-number">2</span>,
      <span class="hljs-attr">"points"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"renderer"</span>: <span class="hljs-string">"flot"</span>,
      <span class="hljs-attr">"seriesOverrides"</span>: [],
      <span class="hljs-attr">"spaceLength"</span>: <span class="hljs-number">10</span>,
      <span class="hljs-attr">"stack"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"steppedLine"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"targets"</span>: [
        {
          <span class="hljs-attr">"datasource"</span>: {
            <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"${DS_PROMETHEUS}"</span>
          },
          <span class="hljs-attr">"editorMode"</span>: <span class="hljs-string">"code"</span>,
          <span class="hljs-attr">"expr"</span>: <span class="hljs-string">"sum(minecraft_blocks_mined_total{player=\"$player\"}) by (block)"</span>,
          <span class="hljs-attr">"legendFormat"</span>: <span class="hljs-string">"{{block}}"</span>,
          <span class="hljs-attr">"range"</span>: <span class="hljs-literal">true</span>,
          <span class="hljs-attr">"refId"</span>: <span class="hljs-string">"A"</span>
        }
      ],
      <span class="hljs-attr">"thresholds"</span>: [],
      <span class="hljs-attr">"timeRegions"</span>: [],
      <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Blocks mined"</span>,
      <span class="hljs-attr">"tooltip"</span>: {
        <span class="hljs-attr">"shared"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"sort"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"value_type"</span>: <span class="hljs-string">"individual"</span>
      },
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"graph"</span>,
      <span class="hljs-attr">"xaxis"</span>: {
        <span class="hljs-attr">"mode"</span>: <span class="hljs-string">"time"</span>,
        <span class="hljs-attr">"show"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"values"</span>: []
      },
      <span class="hljs-attr">"yaxes"</span>: [
        {
          <span class="hljs-attr">"format"</span>: <span class="hljs-string">"short"</span>,
          <span class="hljs-attr">"logBase"</span>: <span class="hljs-number">1</span>,
          <span class="hljs-attr">"show"</span>: <span class="hljs-literal">true</span>
        },
        {
          <span class="hljs-attr">"format"</span>: <span class="hljs-string">"short"</span>,
          <span class="hljs-attr">"logBase"</span>: <span class="hljs-number">1</span>,
          <span class="hljs-attr">"show"</span>: <span class="hljs-literal">true</span>
        }
      ],
      <span class="hljs-attr">"yaxis"</span>: {
        <span class="hljs-attr">"align"</span>: <span class="hljs-literal">false</span>
      }
    }
  ],
  <span class="hljs-attr">"refresh"</span>: <span class="hljs-literal">false</span>,
  <span class="hljs-attr">"schemaVersion"</span>: <span class="hljs-number">37</span>,
  <span class="hljs-attr">"style"</span>: <span class="hljs-string">"dark"</span>,
  <span class="hljs-attr">"tags"</span>: [],
  <span class="hljs-attr">"templating"</span>: {
    <span class="hljs-attr">"list"</span>: [
      {
        <span class="hljs-attr">"current"</span>: {
          <span class="hljs-attr">"selected"</span>: <span class="hljs-literal">false</span>,
          <span class="hljs-attr">"text"</span>: <span class="hljs-string">"_diri"</span>,
          <span class="hljs-attr">"value"</span>: <span class="hljs-string">"_diri"</span>
        },
        <span class="hljs-attr">"definition"</span>: <span class="hljs-string">"minecraft_player_online_total"</span>,
        <span class="hljs-attr">"hide"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"includeAll"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"multi"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"player"</span>,
        <span class="hljs-attr">"options"</span>: [],
        <span class="hljs-attr">"query"</span>: {
          <span class="hljs-attr">"query"</span>: <span class="hljs-string">"minecraft_player_online_total"</span>,
          <span class="hljs-attr">"refId"</span>: <span class="hljs-string">"StandardVariableQuery"</span>
        },
        <span class="hljs-attr">"refresh"</span>: <span class="hljs-number">1</span>,
        <span class="hljs-attr">"regex"</span>: <span class="hljs-string">"/player=\"(?&lt;text&gt;[^\"]+)/"</span>,
        <span class="hljs-attr">"skipUrlSync"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"sort"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"tagValuesQuery"</span>: <span class="hljs-string">""</span>,
        <span class="hljs-attr">"tagsQuery"</span>: <span class="hljs-string">""</span>,
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"query"</span>,
        <span class="hljs-attr">"useTags"</span>: <span class="hljs-literal">false</span>
      }
    ]
  },
  <span class="hljs-attr">"time"</span>: {
    <span class="hljs-attr">"from"</span>: <span class="hljs-string">"now-6h"</span>,
    <span class="hljs-attr">"to"</span>: <span class="hljs-string">"now"</span>
  },
  <span class="hljs-attr">"timepicker"</span>: {
    <span class="hljs-attr">"refresh_intervals"</span>: [
      <span class="hljs-string">"5s"</span>,
      <span class="hljs-string">"10s"</span>,
      <span class="hljs-string">"30s"</span>,
      <span class="hljs-string">"1m"</span>,
      <span class="hljs-string">"5m"</span>,
      <span class="hljs-string">"15m"</span>,
      <span class="hljs-string">"30m"</span>,
      <span class="hljs-string">"1h"</span>,
      <span class="hljs-string">"2h"</span>,
      <span class="hljs-string">"1d"</span>
    ]
  },
  <span class="hljs-attr">"timezone"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"title"</span>: <span class="hljs-string">"minecraft Player stats"</span>,
  <span class="hljs-attr">"uid"</span>: <span class="hljs-string">"gAy914AZk"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-number">1</span>,
  <span class="hljs-attr">"weekStart"</span>: <span class="hljs-string">""</span>
}
</code></pre>
<p>This will give you dashboards looking like this. A good starting point to build your dashboards.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679498061857/e6a827d2-8f42-41ad-a4ef-473913420ff4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-housekeeping">Housekeeping</h2>
<p>When you are done recreating this blog post, you can delete the resources you created by running the following command:</p>
<pre><code class="lang-bash">pulumi destroy -y -f
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The new services from Scaleway are really great and easy to integrate into existing tools like <code>kube-prometheus-stack</code>, <code>promtail</code> and <code>external-secrets</code>.</p>
<p>They are still marked as <code>beta</code> though, so keep this in mind.</p>
<p>I hope you enjoyed this blog post and learned something new. If you have any questions or comments, feel free to reach out to me on Twitter or via email.</p>
]]></content:encoded></item></channel></rss>