<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Steven Hoang</title>
    <description>The latest articles on Forem by Steven Hoang (@baoduy2412).</description>
    <link>https://forem.com/baoduy2412</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F75949%2Ff02584e9-bc30-425f-ba0a-57a422df4ae4.jpeg</url>
      <title>Forem: Steven Hoang</title>
      <link>https://forem.com/baoduy2412</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/baoduy2412"/>
    <language>en</language>
    <item>
      <title>[Tools] Drunk Charts: A Reusable Helm Library for Kubernetes Deployments</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:30:08 +0000</pubDate>
      <link>https://forem.com/baoduy2412/tools-drunk-charts-a-reusable-helm-library-for-kubernetes-deployments-4mjh</link>
      <guid>https://forem.com/baoduy2412/tools-drunk-charts-a-reusable-helm-library-for-kubernetes-deployments-4mjh</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you've managed multiple microservices on Kubernetes, you've likely felt the pain of maintaining dozens of near-identical Helm charts — each with the same Deployment, Service, Ingress, and HPA templates copied and slightly tweaked. It's tedious, error-prone, and hard to keep consistent.&lt;/p&gt;

&lt;p&gt;To solve this, I built &lt;strong&gt;&lt;a href="https://github.com/baoduy/drunk.charts" rel="noopener noreferrer"&gt;drunk.charts&lt;/a&gt;&lt;/strong&gt; — a set of Helm charts anchored by a &lt;strong&gt;library chart&lt;/strong&gt; (&lt;code&gt;drunk-lib&lt;/code&gt;) that provides standardized, reusable templates, and a thin &lt;strong&gt;application chart&lt;/strong&gt; (&lt;code&gt;drunk-app&lt;/code&gt;) that consumes it. Deploy any application by simply providing a &lt;code&gt;values.yaml&lt;/code&gt; — no template duplication required.&lt;/p&gt;

&lt;p&gt;All charts are published as &lt;strong&gt;OCI artifacts&lt;/strong&gt; on GitHub Container Registry at &lt;code&gt;oci://ghcr.io/baoduy&lt;/code&gt; and are available on &lt;a href="https://artifacthub.io/" rel="noopener noreferrer"&gt;Artifact Hub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

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

&lt;p&gt;The design follows Helm's &lt;a href="https://helm.sh/docs/topics/library_charts/" rel="noopener noreferrer"&gt;library chart pattern&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;drunk-lib (library chart — type: library)
  └── 19 named templates: drunk-lib.&amp;lt;resource-type&amp;gt;
  └── Published as OCI: oci://ghcr.io/baoduy/drunk-lib

drunk-app (application chart — type: application)
  └── Thin wrapper: each template is a single include of drunk-lib.&amp;lt;name&amp;gt;
  └── Declares drunk-lib as a dependency
  └── Published as OCI: oci://ghcr.io/baoduy/drunk-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation means you fix a bug or add a feature in &lt;code&gt;drunk-lib&lt;/code&gt; once, bump the version, and every &lt;code&gt;drunk-app&lt;/code&gt; deployment inherits it on the next &lt;code&gt;helm dependency update&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  drunk-lib: The Library Chart
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;drunk-lib&lt;/code&gt; (v1.3.0) is a Helm &lt;strong&gt;library&lt;/strong&gt; chart — it produces no resources on its own. Instead, it provides named templates that other charts invoke via &lt;code&gt;{{ include "drunk-lib.&amp;lt;name&amp;gt;" . }}&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Available Templates
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Template&lt;/th&gt;
&lt;th&gt;Resource Generated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core Workloads&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.deployment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apps/v1 Deployment&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.statefulset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;apps/v1 StatefulSet&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Networking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1 Service&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.ingress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;networking.k8s.io/v1 Ingress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.gateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gateway.networking.k8s.io/v1 Gateway&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.httproute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gateway.networking.k8s.io/v1 HTTPRoute&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.backendTlsPolicy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gateway.networking.k8s.io BackendTLSPolicy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Configuration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.configMap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1 ConfigMap&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.secrets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1 Secret&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.secretProviderClass&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SecretProviderClass&lt;/code&gt; (CSI Driver)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.tls-secrets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v1 Secret&lt;/code&gt; (TLS type)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.volumes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1 PersistentVolumeClaim&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.cronjob&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;batch/v1 CronJob&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.job&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;batch/v1 Job&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.hpa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;autoscaling/v2 HorizontalPodAutoscaler&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.networkPolicy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;networking.k8s.io/v1 NetworkPolicy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.serviceAccount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1 ServiceAccount&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drunk-lib.imagePull-secret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v1 Secret&lt;/code&gt; (dockerconfigjson)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Helper Functions
&lt;/h3&gt;

&lt;p&gt;The chart also provides shared helper templates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;app.name&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;app.fullname&lt;/code&gt;&lt;/strong&gt; — consistent naming across all resources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;app.labels&lt;/code&gt;&lt;/strong&gt; — standard Kubernetes labels (app.kubernetes.io/*)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;app.selectorLabels&lt;/code&gt;&lt;/strong&gt; — selector labels for pod matching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;app.checksums&lt;/code&gt;&lt;/strong&gt; — annotations that trigger pod rollouts when ConfigMap/Secret content changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Templates Work
&lt;/h3&gt;

&lt;p&gt;Each template is self-contained and gate-controlled. For example, the Deployment template:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks &lt;code&gt;.Values.deployment.enabled&lt;/code&gt; — no-op if false&lt;/li&gt;
&lt;li&gt;Composes pod metadata using &lt;code&gt;app.labels&lt;/code&gt; / &lt;code&gt;app.selectorLabels&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Attaches checksum annotations via &lt;code&gt;app.checksums&lt;/code&gt; so ConfigMap/Secret changes trigger rollouts&lt;/li&gt;
&lt;li&gt;Wires init containers from &lt;code&gt;.Values.global.initContainer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pulls environment from &lt;code&gt;configMap&lt;/code&gt;, &lt;code&gt;secrets&lt;/code&gt;, &lt;code&gt;configFrom[]&lt;/code&gt;, &lt;code&gt;secretFrom[]&lt;/code&gt;, and CSI &lt;code&gt;secretProvider&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reads scheduling (&lt;code&gt;nodeSelector&lt;/code&gt;, &lt;code&gt;affinity&lt;/code&gt;, &lt;code&gt;tolerations&lt;/code&gt;), security contexts, resources, and volumes from &lt;strong&gt;root-level&lt;/strong&gt; values — shared with sibling resources (Jobs, CronJobs)&lt;/li&gt;
&lt;li&gt;Hard-codes &lt;code&gt;automountServiceAccountToken: false&lt;/code&gt; for security&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Deployment Template — Full Schema
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bool&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gate for the entire resource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.replicaCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pod replicas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.strategy.type&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RollingUpdate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RollingUpdate&lt;/code&gt; or &lt;code&gt;Recreate&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.strategy.maxSurge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int/string&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rolling update config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.strategy.maxUnavailable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int/string&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rolling update config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.ports&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;map&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;name: containerPort&lt;/code&gt; pairs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.command&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Container command override&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.args&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Container args override&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.liveness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;HTTP path for liveness probe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.readiness&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;HTTP path for readiness probe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment.podAnnotations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;map&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Extra pod annotations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  StatefulSet Template
&lt;/h3&gt;

&lt;p&gt;For workloads requiring stable network identity and persistent storage. Same pod spec composition as Deployment, with added &lt;code&gt;volumeClaimTemplates&lt;/code&gt; support and headless Service generation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Defaults
&lt;/h3&gt;

&lt;p&gt;All templates enforce security best practices out of the box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;podSecurityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fsGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;
  &lt;span class="na"&gt;runAsUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;
  &lt;span class="na"&gt;runAsGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;

&lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALL"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;readOnlyRootFilesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using drunk-lib in Your Own Charts
&lt;/h3&gt;

&lt;p&gt;To build a custom chart on top of &lt;code&gt;drunk-lib&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Chart.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v2&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-custom-chart&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application&lt;/span&gt;
&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drunk-lib&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.3.0"&lt;/span&gt;
    &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oci://ghcr.io/baoduy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your templates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# templates/deployment.yaml&lt;/span&gt;
&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- include "drunk-lib.deployment" . -&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;

&lt;span class="c1"&gt;# templates/service.yaml&lt;/span&gt;
&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- include "drunk-lib.service" . -&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  drunk-app: The Application Chart
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;drunk-app&lt;/code&gt; (v1.3.5) is the ready-to-use chart for deploying applications. It's intentionally a &lt;strong&gt;thin wrapper&lt;/strong&gt; — every template file is a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- include "drunk-lib.deployment" . -&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means you never write templates. You configure everything through &lt;code&gt;values.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From OCI registry (recommended)&lt;/span&gt;
helm &lt;span class="nb"&gt;install &lt;/span&gt;my-app oci://ghcr.io/baoduy/drunk-app &lt;span class="nt"&gt;--version&lt;/span&gt; 1.3.5 &lt;span class="nt"&gt;-f&lt;/span&gt; values.yaml

&lt;span class="c"&gt;# Or via Helm repo&lt;/span&gt;
helm repo add drunk-charts https://baoduy.github.io/drunk.charts/drunk-app
helm repo update
helm &lt;span class="nb"&gt;install &lt;/span&gt;my-app drunk-charts/drunk-app &lt;span class="nt"&gt;-f&lt;/span&gt; values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Minimal Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-registry.azurecr.io/my-app&lt;/span&gt;
  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0"&lt;/span&gt;

&lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;

&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIP&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tmp&lt;/span&gt;
    &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
    &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;tmp&lt;/code&gt; volume is essential because &lt;code&gt;readOnlyRootFilesystem: true&lt;/code&gt; is the default — your app needs a writable &lt;code&gt;/tmp&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Inline environment variables&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;APP_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
  &lt;span class="na"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;

&lt;span class="c1"&gt;# Inline secrets (stored as K8s Secret)&lt;/span&gt;
&lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;DB_CONNECTION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Server=db;Database=mydb;User=admin;Password=secret"&lt;/span&gt;

&lt;span class="c1"&gt;# Reference existing secrets&lt;/span&gt;
&lt;span class="na"&gt;secretFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;existing-secret&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;connection-string&lt;/span&gt;
    &lt;span class="na"&gt;envName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DB_CONNECTION&lt;/span&gt;

&lt;span class="c1"&gt;# Reference existing configmaps&lt;/span&gt;
&lt;span class="na"&gt;configFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared-config&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-url&lt;/span&gt;
    &lt;span class="na"&gt;envName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API_URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Health Checks and Autoscaling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;liveness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/healthz&lt;/span&gt;
  &lt;span class="na"&gt;readiness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/ready&lt;/span&gt;

&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;500m&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512Mi&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100m&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128Mi&lt;/span&gt;

&lt;span class="na"&gt;autoscaling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;minReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;maxReplicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;targetCPUUtilizationPercentage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;70&lt;/span&gt;
  &lt;span class="na"&gt;targetMemoryUtilizationPercentage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Azure Key Vault Integration (CSI Driver)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;secretProvider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-tenant-id"&lt;/span&gt;
  &lt;span class="na"&gt;vaultName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-keyvault"&lt;/span&gt;
  &lt;span class="na"&gt;useWorkloadIdentity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;objects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-connection-string&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
      &lt;span class="na"&gt;envName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DB_CONNECTION&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-key&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
      &lt;span class="na"&gt;envName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TLS Secrets Management
&lt;/h3&gt;

&lt;p&gt;Three modes supported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tlsSecrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Mode 1: Inline base64-encoded certificates&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-tls&lt;/span&gt;
    &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;base64-encoded-cert&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;base64-encoded-key&amp;gt;&lt;/span&gt;

  &lt;span class="c1"&gt;# Mode 2: File-based (populate via --set-file during helm install)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-tls-file&lt;/span&gt;
    &lt;span class="na"&gt;certFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;

  &lt;span class="c1"&gt;# Mode 3: CA certificate only (for backend TLS validation)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-ca&lt;/span&gt;
    &lt;span class="na"&gt;ca&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;base64-encoded-ca&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gateway API with TLS Validation
&lt;/h3&gt;

&lt;p&gt;For clusters using the Gateway API instead of traditional Ingress:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;httpRoute&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;hostnames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-app.drunkcoding.net&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;backendRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="na"&gt;parentRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main-gateway&lt;/span&gt;
      &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway-system&lt;/span&gt;
  &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app.drunkcoding.net&lt;/span&gt;
    &lt;span class="na"&gt;wellKnownCACertificates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;System&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CronJobs and Jobs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cronJobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nightly-cleanup&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./cleanup.sh"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;200m&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256Mi&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-migration&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dotnet"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ef"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Network Policies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;networkPolicies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow-ingress-only&lt;/span&gt;
    &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app&lt;/span&gt;
    &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ingress-system&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
    &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Init Containers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-registry.azurecr.io/my-app&lt;/span&gt;
  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0"&lt;/span&gt;
  &lt;span class="na"&gt;initContainer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;busybox&lt;/span&gt;
    &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;initializing"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Claude Code AI Plugins
&lt;/h2&gt;

&lt;p&gt;Both charts ship with &lt;strong&gt;Claude Code plugins&lt;/strong&gt; that provide AI-assisted configuration. These are available on the &lt;a href="https://github.com/baoduy/drunk.charts" rel="noopener noreferrer"&gt;Claude Code Plugin Marketplace&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing the Plugins
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add from marketplace&lt;/span&gt;
plugin marketplace add baoduy/drunk.charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  drunk-app Plugin
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;drunk-app&lt;/code&gt; plugin provides an AI assistant with three modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Answer&lt;/strong&gt; — Ask "how does X work?" and get explanations with YAML snippets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate&lt;/strong&gt; — Say "give me a values.yaml for [use case]" and get a complete, validated configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt; — Paste a values.yaml and get a full validation report with fix instructions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example interactions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/drunk-app generate a values.yaml for a .NET API with health checks, 
autoscaling, and Azure Key Vault secrets

/drunk-app validate my values.yaml — is the probe configuration correct?

/drunk-app how do I configure multiple network policies?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  drunk-lib Plugin
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;drunk-lib&lt;/code&gt; plugin provides &lt;strong&gt;per-template expert skills&lt;/strong&gt; — 18 specialized assistants, one for each resource type:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&lt;/th&gt;
&lt;th&gt;Expertise&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-deployment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment workloads, rolling updates, pod spec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-statefulset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stateful workloads, headless services, volume claims&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-service&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Service types, port mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-ingress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nginx ingress, TLS, path routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-httproute&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gateway API HTTPRoute configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-gateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Gateway resource setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-backend-tls-policy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Backend TLS validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-hpa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Horizontal Pod Autoscaler tuning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-cronjob&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scheduled batch workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-job&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One-off batch workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-configmap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ConfigMap from values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-secrets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Secret management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-secretprovider&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CSI Secret Store (Azure Key Vault, AWS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-tls-secrets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TLS certificate management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-networkpolicy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Network isolation rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-serviceaccount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ServiceAccount configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-imagepull-secret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Private registry credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drunk-lib-volumes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PVC and volume management&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each skill contains the &lt;strong&gt;exact values schema&lt;/strong&gt; consumed by that template, the rendering logic, validation rules, and generation templates — so Claude can produce correct configurations without guessing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a Library Chart?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Copy-paste templates per service&lt;/td&gt;
&lt;td&gt;Full control&lt;/td&gt;
&lt;td&gt;Drift, maintenance nightmare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Umbrella chart with subcharts&lt;/td&gt;
&lt;td&gt;Organized&lt;/td&gt;
&lt;td&gt;Heavy, complex dependency management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Library chart + thin wrapper&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DRY, consistent, flexible&lt;/td&gt;
&lt;td&gt;Slightly opinionated defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The library chart pattern gives you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single source of truth&lt;/strong&gt; — fix a bug in &lt;code&gt;drunk-lib&lt;/code&gt;, all apps get it on next upgrade&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt; — every service follows the same security, labeling, and resource patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; — new services need only a &lt;code&gt;values.yaml&lt;/code&gt;, no template work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibility&lt;/strong&gt; — disable any resource by setting &lt;code&gt;enabled: false&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-ready&lt;/strong&gt; — Claude Code plugins understand the exact schema and can generate/validate configs&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  OCI Distribution
&lt;/h2&gt;

&lt;p&gt;All charts are published to GitHub Container Registry as OCI artifacts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pull directly&lt;/span&gt;
helm pull oci://ghcr.io/baoduy/drunk-app &lt;span class="nt"&gt;--version&lt;/span&gt; 1.3.5
helm pull oci://ghcr.io/baoduy/drunk-lib &lt;span class="nt"&gt;--version&lt;/span&gt; 1.3.0

&lt;span class="c"&gt;# Use as dependency&lt;/span&gt;
&lt;span class="c"&gt;# Chart.yaml&lt;/span&gt;
dependencies:
  - name: drunk-lib
    version: &lt;span class="s2"&gt;"1.3.0"&lt;/span&gt;
    repository: &lt;span class="s2"&gt;"oci://ghcr.io/baoduy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Published packages are visible at &lt;a href="https://github.com/baoduy?tab=packages&amp;amp;repo_name=drunk.charts" rel="noopener noreferrer"&gt;github.com/baoduy?tab=packages&amp;amp;repo_name=drunk.charts&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;drunk.charts&lt;/code&gt; eliminates the boilerplate of Kubernetes deployments while maintaining full flexibility. The &lt;code&gt;drunk-lib&lt;/code&gt; library chart provides battle-tested templates with security-first defaults, and &lt;code&gt;drunk-app&lt;/code&gt; makes them instantly usable with just a &lt;code&gt;values.yaml&lt;/code&gt;. Combined with Claude Code AI plugins that understand the exact schema, you can generate and validate configurations with confidence.&lt;/p&gt;

&lt;p&gt;The charts are open source and actively maintained. Contributions and feedback are welcome on &lt;a href="https://github.com/baoduy/drunk.charts" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>helm</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>[DevOps] Automating Branch Cleanup in Azure DevOps with Node.js</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:29:35 +0000</pubDate>
      <link>https://forem.com/baoduy2412/devops-automating-branch-cleanup-in-azure-devops-with-nodejs-4bda</link>
      <guid>https://forem.com/baoduy2412/devops-automating-branch-cleanup-in-azure-devops-with-nodejs-4bda</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As software projects evolve, Git repositories can become cluttered with outdated or redundant branches. This clutter makes repository navigation cumbersome and can introduce confusion or errors in the development process. Automating the cleanup of these branches helps maintain an organized and efficient development environment.&lt;/p&gt;

&lt;p&gt;In this guide, we'll walk through setting up a TypeScript script that automatically deletes old, unnecessary branches in Azure DevOps. We'll cover the essential steps, focusing on the implementation and automation of the cleanup process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Why Automate Branch Cleanup?
&lt;/h2&gt;

&lt;p&gt;Automating branch cleanup is essential for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduce Clutter&lt;/strong&gt;: Keeps the repository clean, making it easier for developers to navigate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve Performance&lt;/strong&gt;: Enhances CI/CD pipeline performance by reducing overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevent Confusion&lt;/strong&gt;: Minimizes the risk of developers working on or merging outdated branches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhance Security&lt;/strong&gt;: Removes obsolete branches that may contain vulnerabilities.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Ensure you have the following before starting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure DevOps Account&lt;/strong&gt;: Access to your organization's Azure DevOps instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal Access Token (PAT)&lt;/strong&gt;: A PAT with permissions to access and manage repositories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js and npm&lt;/strong&gt;: Installed on your machine (Node.js version 14 or later).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt;: Installed globally (&lt;code&gt;npm install -g typescript&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure DevOps Node API Package&lt;/strong&gt;: Install via &lt;code&gt;npm install azure-devops-node-api&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dotenv Package&lt;/strong&gt;: Install via &lt;code&gt;npm install dotenv&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project Setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a New Directory&lt;/strong&gt;: Initialize a new Node.js project.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;mkdir &lt;/span&gt;azure-devops-branch-cleanup
   &lt;span class="nb"&gt;cd &lt;/span&gt;azure-devops-branch-cleanup
   npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install Dependencies&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   npm &lt;span class="nb"&gt;install&lt;/span&gt; @azure/identity @microsoft/microsoft-graph-client azure-devops-node-api dayjs dotenv
   npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; typescript @types/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuration File
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;config.json&lt;/code&gt; file in your project root to specify branches that should be excluded from deletion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"globalExcludes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"master"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"develop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"release"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repositoryExcludes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"your-repo-name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"feature/important-branch"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;globalExcludes&lt;/strong&gt;: Branches excluded from deletion across all repositories.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;repositoryExcludes&lt;/strong&gt;: Specific branches to exclude in specific repositories.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementing the TypeScript Script
&lt;/h2&gt;

&lt;p&gt;Create a TypeScript file, e.g., &lt;code&gt;cleanup.ts&lt;/code&gt;, and implement the following steps:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Loading Environment Variables
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;dotenv&lt;/code&gt; package to load environment variables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dotenv&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDryRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DryRun&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Environment Variables Required&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AZURE_DEVOPS_URL&lt;/code&gt;: Your Azure DevOps organization URL.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AZURE_DEVOPS_PAT&lt;/code&gt;: Your Personal Access Token.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AZURE_DEVOPS_PROJECT&lt;/code&gt;: Your project name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DryRun&lt;/code&gt;: Set to &lt;code&gt;"true"&lt;/code&gt; for dry-run mode (no actual deletions).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Defining the Configuration Interface
&lt;/h3&gt;

&lt;p&gt;Define an interface to ensure type safety.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;globalExcludes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;repositoryExcludes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="na"&gt;repoName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Setting Constants
&lt;/h3&gt;

&lt;p&gt;Define constants used in the script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DAYS_90_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 90 days in milliseconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Getting the Git API Client
&lt;/h3&gt;

&lt;p&gt;Authenticate and obtain the Git API client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;azdev&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;azure-devops-node-api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;GitApi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;azure-devops-node-api/GitApi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getGitApi&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IGitApi&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orgUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AZURE_DEVOPS_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AZURE_DEVOPS_PAT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;orgUrl&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Azure DevOps URL or PAT is not set in environment variables.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azdev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPersonalAccessTokenHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azdev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WebApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orgUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authHandler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getGitApi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Loading the Configuration
&lt;/h3&gt;

&lt;p&gt;Load the &lt;code&gt;config.json&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadConfig&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;configPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;configContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;configPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;configContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Retrieving Repositories and Branches
&lt;/h3&gt;

&lt;p&gt;Get the list of repositories and branches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;GitInterfaces&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;azure-devops-node-api/interfaces/GitInterfaces&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRepositories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IGitApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GitInterfaces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GitRepository&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRepositories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBranches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IGitApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;repoId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GitInterfaces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GitRef&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;branches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRefs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refs/heads/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Determining the Last Commit Date
&lt;/h3&gt;

&lt;p&gt;Get the date of the last commit on a branch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getLastCommitDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IGitApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;repoId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;branchName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCommits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;repoId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;itemVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;branchName&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;committer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commitDate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. Checking if a Branch is Merged
&lt;/h3&gt;

&lt;p&gt;Check if a branch is merged into any of the target branches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GitVersionType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;azure-devops-node-api/interfaces/GitInterfaces&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isBranchMerged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IGitApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;repoId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;targetBranches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetBranch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;targetBranches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCommitDiffs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;repoId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;baseVersionType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GitVersionType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;baseVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;targetVersionType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GitVersionType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;targetVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;targetBranch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aheadCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  9. Deleting a Branch
&lt;/h3&gt;

&lt;p&gt;Delete the branch if it meets the criteria.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deleteBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IGitApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;repoId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GitInterfaces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GitRef&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isDryRun&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLocked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isLocked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nx"&gt;repoId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;project&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateRefs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;newObjectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0000000000000000000000000000000000000000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;oldObjectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="nx"&gt;repoId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;project&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Deleted branch: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (Dry Run: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;isDryRun&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  10. Compiling the Exclusion List
&lt;/h3&gt;

&lt;p&gt;Combine global and repository-specific exclusions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getExclusionList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repoName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;globalExcludes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalExcludes&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repoSpecificExcludes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repositoryExcludes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;repoName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;globalExcludes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;repoSpecificExcludes&lt;/span&gt;&lt;span class="p"&gt;])];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  11. Cleaning Up Branches
&lt;/h3&gt;

&lt;p&gt;Main function orchestrating the cleanup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cleanUpBranches&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AZURE_DEVOPS_PROJECT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Azure DevOps project name is not set in environment variables.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gitApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getGitApi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repositories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getRepositories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;repositories&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Processing repository: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;excludeBranches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getExclusionList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;branches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBranches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;branchName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refs/heads/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;excludeBranches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;branchName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Skipping excluded branch: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;branchName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastCommitDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getLastCommitDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;branchName&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;lastCommitDate&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
        &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastCommitDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;DAYS_90_MS&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Branch is recent or active: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;branchName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isMerged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;isBranchMerged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;branchName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalExcludes&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isMerged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;deleteBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gitApi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Branch is not merged: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;branchName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;cleanUpBranches&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;An error occurred:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Automating with Azure DevOps Pipeline
&lt;/h2&gt;

&lt;p&gt;To automate the script execution, set up an Azure DevOps Pipeline.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a Variable Group&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Pipelines&lt;/strong&gt; &amp;gt; &lt;strong&gt;Library&lt;/strong&gt; in Azure DevOps.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Variable groups"&lt;/strong&gt; &amp;gt; &lt;strong&gt;"Add variable group"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name the group, e.g., &lt;code&gt;az-devops&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add the variables:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AZURE_DEVOPS_URL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AZURE_DEVOPS_PAT&lt;/code&gt; (set as secret)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_DEVOPS_PROJECT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Save the variable group.&lt;/li&gt;

&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the Pipeline YAML File&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create a &lt;code&gt;azure-pipelines.yml&lt;/code&gt; file in your repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;   &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;

   &lt;span class="na"&gt;schedules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt; &lt;span class="c1"&gt;# Runs every Sunday at 00:00&lt;/span&gt;
       &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Weekly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Branch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Cleanup"&lt;/span&gt;
       &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
           &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
       &lt;span class="na"&gt;always&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
       &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

   &lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;vmImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

   &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;az-devops&lt;/span&gt;

   &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodeTool@0&lt;/span&gt;
       &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="na"&gt;versionSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;14.x"&lt;/span&gt;
       &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Install&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Node.js"&lt;/span&gt;

     &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
         &lt;span class="s"&gt;npm ci&lt;/span&gt;
         &lt;span class="s"&gt;npx ts-node cleanup.ts&lt;/span&gt;
       &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Branch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Cleanup&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Script"&lt;/span&gt;
       &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="na"&gt;AZURE_DEVOPS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(AZURE_DEVOPS_URL)&lt;/span&gt;
         &lt;span class="na"&gt;AZURE_DEVOPS_PAT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(AZURE_DEVOPS_PAT)&lt;/span&gt;
         &lt;span class="na"&gt;AZURE_DEVOPS_PROJECT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(AZURE_DEVOPS_PROJECT)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Replace &lt;code&gt;cleanup.ts&lt;/code&gt; with the path to your script.&lt;/li&gt;
&lt;li&gt;Ensure the pipeline has access to the variable group.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

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

&lt;p&gt;Automating branch cleanup ensures your repositories remain organized, improving developer productivity and reducing potential errors. By following this guide, you can set up a script to automatically identify and delete old, unused branches in Azure DevOps, and schedule it using Azure Pipelines for regular maintenance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefits&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Efficiency&lt;/strong&gt;: Saves time and resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;: Maintains a consistent repository state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Easily extends to multiple projects and repositories.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full Working Source Code&lt;/strong&gt;: &lt;a href="https://dev.azure.com/drunk24/drunkcoding-public/_git/az.tools?path=/az-devops-delete-branches&amp;amp;version=GBmain" rel="noopener noreferrer"&gt;drunkcoding public code&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure DevOps Node API Documentation&lt;/strong&gt;: &lt;a href="https://github.com/microsoft/azure-devops-node-api/blob/master/api/GitApi.ts" rel="noopener noreferrer"&gt;Git API Reference&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure DevOps REST API Reference&lt;/strong&gt;: &lt;a href="https://docs.microsoft.com/en-us/rest/api/azure/devops/git/repositories" rel="noopener noreferrer"&gt;Git Repositories&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Always test scripts in a controlled environment before deploying them in production. Ensure compliance with your organization's policies and procedures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azuredevops</category>
      <category>repocleanup</category>
      <category>tools</category>
    </item>
    <item>
      <title>[Tools] Cleaning Up Azure Service Bus Dead-Letter Queues with .NET</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:29:01 +0000</pubDate>
      <link>https://forem.com/baoduy2412/tools-cleaning-up-azure-service-bus-dead-letter-queues-with-net-5e0e</link>
      <guid>https://forem.com/baoduy2412/tools-cleaning-up-azure-service-bus-dead-letter-queues-with-net-5e0e</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In cloud-based applications, message queues are critical for enabling reliable, asynchronous communication between services. &lt;strong&gt;Azure Service Bus&lt;/strong&gt; is a robust messaging platform that facilitates this communication in distributed systems. However, messages that cannot be processed or delivered successfully may end up in the &lt;strong&gt;Dead-Letter Queue (DLQ)&lt;/strong&gt;. If left unmanaged, these dead-letter messages can accumulate, leading to storage issues and degraded system performance.&lt;/p&gt;

&lt;p&gt;In this article, we'll explore the importance of regularly cleaning up dead-letter queues in Azure Service Bus. We'll guide you through implementing a .NET background service that automates this cleanup process by moving dead-letter messages to Azure Blob Storage. This approach ensures your messaging system remains efficient while preserving problematic messages for future analysis or reprocessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Understanding Dead-Letter Queues
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Dead-Letter Queue (DLQ)&lt;/strong&gt; is a sub-queue associated with each Azure Service Bus entity (queue or topic subscription). It holds messages that cannot be delivered or processed successfully. Messages may be moved to the DLQ for several reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exceeding Maximum Delivery Attempts&lt;/strong&gt;: A message is retried multiple times but still cannot be processed successfully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message Expiration&lt;/strong&gt;: The message's &lt;strong&gt;Time to Live (TTL)&lt;/strong&gt; expires before it is processed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter Violations&lt;/strong&gt;: The message does not match the filter criteria of a subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing Errors&lt;/strong&gt;: An application explicitly moves a message to the DLQ due to a processing failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By design, the DLQ provides a way to isolate faulty messages, allowing your system to continue processing valid messages without interruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Regularly Clean Up Dead-Letter Messages?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Prevent Storage Overruns
&lt;/h3&gt;

&lt;p&gt;Dead-letter messages accumulate over time, consuming storage resources. If left unchecked, this can lead to a &lt;code&gt;QuotaExceededException&lt;/code&gt;, where the maximum size limit for a Service Bus entity is reached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Microsoft.Azure.ServiceBus.QuotaExceededException:
The maximum entity size has been reached or exceeded for Topic:
'YourTopicName'. Size of entity in bytes: 2147489161, Max entity size in bytes: 2147483648.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This exception can disrupt normal operations, preventing new messages from being sent or received.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Maintain System Performance and Reliability
&lt;/h3&gt;

&lt;p&gt;Large volumes of dead-letter messages can degrade the performance of your Service Bus namespace. They can slow down operations such as message retrieval and monitoring, leading to bottlenecks in your system.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Enable Effective Error Handling and Analysis
&lt;/h3&gt;

&lt;p&gt;By archiving dead-letter messages to Azure Blob Storage, you retain the ability to analyze and diagnose issues without impacting the performance of your messaging system. This allows your team to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Investigate Failures&lt;/strong&gt;: Understand why messages failed and identify patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reprocess Messages&lt;/strong&gt;: Correct issues and resend messages if necessary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improve Resilience&lt;/strong&gt;: Implement fixes to prevent similar failures in the future.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementing a Dead-Letter Cleanup Service with .NET
&lt;/h2&gt;

&lt;p&gt;To automate the cleanup process, we'll create a .NET background service that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Retrieves dead-letter messages&lt;/strong&gt; from all queues and topic subscriptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archives messages&lt;/strong&gt; to Azure Blob Storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deletes messages&lt;/strong&gt; from the DLQ after successful archiving.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure Service Bus Namespace&lt;/strong&gt;: With appropriate permissions (Manage rights) to access queues and topics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Storage Account&lt;/strong&gt;: For storing archived dead-letter messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET 6 SDK&lt;/strong&gt;: Installed on your development machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker&lt;/strong&gt;: (Optional) For containerized deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Getting the Source Code
&lt;/h3&gt;

&lt;p&gt;The source code for the cleanup tool is available on GitHub:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/baoduy/tool-serviceBus-deadLetters-cleanup" rel="noopener noreferrer"&gt;Azure Service Bus Dead-Letter Cleanup Tool&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Service Configuration
&lt;/h3&gt;

&lt;p&gt;The service uses an &lt;code&gt;appsettings.json&lt;/code&gt; file for configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ServiceBus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ConnectionString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_SERVICE_BUS_CONNECTION_STRING"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"StorageAccount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ConnectionString"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_STORAGE_ACCOUNT_CONNECTION_STRING"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ContainerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bus-dead-letters"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Logging"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LogLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Microsoft"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ConnectionString&lt;/code&gt;: Your Azure Service Bus connection string with Manage permissions.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;StorageAccount&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ConnectionString&lt;/code&gt;: Your Azure Storage Account connection string.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ContainerName&lt;/code&gt;: The name of the Blob Storage container where dead-letter messages will be stored.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security Note&lt;/strong&gt;: For production environments, consider using Azure Key Vault or environment variables to securely manage connection strings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How the Cleanup Service Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connect to Azure Service Bus&lt;/strong&gt;: The service connects to your Service Bus namespace using the provided connection string.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Discover Entities&lt;/strong&gt;: It retrieves all queues and topic subscriptions in the namespace.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Process Dead-Letter Messages&lt;/strong&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;For each entity, it checks the DLQ for messages.&lt;/li&gt;
&lt;li&gt;If messages are found, it reads them and saves each message as a JSON file in Azure Blob Storage.&lt;/li&gt;
&lt;li&gt;The messages are organized in folders by entity name and date, making them easy to locate.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Delete Processed Messages&lt;/strong&gt;: After successfully archiving, the messages are deleted from the DLQ.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Archiving Structure in Blob Storage
&lt;/h3&gt;

&lt;p&gt;The messages are stored in Azure Blob Storage with the following structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bus-dead-letters/
├── queues/
│   ├── queue1/
│   │   └── 2023-09-20/
│   │       ├── message1.json
│   │       └── message2.json
│   └── queue2/
│       └── 2023-09-20/
│           └── message1.json
└── topics/
    ├── topic1/
    │   ├── subscription1/
    │   │   └── 2023-09-20/
    │   │       └── message1.json
    │   └── subscription2/
    │       └── 2023-09-20/
    │           └── message1.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setting Up the Service
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Option 1: Running Locally
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clone the Repository&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git clone https://github.com/baoduy/tool-serviceBus-deadLetters-cleanup.git
   &lt;span class="nb"&gt;cd &lt;/span&gt;tool-serviceBus-deadLetters-cleanup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configure the Service&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Update &lt;code&gt;appsettings.json&lt;/code&gt; with your connection strings.&lt;/li&gt;
&lt;li&gt;Alternatively, set the environment variables &lt;code&gt;ServiceBus__ConnectionString&lt;/code&gt;, &lt;code&gt;StorageAccount__ConnectionString&lt;/code&gt;, and &lt;code&gt;StorageAccount__ContainerName&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build and Run the Service&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   dotnet build
   dotnet run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Option 2: Using Docker
&lt;/h4&gt;

&lt;p&gt;A Docker image is available for easy deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Image&lt;/strong&gt;: &lt;a href="https://hub.docker.com/r/baoduy2412/servicebus-cleanup" rel="noopener noreferrer"&gt;baoduy2412/servicebus-cleanup&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  Running with Docker Compose
&lt;/h5&gt;

&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;servicebus-cleanup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baoduy2412/servicebus-cleanup:latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ServiceBus__ConnectionString&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_SERVICE_BUS_CONNECTION_STRING&lt;/span&gt;
      &lt;span class="na"&gt;StorageAccount__ConnectionString&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_STORAGE_ACCOUNT_CONNECTION_STRING&lt;/span&gt;
      &lt;span class="na"&gt;StorageAccount__ContainerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bus-dead-letters&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Running with Docker Command Line
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ServiceBus__ConnectionString&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_SERVICE_BUS_CONNECTION_STRING &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;StorageAccount__ConnectionString&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_STORAGE_ACCOUNT_CONNECTION_STRING &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;StorageAccount__ContainerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bus-dead-letters &lt;span class="se"&gt;\&lt;/span&gt;
  baoduy2412/servicebus-cleanup:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Managing Storage Costs with Lifecycle Policies
&lt;/h3&gt;

&lt;p&gt;Over time, archived messages in Blob Storage can accumulate and consume significant storage space. To manage this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set Up Lifecycle Management&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;In the Azure Portal, navigate to your Storage Account.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Lifecycle management&lt;/strong&gt; under &lt;strong&gt;Blob service&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a Rule&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: e.g., &lt;code&gt;DeleteOldArchivedMessages&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope&lt;/strong&gt;: Apply to the container &lt;code&gt;bus-dead-letters&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt;: Optionally specify filters if needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action&lt;/strong&gt;: Delete blobs older than a specified number of days (e.g., 30 days).&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Save the Rule&lt;/strong&gt;: Azure will automatically delete archived messages older than the specified retention period.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Dead-letter queues are an integral part of Azure Service Bus, providing a mechanism to handle messages that cannot be processed. However, without regular maintenance, they can lead to storage overruns and impact system performance.&lt;/p&gt;

&lt;p&gt;By implementing the .NET background service described in this article, you can automate the cleanup of dead-letter queues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated Cleanup&lt;/strong&gt;: Keeps DLQs empty, preventing storage issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Message Archiving&lt;/strong&gt;: Stores messages for future analysis without impacting Service Bus performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: The tool operates at the namespace level, handling all queues and topics automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Management&lt;/strong&gt;: Utilizes storage lifecycle policies to control storage costs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach ensures your messaging system remains reliable and efficient while preserving valuable data for troubleshooting and improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/baoduy/tool-serviceBus-deadLetters-cleanup" rel="noopener noreferrer"&gt;Azure Service Bus Dead-Letter Cleanup Tool&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Image&lt;/strong&gt;: &lt;a href="https://hub.docker.com/r/baoduy2412/servicebus-cleanup" rel="noopener noreferrer"&gt;baoduy2412/servicebus-cleanup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Service Bus Documentation&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/azure/service-bus-messaging/service-bus-dead-letter-queues" rel="noopener noreferrer"&gt;Dead-letter Queues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/azure/service-bus-messaging/service-bus-quotas" rel="noopener noreferrer"&gt;Service Bus Quotas and Limits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Azure Blob Storage&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/azure/storage/blobs/lifecycle-management-overview" rel="noopener noreferrer"&gt;Lifecycle Management Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/azure/storage/blobs/storage-lifecycle-management-concepts" rel="noopener noreferrer"&gt;Optimize Costs by Automating Data Lifecycle Management&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;By automating dead-letter queue management, you enhance the stability and maintainability of your messaging infrastructure, ensuring it continues to meet the demands of your applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azureservicebuscleanup</category>
      <category>deadletterscleanup</category>
      <category>tools</category>
    </item>
    <item>
      <title>[AZ] How to Scan and Disable Inactive Accounts on Azure EntraID</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:28:13 +0000</pubDate>
      <link>https://forem.com/baoduy2412/az-how-to-scan-and-disable-inactive-accounts-on-azure-entraid-3chk</link>
      <guid>https://forem.com/baoduy2412/az-how-to-scan-and-disable-inactive-accounts-on-azure-entraid-3chk</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As organizations grow, so does the number of user accounts within their systems. Managing these accounts efficiently is crucial for maintaining security and compliance. In particular, inactive accounts in &lt;strong&gt;Azure Entra ID&lt;/strong&gt; (formerly known as Azure Active Directory) can pose significant security risks. These dormant accounts are potential entry points for malicious actors seeking unauthorized access to sensitive data.&lt;/p&gt;

&lt;p&gt;In this comprehensive guide, we'll walk through how to automate the management of inactive Azure Entra ID accounts using a TypeScript application. We'll cover everything from setting up an Azure Entra ID application, implementing the TypeScript program, to scheduling the script using Azure DevOps for regular execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Understanding the Risks of Inactive Accounts&lt;/li&gt;
&lt;li&gt;Creating an Azure Entra ID Application&lt;/li&gt;
&lt;li&gt;Implementing the TypeScript Program&lt;/li&gt;
&lt;li&gt;Scheduling the Script with Azure DevOps&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Understanding the Risks of Inactive Accounts
&lt;/h2&gt;

&lt;p&gt;Inactive accounts are user accounts that haven't been used for a significant period. They can accumulate due to employee turnover, role changes, or users simply forgetting about them. These accounts are risky because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security Vulnerabilities&lt;/strong&gt;: They might have weak or outdated passwords, making them easy targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unauthorized Access&lt;/strong&gt;: If compromised, they can provide unauthorized access to internal systems and data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance Issues&lt;/strong&gt;: Regulations often require the timely removal or deactivation of unused accounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regularly auditing and managing these accounts helps mitigate these risks and ensures compliance with security best practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Creating an Azure Entra ID Application
&lt;/h2&gt;

&lt;p&gt;To interact with Azure Entra ID programmatically, we'll create an &lt;strong&gt;App Registration&lt;/strong&gt;.&lt;br&gt;
This application will authenticate and manage user accounts via the Microsoft Graph API.&lt;/p&gt;
&lt;h3&gt;
  
  
  Steps to Create an App Registration
&lt;/h3&gt;
&lt;h4&gt;
  
  
  1. Navigate to Azure Entra ID
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Log in to the &lt;a href="https://portal.azure.com" rel="noopener noreferrer"&gt;Azure portal&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;In the left-hand navigation pane, select &lt;strong&gt;Azure Entra ID&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  2. Create a New App Registration
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Click on &lt;strong&gt;"App registrations"&lt;/strong&gt; in the sidebar.&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;"New registration"&lt;/strong&gt; at the top.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="/assets/tools-az-scan-and-disable-entra-accounts/app-registration.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/tools-az-scan-and-disable-entra-accounts/app-registration.png" alt="App Registration Navigation"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  3. Configure the App Registration
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: Enter a meaningful name, e.g., &lt;code&gt;Azure-EntraID-Management&lt;/code&gt; app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supported account types&lt;/strong&gt;: Choose &lt;strong&gt;"Accounts in this organizational directory only (Single tenant)"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect URI&lt;/strong&gt;: This is not required for our application, so you can leave it blank.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Register"&lt;/strong&gt; to create the app.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Adding API Permissions
&lt;/h3&gt;

&lt;p&gt;After creating the app registration, we need to grant it permissions to access the Microsoft Graph API.&lt;/p&gt;
&lt;h4&gt;
  
  
  1. Navigate to API Permissions
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;In the app registration's overview page, click on &lt;strong&gt;"API permissions"&lt;/strong&gt; in the sidebar.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  2. Add Permissions
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Click on &lt;strong&gt;"Add a permission"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;"Microsoft Graph"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;"Application permissions"&lt;/strong&gt; since this app will run as a background service or daemon.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  3. Select Required Permissions
&lt;/h4&gt;

&lt;p&gt;Search for and select the following permissions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AuditLog.Read.All&lt;/code&gt;&lt;/strong&gt;: Allows the app to read all audit log data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;User.Read.All&lt;/code&gt;&lt;/strong&gt;: Allows the app to read user profiles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;User.ReadWrite.All&lt;/code&gt;&lt;/strong&gt;: Allows the app to read and write user profiles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="/assets/tools-az-scan-and-disable-entra-accounts/app-api-permission.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/tools-az-scan-and-disable-entra-accounts/app-api-permission.png" alt="API Permissions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;strong&gt;&lt;code&gt;User.ReadWrite.All&lt;/code&gt;&lt;/strong&gt; permission is required to enable or disable user accounts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  4. Grant Admin Consent
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;After adding the permissions, click on &lt;strong&gt;"Grant admin consent for [Your Tenant Name]"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Confirm by clicking &lt;strong&gt;"Yes"&lt;/strong&gt; in the prompt.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  5. Create a Client Secret
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to &lt;strong&gt;"Certificates &amp;amp; secrets"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;"New client secret"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Provide a description and set an expiration period.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Add"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;Value&lt;/strong&gt; of the client secret and store it securely. You won't be able to view it again.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Collect Necessary Information
&lt;/h3&gt;

&lt;p&gt;You'll need the following information for your application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tenant ID&lt;/strong&gt;: Found in &lt;strong&gt;Azure Entra ID&lt;/strong&gt; &amp;gt; &lt;strong&gt;Properties&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client ID&lt;/strong&gt;: Found in your app registration's &lt;strong&gt;Overview&lt;/strong&gt; page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Secret&lt;/strong&gt;: The value you just created.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  3. Implementing the TypeScript Program
&lt;/h2&gt;

&lt;p&gt;We'll create a TypeScript program that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authenticates with Azure Entra ID using the app registration.&lt;/li&gt;
&lt;li&gt;Retrieves inactive user accounts based on their last sign-in date.&lt;/li&gt;
&lt;li&gt;Disables these inactive accounts.&lt;/li&gt;
&lt;li&gt;Generates a report of all disabled accounts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Setting Up the Project
&lt;/h3&gt;
&lt;h4&gt;
  
  
  1. Initialize the Project
&lt;/h4&gt;

&lt;p&gt;Create a new directory for your project and initialize npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;azure-entra-id-management
&lt;span class="nb"&gt;cd &lt;/span&gt;azure-entra-id-management
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Install Dependencies
&lt;/h4&gt;

&lt;p&gt;Install the required packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @azure/identity @microsoft/microsoft-graph-client dayjs
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; typescript ts-node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@azure/identity&lt;/code&gt;&lt;/strong&gt;: Provides Azure authentication methods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@microsoft/microsoft-graph-client&lt;/code&gt;&lt;/strong&gt;: Allows interaction with Microsoft Graph API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dayjs&lt;/code&gt;&lt;/strong&gt;: A lightweight library for date manipulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;typescript&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;ts-node&lt;/code&gt;&lt;/strong&gt;: Required for compiling and running TypeScript code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. Configure TypeScript
&lt;/h4&gt;

&lt;p&gt;Create a &lt;code&gt;tsconfig.json&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"commonjs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"esModuleInterop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"es6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moduleResolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sourceMap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"es2015"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"index.ts"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Writing Code
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Create the Source File and Import Required Modules
&lt;/h4&gt;

&lt;p&gt;At the top of &lt;code&gt;index.ts&lt;/code&gt;, import the necessary modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DefaultAzureCredential&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@azure/identity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@microsoft/microsoft-graph-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TokenCredentialAuthenticationProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dayjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The &lt;code&gt;isomorphic-fetch&lt;/code&gt; import is necessary for environments where &lt;code&gt;fetch&lt;/code&gt; is not available globally.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  2. Set Configuration Variables
&lt;/h4&gt;

&lt;p&gt;Define the configuration variables and excluded accounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Configuration&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;INACTIVITY_THRESHOLD_MONTHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Adjust as needed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EXCLUDED_ACCOUNTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceaccount@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Azure AD App Credentials - Replace with your actual credentials or use environment variables&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TENANT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AZURE_TENANT_ID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AZURE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CLIENT_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AZURE_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security Tip&lt;/strong&gt;: Never hard-code credentials. Use environment variables or secure credential management.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  3. Set Up Authentication and Client
&lt;/h4&gt;

&lt;p&gt;Create a &lt;code&gt;ClientSecretCredential&lt;/code&gt; and initialize the Microsoft Graph client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;TENANT_ID&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;CLIENT_ID&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please ensure AZURE_TENANT_ID, AZURE_CLIENT_ID, and AZURE_CLIENT_SECRET are set.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** 1. Setup Credentials and Microsoft Graph client */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initWithMiddleware&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;debugLogging&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;authProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TokenCredentialAuthenticationProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;// This DefaultAzureCredential will detect the credentials from environment variables above automatically.&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DefaultAzureCredential&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://graph.microsoft.com/.default&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  4. Define Types
&lt;/h4&gt;

&lt;p&gt;Define types for the Azure AD user and result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AzResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AdUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;accountEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  5. Implement Functions
&lt;/h4&gt;

&lt;h5&gt;
  
  
  a. Retrieve Inactive Accounts
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getInactiveAccounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cutoffDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Dayjs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AdUser&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/** Query all the accounts that has signInActivity date before the expected parameter date */&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`signInActivity/lastSignInDateTime lt &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cutoffDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id,userPrincipalName,accountEnabled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AzResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AdUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="cm"&gt;/** Filter the enabled account only here
   * as the API filter has limitation that not allows to query based on both signInActivity and accountEnabled */&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountEnabled&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The Microsoft Graph API may paginate results. The loop ensures all pages are retrieved.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h5&gt;
  
  
  b. Disable Accounts
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;disableAccounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdUser&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="cm"&gt;/* Check and keep the account if found in the excludedAccounts */&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;EXCLUDED_ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`User account &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has been excluded from disabling.`&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Perform the account disabling&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;accountEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`User account with ID &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has been disabled.`&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  c. Retrieve Disabled Accounts
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDisabledAccounts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AdUser&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/** Query all the accounts that has accountEnabled is false */&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`accountEnabled eq false`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id,userPrincipalName,accountEnabled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AzResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AdUser&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h5&gt;
  
  
  d. Print Accounts
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;printAccounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AdUser&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;accounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPrincipalName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (Enabled: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountEnabled&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  6. Main Execution Function
&lt;/h4&gt;

&lt;p&gt;Finally, create the main function to orchestrate the process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//1. Scanning account that inactive for more than 2 months&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cutoffDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;INACTIVITY_THRESHOLD_MONTHS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;month&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`Scanning for accounts inactive since before &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cutoffDate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YYYY-MM-DD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;...\n`&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Retrieve inactive accounts&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inactiveAccounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getInactiveAccounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cutoffDate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;printAccounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`Found &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;inactiveAccounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; inactive account(s):`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;inactiveAccounts&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Disable inactive accounts&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inactiveAccounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;disableAccounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inactiveAccounts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No inactive accounts to disable.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Retrieve and print disabled accounts&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;disabledAccounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getDisabledAccounts&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;printAccounts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\nCurrently disabled accounts:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disabledAccounts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;An error occurred:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  7. Running the Program
&lt;/h4&gt;

&lt;p&gt;Ensure the environment variables are set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AZURE_TENANT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_tenant_id
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AZURE_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_client_id
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AZURE_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_client_secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compile and run the program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx ts-node src/index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Scheduling the Script with Azure DevOps
&lt;/h2&gt;

&lt;p&gt;To automate the execution of the script, we'll use Azure DevOps to schedule it as part of a pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Steps to Schedule the Script
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Commit the Code to a Repository
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Create a Git repository in Azure DevOps.&lt;/li&gt;
&lt;li&gt;Commit all your code, including the &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;tsconfig.json&lt;/code&gt;, and &lt;code&gt;src&lt;/code&gt; directory.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Create a Variable Group
&lt;/h4&gt;

&lt;p&gt;Navigate to &lt;strong&gt;Pipelines&lt;/strong&gt; &amp;gt; &lt;strong&gt;Library&lt;/strong&gt; in Azure DevOps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;"Variable groups"&lt;/strong&gt; and then &lt;strong&gt;"Add variable group"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name the variable group, e.g., &lt;code&gt;AzureEntraIDCredentials&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add the following variables:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AZURE_TENANT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AZURE_CLIENT_SECRET&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;For each variable, enter the corresponding value and mark it as &lt;strong&gt;secret&lt;/strong&gt;.&lt;/li&gt;

&lt;li&gt;Save the variable group.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;a href="/assets/tools-az-scan-and-disable-entra-accounts/variable-group.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/tools-az-scan-and-disable-entra-accounts/variable-group.png" alt="Variable Group"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Create the Pipeline
&lt;/h4&gt;

&lt;p&gt;In &lt;strong&gt;Pipelines&lt;/strong&gt;, click &lt;strong&gt;"Create Pipeline"&lt;/strong&gt; and follow the prompts to set up a YAML pipeline.&lt;/p&gt;

&lt;h5&gt;
  
  
  a. Define the YAML Pipeline
&lt;/h5&gt;

&lt;p&gt;Create a &lt;code&gt;azure-pipelines.yml&lt;/code&gt; file in your repository with the following content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;

&lt;span class="na"&gt;schedules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt; &lt;span class="c1"&gt;# Runs at midnight every Sunday&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Weekly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Sunday&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Run"&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;always&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vmImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ubuntu-latest"&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AzureEntraIDCredentials&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodeTool@0&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;versionSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;14.x"&lt;/span&gt; &lt;span class="c1"&gt;# Adjust Node.js version as needed&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Install&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Node.js"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="s"&gt;npx ts-node src/index.ts&lt;/span&gt;
    &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Install&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dependencies&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;script"&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AZURE_TENANT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(AZURE_TENANT_ID)&lt;/span&gt;
      &lt;span class="na"&gt;AZURE_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(AZURE_CLIENT_ID)&lt;/span&gt;
      &lt;span class="na"&gt;AZURE_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$(AZURE_CLIENT_SECRET)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  b. Pipeline Explanation
&lt;/h5&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger&lt;/strong&gt;: Set to &lt;code&gt;none&lt;/code&gt; to prevent automatic builds on code changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedules&lt;/strong&gt;: Configured to run every Sunday at midnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pool&lt;/strong&gt;: Uses the latest Ubuntu VM image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variables&lt;/strong&gt;: Includes the variable group with your credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NodeTool&lt;/strong&gt;: Ensures Node.js is available on the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Script&lt;/strong&gt;: Installs dependencies and runs the script.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  4. Run and Monitor the Pipeline
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Save and run the pipeline manually to test it.&lt;/li&gt;
&lt;li&gt;Monitor the pipeline's execution in the &lt;strong&gt;Pipelines&lt;/strong&gt; section.&lt;/li&gt;
&lt;li&gt;Ensure that the script runs successfully and performs the expected actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  5. Secure the Pipeline
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permissions&lt;/strong&gt;: Ensure only authorized personnel can modify the pipeline and variable group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret Variables&lt;/strong&gt;: Keep your credentials secure by marking them as secrets and avoiding logging sensitive information.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Conclusion
&lt;/h2&gt;

&lt;p&gt;Automating the management of inactive Azure Entra ID accounts enhances your organization's security posture by reducing potential attack surfaces. By leveraging TypeScript and Azure DevOps, you can create a scalable and maintainable solution that integrates seamlessly with your existing workflows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can refer the &lt;strong&gt;Full Working Source Code here&lt;/strong&gt;: &lt;a href="https://dev.azure.com/drunk24/drunkcoding-public/_git/az.tools?path=/az-entraID-scan&amp;amp;version=GBmain" rel="noopener noreferrer"&gt;drunkcoding public code&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security First&lt;/strong&gt;: Regularly auditing and managing inactive accounts is critical for security and compliance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation&lt;/strong&gt;: Automating tasks reduces manual effort and the likelihood of human error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Using TypeScript and Azure DevOps allows for easy updates and scalability as your organization grows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Next Topic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enhancements&lt;/strong&gt;: Extend the script to send email notifications before disabling accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging&lt;/strong&gt;: Integrate logging mechanisms for better auditing and monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy Compliance&lt;/strong&gt;: Ensure the solution complies with your organization's policies and any applicable regulations.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: Accessing certain Microsoft Graph API endpoints requires appropriate licensing. Ensure you have the necessary Microsoft Entra ID P2 or equivalent licenses to use the AuditLog API and other premium features.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>entraid</category>
      <category>disableaccount</category>
    </item>
    <item>
      <title>[Tools] Automating Let's Encrypt Certificate Management with Azure Key Vault and Cloudflare</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:27:55 +0000</pubDate>
      <link>https://forem.com/baoduy2412/tools-automating-lets-encrypt-certificate-management-with-azure-key-vault-and-cloudflare-4cnb</link>
      <guid>https://forem.com/baoduy2412/tools-automating-lets-encrypt-certificate-management-with-azure-key-vault-and-cloudflare-4cnb</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Custom domain names enhance the professionalism and credibility of applications hosted on Azure services.&lt;br&gt;
However, associating a custom domain requires a trusted SSL/TLS certificate. For &lt;strong&gt;development and sandbox environments&lt;/strong&gt;&lt;br&gt;
used internally by development teams, leveraging &lt;strong&gt;Let's Encrypt&lt;/strong&gt; certificates offers a convenient and automated solution.&lt;br&gt;
Let's Encrypt provides free SSL certificates, but they have a short lifespan of only 90 days, necessitating frequent renewals.&lt;/p&gt;

&lt;p&gt;To streamline this process, I've developed a tool that automates the generation and renewal of Let's Encrypt certificates specifically for development&lt;br&gt;
and sandbox environments. The tool detects expiring certificates and renews only those that are nearing expiration, ensuring efficient management.&lt;br&gt;
The new certificates are securely imported into &lt;strong&gt;Azure Key Vault&lt;/strong&gt;, allowing seamless integration with Azure resources such as &lt;strong&gt;Azure API Management&lt;/strong&gt;&lt;br&gt;
and &lt;strong&gt;Azure Front Door&lt;/strong&gt;. To eliminate manual intervention entirely, the tool runs as a monthly cron job on &lt;strong&gt;Azure Kubernetes Service (AKS)&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Automate Certificate Management?
&lt;/h2&gt;

&lt;p&gt;Manually managing short-lived Let's Encrypt SSL certificates can be time-consuming and error-prone, especially when dealing with multiple domains&lt;br&gt;
and environments. Automating the certificate management process offers several significant advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost Savings for Development and Sandbox Environments&lt;/strong&gt;: Let's Encrypt provides a free alternative to paid certificates,&lt;br&gt;
making it ideal for non-production environments where cost optimization is important.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Elimination of Certificate Expiration Concerns&lt;/strong&gt;: The tool proactively identifies and renews certificates nearing expiration,&lt;br&gt;
ensuring your services remain secure without requiring manual intervention.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simplified Management of Multiple Domains&lt;/strong&gt;: With built-in support for handling multiple domains via &lt;strong&gt;Cloudflare&lt;/strong&gt;,&lt;br&gt;
the tool streamlines the process of managing DNS challenges required for certificate validation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Secure Integration with Azure Key Vault&lt;/strong&gt;: Automatically importing generated certificates into Azure Key Vault provides a secure&lt;br&gt;
and centralized storage solution, enhancing overall security and simplifying certificate management across your Azure resources.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The tool automates SSL certificate management by running as a &lt;strong&gt;monthly cron job&lt;/strong&gt; on AKS. It handles the entire lifecycle of SSL certificates, from detection of impending expiration to deployment of new certificates. The workflow is as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check Certificate Expiration&lt;/strong&gt;: The tool scans all certificates stored in Azure Key Vault to determine their expiration dates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generate New Certificates&lt;/strong&gt;: For certificates nearing expiration, the tool requests new SSL certificates from &lt;strong&gt;Let's Encrypt&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DNS Challenge via Cloudflare&lt;/strong&gt;: The tool integrates with &lt;strong&gt;Cloudflare&lt;/strong&gt; to perform DNS challenges required by Let's Encrypt to validate domain ownership.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Import Certificates to Azure Key Vault&lt;/strong&gt;: The newly obtained certificates are securely imported into &lt;strong&gt;Azure Key Vault&lt;/strong&gt;, replacing the old certificates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated Monthly Execution&lt;/strong&gt;: The tool is scheduled to run monthly on AKS, ensuring that certificates are kept up-to-date with minimal manual effort.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Setting Up Cloudflare DNS API Token
&lt;/h2&gt;

&lt;p&gt;To enable the tool to perform DNS challenges for domain validation, you need to create a Cloudflare API token with permissions to manage DNS records.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create an API Token&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Log in to your Cloudflare account and navigate to your profile.&lt;/li&gt;
&lt;li&gt;Go to the &lt;strong&gt;API Tokens&lt;/strong&gt; section or directly via &lt;a href="https://dash.cloudflare.com/profile/api-tokens" rel="noopener noreferrer"&gt;this link&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;"Create Token"&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configure Token Permissions&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permissions&lt;/strong&gt;: Grant &lt;strong&gt;Zone&lt;/strong&gt; &amp;gt; &lt;strong&gt;DNS&lt;/strong&gt; &amp;gt; &lt;strong&gt;Edit&lt;/strong&gt; permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zone Resources&lt;/strong&gt;: Select &lt;strong&gt;Specific Zone&lt;/strong&gt; and choose the domain(s) you want to manage.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client IP Address Filtering (Optional but Recommended)&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;For enhanced security, specify the AKS cluster's public IP address under &lt;strong&gt;"Client IP Address Filtering"&lt;/strong&gt; in the token settings.&lt;/li&gt;
&lt;li&gt;This restricts API token usage to requests originating from your AKS cluster, preventing unauthorized access.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Save the Token&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Generate the token and copy it. You'll need it for the tool's configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="/assets/tools-aks-cert-manager-with-private-aks/cf-dns-token.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/tools-aks-cert-manager-with-private-aks/cf-dns-token.png" alt="Cloudflare API Token Creation"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;The tool is configured using environment variables or a JSON configuration file. Here's an example &lt;code&gt;appsettings.json&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CertManager"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ProductionEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CfEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-cloudflare-email@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CfToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_CLOUDFLARE_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ZoneId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_CLOUDFLARE_ZONE_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"LetsEncryptEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-email@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Domains"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"api.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.example.com"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CertInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"CountryName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"State"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Singapore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Locality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Singapore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Organization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourOrganization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OrganizationUnit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourUnit"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"KeyVaultUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-keyvault-name.vault.azure.net/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"KeyVaultUID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OPTIONAL_USER_ASSIGNED_IDENTITY_CLIENT_ID"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuration Parameters Explained&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ProductionEnabled&lt;/strong&gt;: Set to &lt;code&gt;true&lt;/code&gt; to use Let's Encrypt production environment. Set to &lt;code&gt;false&lt;/code&gt; for testing purposes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CfEmail&lt;/strong&gt;: Your Cloudflare account email address.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CfToken&lt;/strong&gt;: The Cloudflare API token created earlier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ZoneId&lt;/strong&gt;: The ID of your Cloudflare DNS zone. You can find this in your Cloudflare dashboard under the domain's &lt;strong&gt;Overview&lt;/strong&gt; section.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LetsEncryptEmail&lt;/strong&gt;: An email address for Let's Encrypt notifications.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Domains&lt;/strong&gt;: An array of domains and subdomains for which you want to generate certificates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CertInfo&lt;/strong&gt;: Certificate subject information.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KeyVaultUrl&lt;/strong&gt;: The URL of your Azure Key Vault where certificates will be stored.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;KeyVaultUID&lt;/strong&gt;: The Client ID of the User Assigned Managed Identity (UAMI) used by your AKS cluster (optional if using the default identity).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deploying to AKS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An AKS cluster where the tool will run.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The AKS cluster's agent pool has a User Assigned Managed Identity (UAMI) for Azure resource authentication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Access to the Azure Key Vault where certificates will be stored.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Granting Key Vault Access to AKS UAMI
&lt;/h3&gt;

&lt;p&gt;Before deploying the tool, you need to grant your AKS cluster's UAMI the necessary permissions to access Azure Key Vault:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify the AKS Agent Pool UAMI&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;In the Azure portal, navigate to your AKS cluster.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Settings&lt;/strong&gt;, select &lt;strong&gt;Identity&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Note the &lt;strong&gt;Client ID&lt;/strong&gt; of the &lt;strong&gt;User Assigned&lt;/strong&gt; identity associated with your node pools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="/assets/tools-automate-letsencrypt-certification-with-azure-keyvault/aks-uaid.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/tools-automate-letsencrypt-certification-with-azure-keyvault/aks-uaid.png" alt="AKS User Assigned Managed Identity"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grant Key Vault Permissions&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Navigate to your Azure Key Vault.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Access control (IAM)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;"Add role assignment"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Role&lt;/strong&gt; dropdown, select &lt;strong&gt;"Key Vault Certificates Officer"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Next&lt;/strong&gt; and select the AKS UAMI as the &lt;strong&gt;Member&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Review and assign the role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By granting the &lt;strong&gt;Key Vault Certificates Officer&lt;/strong&gt; role to your AKS UAMI, you allow the tool running on AKS to manage certificates within the Key Vault.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying the Tool Using Helm
&lt;/h3&gt;

&lt;p&gt;Assuming you are using Helm for deployment, you can update your Helm chart values file with the necessary configurations.&lt;/p&gt;

&lt;p&gt;Here's an example &lt;code&gt;values.yaml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cert-renewal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baoduy2412/keyvault-letsencrypt:latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__ProductionEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__CfEmail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-cloudflare-email@example.com"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__CfToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_CLOUDFLARE_API_TOKEN"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__ZoneId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_CLOUDFLARE_ZONE_ID"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__LetsEncryptEmail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-email@example.com"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__Domains__0&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api.example.com"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__Domains__1&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.example.com"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__CertInfo__CountryName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SG"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__CertInfo__State&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Singapore"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__CertInfo__Locality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Singapore"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__CertInfo__Organization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YourOrganization"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__CertInfo__OrganizationUnit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YourUnit"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__KeyVaultUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://your-keyvault-name.vault.azure.net/"&lt;/span&gt;
      &lt;span class="na"&gt;CertManager__KeyVaultUID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPTIONAL_USER_ASSIGNED_IDENTITY_CLIENT_ID"&lt;/span&gt;
    &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt; &lt;span class="c1"&gt;# Runs on the 1st of every month at midnight&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Image&lt;/strong&gt;: Ensure you're using the correct Docker image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Environment Variables&lt;/strong&gt;: Update all placeholders with your actual configuration values.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Schedule&lt;/strong&gt;: The cron expression &lt;code&gt;"0 0 1 * *"&lt;/code&gt; schedules the job to run at midnight on the first day of every month.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Deploying the Helm Chart
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Update Helm Repositories&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   helm repo update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deploy or Upgrade the Chart&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; cert-renewal ./path-to-your-chart &lt;span class="nt"&gt;-f&lt;/span&gt; values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;./path-to-your-chart&lt;/code&gt; with the path to your Helm chart.&lt;/p&gt;

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

&lt;p&gt;Managing SSL certificates for Azure resources with custom domains can be challenging due to the frequent renewal requirements of Let's Encrypt certificates. This tool automates the entire process of certificate generation, validation, and deployment, significantly simplifying SSL certificate management for development and sandbox environments.&lt;/p&gt;

&lt;p&gt;By running as a monthly cron job on AKS, it ensures that your certificates are always up-to-date without manual intervention. The integration with Azure Key Vault enhances security by providing centralized and secure storage of your certificates, which can be accessed by other Azure services as needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leveraging Infrastructure as Code for Deployment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the certificates are stored in Azure Key Vault, you can further automate the deployment process by using infrastructure as code (IaC) tools like Pulumi or Terraform.&lt;br&gt;
These tools can retrieve the certificates from Key Vault and deploy them to your Azure resources automatically.&lt;br&gt;
By incorporating this into your IaC pipelines, you ensure that any updates to the certificates are seamlessly propagated to services like Azure API Management, Azure Front Door, or Azure Application Gateway, maintaining consistent and secure configurations across your infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Benefits&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated Renewal&lt;/strong&gt;: Eliminates the manual effort required to renew Let's Encrypt certificates every 90 days.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost Efficiency&lt;/strong&gt;: Uses free Let's Encrypt certificates, reducing costs for non-production environments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: Easily manages multiple domains and environments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;: Securely stores certificates in Azure Key Vault and restricts Cloudflare API access to your AKS cluster.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give it a try and simplify your SSL certificate management process!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/baoduy/az-keyvault-letsencrypt" rel="noopener noreferrer"&gt;az-keyvault-letsencrypt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Image&lt;/strong&gt;: &lt;a href="https://hub.docker.com/r/baoduy2412/keyvault-letsencrypt" rel="noopener noreferrer"&gt;baoduy2412/keyvault-letsencrypt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azurekeyvault</category>
      <category>letsencrypt</category>
      <category>cloudflare</category>
      <category>tools</category>
    </item>
    <item>
      <title>[AKS] Implementing Cert Manager with Private Azure Kubernetes Service (AKS).</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:27:22 +0000</pubDate>
      <link>https://forem.com/baoduy2412/aks-implementing-cert-manager-with-private-azure-kubernetes-service-aks-124d</link>
      <guid>https://forem.com/baoduy2412/aks-implementing-cert-manager-with-private-azure-kubernetes-service-aks-124d</guid>
      <description>&lt;p&gt;In a previous &lt;a href="https://drunkcoding.net/posts/ks-03-install-cert-manager-free-ssl-kubernetes-cluster/" rel="noopener noreferrer"&gt;post&lt;/a&gt;, It detailed how to set up Cert Manager on a Raspberry Pi K3s Cluster. That was a great starting point, and in this article, I decided to explore a more complex scenario by deploying Cert Manager within a private Kubernetes cluster on Azure. I’m excited to share the insights and techniques I discovered along the way, hoping they can make your journey a bit smoother."&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Azure Architecture Overview&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The following diagram illustrates the architecture we'll be working with:&lt;br&gt;
&lt;a href="/assets/aks-cert-manager-with-private-aks/private-AKS-with-cert-manager.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/aks-cert-manager-with-private-aks/private-AKS-with-cert-manager.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The setup involves two Virtual Networks (VNETs) that are peered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CloudPC VNET:&lt;/strong&gt; This is where we host all our Windows 365 Enterprise environments, allowing remote users to securely access the company’s resources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AKS VNET:&lt;/strong&gt; This VNET hosts our private AKS cluster. To keep things secure, I’ve set up a firewall that controls all outbound traffic, with no direct inbound access from the internet. The public IP is purely for outbound traffic—no inbound ports are left open.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;NGINX Ingress:&lt;/strong&gt; This piece allows CloudPC to access applications running on AKS through VNET peering, making sure everything stays within a private network.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;The Challenge I Encountered: Securing Internal Traffic&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;One of the challenges I came across was figuring out how to secure the communication between the applications hosted on AKS and the CloudPC environment, even though it’s all internal within the virtual networks. I wanted to make sure that all data in transit was encrypted using SSL certificates.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;How I Solved It: Leveraging Cloudflare DNS and Cert Manager&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To address the challenge, I implemented the following approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Domain Setup with Cloudflare:&lt;/strong&gt; Let’s assume I have a domain, &lt;code&gt;drunk.dev&lt;/code&gt;, registered with Cloudflare. While this domain isn’t directly used for external-facing services. But, I utilize this domain with Let’s Encrypt to verify and issue SSL certificates for the ingress controllers of my applications within the AKS cluster.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Internal DNS Configuration on Azure:&lt;/strong&gt; Internally, I created a private DNS Zone in Azure with the same name (&lt;code&gt;drunk.dev&lt;/code&gt;) and linked this zone to both the CloudPC and AKS VNETs. This setup is critical as it ensures that internal DNS queries for the &lt;code&gt;drunk.dev&lt;/code&gt; domain are resolved correctly within the private network, facilitating secure communication between services.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Installation&lt;/strong&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a Cloudflare DNS API Token:&lt;/strong&gt;
First, navigate to the Cloudflare profile and create an API token by following &lt;a href="https://dash.cloudflare.com/profile/api-tokens" rel="noopener noreferrer"&gt;this link&lt;/a&gt;. The API token should have permissions to manage DNS records for the domains.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Additionally, for enhanced security, specify the AKS public IP address under &lt;code&gt;Client IP Address Filtering&lt;/code&gt; in Cloudflare. This ensures that the API token is only accessible from the AKS platform, preventing unauthorized access from other locations.&lt;br&gt;
   &lt;a href="/assets/aks-cert-manager-with-private-aks/cf-dns-token.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/aks-cert-manager-with-private-aks/cf-dns-token.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create a Kubernetes Secret for the Cloudflare API Token:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next, create a Kubernetes secret to securely store the Cloudflare API token within the AKS cluster. This secret will be referenced by Cert Manager during DNS validation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1"&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cf-dns-secret&lt;/span&gt;
&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR-CF-DNS-TOKEN"&lt;/span&gt;
&lt;span class="c1"&gt;# Replace 'YOUR-CF-DNS-TOKEN' with the actual API token generated in the previous step.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cert Manager Installation:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create a values.yaml file for Helm Installation:&lt;/strong&gt;
Before installing Cert Manager, create a values.yaml file with the following content. The extraArgs section is important as it directs Cert Manager to use Cloudflare’s DNS resolver for DNS-01 challenge validation.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# auto create CRD resources&lt;/span&gt;
&lt;span class="na"&gt;installCRDs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# Default ingress value&lt;/span&gt;
&lt;span class="na"&gt;ingressShim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;defaultIssuerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;letsencrypt-prod"&lt;/span&gt;
  &lt;span class="na"&gt;defaultIssuerKind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ClusterIssuer"&lt;/span&gt;
  &lt;span class="na"&gt;defaultIssuerGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cert-manager.io"&lt;/span&gt;

&lt;span class="c1"&gt;#extra args is important for Cloudflare DNS validation&lt;/span&gt;
&lt;span class="na"&gt;extraArgs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--dns01-recursive-nameservers-only&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--dns01-recursive-nameservers=1.1.1.1:53&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration ensures that Cert Manager will only use Cloudflare’s DNS servers (specifically 1.1.1.1:53) to perform DNS-01 challenge validation, which is necessary for issuing SSL certificates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Install Cert Manager with Helm:&lt;/strong&gt;
Now, proceed with installing Cert Manager using Helm. The following commands will add the Jetstack Helm repository, update it, and then install Cert Manager with the custom values.yaml configuration file above.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add jetstack https://charts.jetstack.io
helm repo update

helm &lt;span class="nb"&gt;install &lt;/span&gt;cert-manager jetstack/cert-manager &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--values&lt;/span&gt; values.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; cert-manager &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-namespace&lt;/span&gt; &lt;span class="nt"&gt;--cleanup-on-fail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set Up a ClusterIssuer for Cert Manager:&lt;/strong&gt;
The next step is create a ClusterIssuer resource to define how Cert Manager should obtain SSL certificates. The following template uses the Cloudflare DNS API token stored in the Kubernetes secret created earlier.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cert-manager.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIssuer&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;acme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://acme-v02.api.letsencrypt.org/directory&lt;/span&gt;
    &lt;span class="c1"&gt;# Replace with the administrator email associated with your domain.&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin@drunk.dev"&lt;/span&gt;
    &lt;span class="na"&gt;privateKeySecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;
    &lt;span class="na"&gt;solvers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;dns01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cloudflare&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;#Update this accoring to your domain&lt;/span&gt;
            &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin@drunk.dev"&lt;/span&gt;
            &lt;span class="c1"&gt;# Ensure that the name matches the secret you created (cf-dns-secret),&lt;/span&gt;
            &lt;span class="c1"&gt;# and the key references the correct data key within the secret (token).&lt;/span&gt;
            &lt;span class="na"&gt;apiTokenSecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cf-dns-secret&lt;/span&gt;
              &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Firewall Whitelisting&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the AKS cluster’s outbound traffic is managed by a firewall, it’s neededs to whitelist specific external services to ensure that Cert Manager can successfully issue certificates. Without these exceptions, the certificate issuance process will fail.&lt;/p&gt;

&lt;p&gt;Here’s what needs to allow in the Firewall rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allow outbound access to &lt;code&gt;api.cloudflare.com&lt;/code&gt; on port &lt;code&gt;443&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Allow outbound access to &lt;code&gt;*.api.letsencrypt.org&lt;/code&gt; on port &lt;code&gt;443&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Nginx Ingress Installation
&lt;/h2&gt;

&lt;p&gt;Setting up NGINX Ingress in a private AKS environment involves configuring it to use a private IP address and an internal ingress class.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install NGINX Ingress Controller&lt;/strong&gt;: To deploy NGINX as an internal Ingress controller with a private IP address, create a values.yaml file with the following configuration.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;controller&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hostNetwork&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;
  &lt;span class="na"&gt;useIngressClassOnly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
  &lt;span class="na"&gt;watchIngressWithoutClass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
  &lt;span class="c1"&gt;# the ingress class name is internal&lt;/span&gt;
  &lt;span class="na"&gt;ingressClass&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;internal"&lt;/span&gt;
  &lt;span class="c1"&gt;# The custom ingress class&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassResource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;internal"&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;controllerValue&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;k8s.io/ingress-nginx&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# This annotation to tell Azure to create an internal load balancer.&lt;/span&gt;
      &lt;span class="na"&gt;service.beta.kubernetes.io/azure-load-balancer-internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
    &lt;span class="na"&gt;externalTrafficPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Local"&lt;/span&gt;
    &lt;span class="c1"&gt;# update this private IP address accroding to your address spaces.&lt;/span&gt;
    &lt;span class="na"&gt;loadBalancerIP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.250.250"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explanation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hostNetwork&lt;/code&gt;: false ensures that the NGINX pods do not bind directly to the node’s network interfaces, maintaining isolation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useIngressClassOnly&lt;/code&gt;: Ensures that only Ingress resources with the specified ingressClass will be processed by this controller.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ingressClass&lt;/code&gt;: Named internal to distinguish it from other Ingress classes, ensuring it’s used specifically for internal traffic.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service.annotations&lt;/code&gt;: The key annotation service.beta.kubernetes.io/azure-load-balancer-internal: 'true' tells Azure to create an internal load balancer instead of a public one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loadBalancerIP&lt;/code&gt;: Assigns a static private IP address (192.168.250.250) to the NGINX load balancer, ensuring it’s accessible only within the internal network.
Once NGINX is deployed with this configuration, it will only be accessible from within the internal network via the private IP address &lt;code&gt;192.168.250.250&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Install Nginx with Helm&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm nginx ingress-nginx/ingress-nginx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--values&lt;/span&gt; values.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt;  nginx-ingress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-namespace&lt;/span&gt; &lt;span class="nt"&gt;--cleanup-on-fail&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Configure DNS for Internal Access&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After deploying the NGINX Ingress controller, the next step is to ensure that internal DNS queries resolve to the NGINX private IP. To do this, add an A record in the Azure private DNS zone that created earlier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DNS Record&lt;/strong&gt;: Add an A record pointing to 192.168.250.250 for the internal domain. This ensures that any internal traffic destined for blogs.drunk.dev is routed to the NGINX Ingress controller.
&lt;img src="/assets/aks-cert-manager-with-private-aks/az-private-dns.png"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create a Secure Ingress with Dynamic TLS Certificate Generation&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, it’s time to create a secure Ingress resource that uses dynamic TLS certificate generation.&lt;/p&gt;

&lt;p&gt;Here’s an example configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drunk-blog-apps&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drunk-apps&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kubernetes.io/tls-acme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
    &lt;span class="na"&gt;nginx.ingress.kubernetes.io/backend-protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTP&lt;/span&gt;
    &lt;span class="na"&gt;ingress.kubernetes.io/force-ssl-redirect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# the name of ingress class percificly here.&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal&lt;/span&gt;
  &lt;span class="c1"&gt;# The tls config&lt;/span&gt;
  &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;blogs.drunk.dev&lt;/span&gt;
      &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tls-blogs-lets&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# the host config&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blogs.drunk.dev&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
            &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
            &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blog-apps&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ingressClassName&lt;/code&gt;: Specifies that this Ingress resource should be handled by the internal Ingress class, ensuring it’s routed through the private NGINX controller.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tls&lt;/code&gt;: Configures automatic TLS certificate generation for &lt;code&gt;blogs.drunk.dev&lt;/code&gt; using Cert Manager. The certificate will be stored in a Kubernetes secret named &lt;code&gt;tls-blogs-lets&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;annotations&lt;/code&gt;: The force-ssl-redirect: 'true' annotation ensures that all HTTP traffic is redirected to HTTPS, securing the communication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the Ingress resource is created, Cert Manager will automatically issue a TLS certificate for &lt;code&gt;blogs.drunk.dev&lt;/code&gt; and bind it to the Ingress. The certificate will be monitored and automatically renewed by Cert Manager before expiration, ensuring continuous security without manual intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;So, that’s how I secured internal communications within my private AKS environment using Cert Manager and Cloudflare DNS management. This approach simplified the management of SSL certificates and provided an extra layer of security for internal data transmissions.&lt;/p&gt;

&lt;p&gt;I hope you found this walkthrough helpful or at least interesting. If you have any thoughts, questions, or would like to share your own experiences, feel free to reach out. I’m always keen to hear how others are tackling similar challenges!&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aks</category>
      <category>certmanager</category>
    </item>
    <item>
      <title>[k8s] Step-By-Step Guide: Hosting Longhorn on K3s (ARM)</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:26:49 +0000</pubDate>
      <link>https://forem.com/baoduy2412/k8s-step-by-step-guide-hosting-longhorn-on-k3s-arm-4nkd</link>
      <guid>https://forem.com/baoduy2412/k8s-step-by-step-guide-hosting-longhorn-on-k3s-arm-4nkd</guid>
      <description>&lt;p&gt;As you know by default the kubernetes provide a &lt;strong&gt;"local-path"&lt;/strong&gt; storage. However, this local storage has many limitation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Node Affinity&lt;/strong&gt;: When using local-path storage, the volume created is tied to the specific node where the pod runs.&lt;br&gt;
If that node goes down for maintenance or any other reason, the pods won’t be able to start on other nodes because they won’t find their volumes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not Network Storage&lt;/strong&gt;: Local-path storage is not network-based. The volume remains local to the K3s node where the pod executes.&lt;br&gt;
It doesn't allow data sharing across nodes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not Suitability for Production&lt;/strong&gt;: While local-path storage is suitable for small, single-node development clusters,&lt;br&gt;
it’s not recommended for production-grade multi-node clusters.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What is Longhorn?
&lt;/h2&gt;

&lt;p&gt;Longhorn, an innovative open-source project by &lt;strong&gt;Rancher Labs&lt;/strong&gt;, offers a reliable, lightweight, and user-friendly distributed block storage system for Kubernetes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;High Availability&lt;/strong&gt;: Longhorn replicates storage volumes across multiple nodes in the Kubernetes cluster,
ensuring that data remains available even if a node fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost-Effective&lt;/strong&gt;: Traditional external storage arrays can be expensive and non-portable. Longhorn offers a cost-effective,
cloud-native solution that can run anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disaster Recovery&lt;/strong&gt;: Longhorn allows you to easily create a disaster recovery volume in another Kubernetes cluster and fail over to it in the event of an emergency.
This ensures that your applications can quickly recover with a defined Recovery Point Objective (RPO) and Recovery Time Objective (RTO).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Longhorn installation
&lt;/h2&gt;

&lt;p&gt;Longhorn provides a straightforward method for installing the iSCSI driver and NFSv4 directly on all nodes. Follow the steps below to set up the necessary components.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Installing open-iscsi&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The open-iscsi package is a prerequisite for Longhorn to create distributed volumes that can be shared across nodes.&lt;br&gt;
   Ensure that this driver is installed on all worker nodes within your cluster.&lt;/p&gt;

&lt;p&gt;Execute the following command on your cluster to install the driver&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# please check the latest release of the longhorn here https://github.com/longhorn/longhorn and update the version accordingly. Current version is v1.6.0&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/longhorn/longhorn/v1.6.0/deploy/prerequisite/longhorn-iscsi-installation.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deploying the iSCSI driver, confirm the status of the installer pods using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod | &lt;span class="nb"&gt;grep &lt;/span&gt;longhorn-iscsi-installation

&lt;span class="c"&gt;# The result&lt;/span&gt;
longhorn-iscsi-installation-pdbgq   1/1     Running   0          21m
longhorn-iscsi-installation-qplbb   1/1     Running   0          39m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, review the installation logs to ensure successful deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs longhorn-iscsi-installation-pdbgq &lt;span class="nt"&gt;-c&lt;/span&gt; iscsi-installation

&lt;span class="c"&gt;# The result&lt;/span&gt;
...
IProcessing triggers &lt;span class="k"&gt;for &lt;/span&gt;libc-bin &lt;span class="o"&gt;(&lt;/span&gt;2.35-0ubuntu3.6&lt;span class="o"&gt;)&lt;/span&gt; ...
Processing triggers &lt;span class="k"&gt;for &lt;/span&gt;man-db &lt;span class="o"&gt;(&lt;/span&gt;2.10.2-1&lt;span class="o"&gt;)&lt;/span&gt; ...
Processing triggers &lt;span class="k"&gt;for &lt;/span&gt;initramfs-tools &lt;span class="o"&gt;(&lt;/span&gt;0.140ubuntu13.1&lt;span class="o"&gt;)&lt;/span&gt; ...
update-initramfs: Generating /boot/initrd.img-6.5.0-18-generic
iscsi &lt;span class="nb"&gt;install &lt;/span&gt;successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the iscsi installed successfully, Then you can safely uninstall the above with following command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/longhorn/longhorn/v1.6.0/deploy/prerequisite/longhorn-iscsi-installation.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Installing NFSv4 client&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To enable Longhorn’s backup functionality and ensure proper operation, the NFSv4 client must be installed on the worker nodes within your cluster.&lt;/p&gt;

&lt;p&gt;Follow these steps to set up the necessary components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# please check the latest release of the longhorn here https://github.com/longhorn/longhorn and update the version accordingly. Current version is v1.6.0&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/longhorn/longhorn/v1.6.0/deploy/prerequisite/longhorn-nfs-installation.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deploying the NFSv4 client, confirm the status of the installer pods using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod | &lt;span class="nb"&gt;grep &lt;/span&gt;longhorn-nfs-installation

&lt;span class="c"&gt;# The results&lt;/span&gt;
NAME                                  READY   STATUS    RESTARTS   AGE
longhorn-nfs-installation-mt5p7   1/1     Running   0          143m
longhorn-nfs-installation-n6nnq   1/1     Running   0          143m
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And also can check the log with the following command to see the installation result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs longhorn-nfs-installation-mt5p7 &lt;span class="nt"&gt;-c&lt;/span&gt; nfs-installation

&lt;span class="c"&gt;# The results&lt;/span&gt;
...
rpc-svcgssd.service is a disabled or a static unit, not starting it.
rpc_pipefs.target is a disabled or a static unit, not starting it.
var-lib-nfs-rpc_pipefs.mount is a disabled or a static unit, not starting it.
Processing triggers &lt;span class="k"&gt;for &lt;/span&gt;man-db &lt;span class="o"&gt;(&lt;/span&gt;2.10.2-1&lt;span class="o"&gt;)&lt;/span&gt; ...
Processing triggers &lt;span class="k"&gt;for &lt;/span&gt;libc-bin &lt;span class="o"&gt;(&lt;/span&gt;2.35-0ubuntu3.6&lt;span class="o"&gt;)&lt;/span&gt; ...
nfs &lt;span class="nb"&gt;install &lt;/span&gt;successfully
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the NFSv4 installed successfully, Then you can safely uninstall the above with following command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/longhorn/longhorn/v1.6.0/deploy/prerequisite/longhorn-nfs-installation.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Installing Longhorn&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Preparing the configuration &lt;code&gt;value.yaml&lt;/code&gt; file
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;csi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kubeletRootDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/lib/kubelet"&lt;/span&gt;
&lt;span class="na"&gt;defaultSettings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;diskType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flesystem"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Install Longhorn in the &lt;code&gt;longhorn-system&lt;/code&gt; namespace with configuration above.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. added longhorn chart&lt;/span&gt;
helm repo add longhorn https://charts.longhorn.io
helm repo update

&lt;span class="c"&gt;# 2. Installing&lt;/span&gt;
helm &lt;span class="nb"&gt;install &lt;/span&gt;longhorn longhorn/longhorn &lt;span class="nt"&gt;-f&lt;/span&gt; value.yaml &lt;span class="nt"&gt;--namespace&lt;/span&gt; longhorn-system &lt;span class="nt"&gt;--create-namespace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed successfully and all the pods are up and running.&lt;br&gt;
You should be able to access Longhorn UI through the &lt;code&gt;longhorn-frontend&lt;/code&gt; service (needs port-forward or expose through nginx or cloudflare tunnel).&lt;br&gt;
&lt;a href="/assets/ks-hosting-longhorn-on-kubernetes/longhorn-ui.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-hosting-longhorn-on-kubernetes/longhorn-ui.png" width="600px"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Uninstalling Longhorn&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any reason we would like to uninstall Longhorn helm then the below commands will help.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Update `deleting-confirmation-flag` to allows uninstall longhorn&lt;/span&gt;
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; longhorn-system patch &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;'{"value": "true"}'&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merge lhs deleting-confirmation-flag
&lt;span class="c"&gt;# Uninstall longhorn&lt;/span&gt;
helm uninstall longhorn &lt;span class="nt"&gt;-n&lt;/span&gt; longhorn-system
&lt;span class="c"&gt;# Delete namespace&lt;/span&gt;
kubectl delete namespace longhorn-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using the Longhorn storage with Mariadb
&lt;/h3&gt;

&lt;p&gt;To explore Longhorn storage capabilities, we’ll set up a MariaDB Galera multi-primary database cluster for synchronous replication and high availability. Follow these steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Setup chart repo&lt;/span&gt;
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

&lt;span class="c"&gt;# Install mariadb-ha&lt;/span&gt;
helm &lt;span class="nb"&gt;install &lt;/span&gt;mariadb-ha bitnami/mariadb-galera &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--set&lt;/span&gt; global.storageClass&lt;span class="o"&gt;=&lt;/span&gt;longhorn &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--set&lt;/span&gt; rootUser.password&lt;span class="o"&gt;=&lt;/span&gt;Pass@word1 &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--set&lt;/span&gt; galera.mariabackup.password&lt;span class="o"&gt;=&lt;/span&gt;Password1 &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--set&lt;/span&gt; db.name&lt;span class="o"&gt;=&lt;/span&gt;drunk_db &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--namespace&lt;/span&gt; db &lt;span class="nt"&gt;--create-namespace&lt;/span&gt;

&lt;span class="c"&gt;# Uninstall mariadb-ha&lt;/span&gt;
helm uninstall mariadb-ha &lt;span class="nt"&gt;-n&lt;/span&gt; db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deployed successful, you should find three MariaDB pods running in the &lt;code&gt;db&lt;/code&gt; namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; db

&lt;span class="c"&gt;# The results&lt;/span&gt;
NAME                          READY   STATUS    RESTARTS   AGE
mariadb-ha-mariadb-galera-0   1/1     Running   0          5m6s
mariadb-ha-mariadb-galera-1   1/1     Running   0          3m44s
mariadb-ha-mariadb-galera-2   1/1     Running   0          2m41s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll also find three persistent volumes in the Longhorn UI portal. !Longhorn Volumes&lt;br&gt;
&lt;a href="/assets/ks-hosting-longhorn-on-kubernetes/longhorn-volumes.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-hosting-longhorn-on-kubernetes/longhorn-volumes.png" width="600px"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Longhorn System Backup
&lt;/h3&gt;

&lt;p&gt;Longhorn supports a variety of backup targets, including Azure Storage, AWS S3, Google Storage, NFS, and SMB/CIFS.&lt;br&gt;
This post will initially cover the configuration for Azure Storage, with subsequent posts addressing the other backup targets.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;System Backup with Azure Storage&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This section assumes that you already have Azure Storage and a Kubernetes cluster that can connect to Azure.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a &lt;code&gt;longhorn-azure-blob-backup.yaml&lt;/code&gt; secret with your Azure Storage Account credentials:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Update the secret below with your Azure Storage Account credentials&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;longhorn-azure-blob-backup&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;longhorn-system&lt;/span&gt;
&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AZBLOB_ACCOUNT_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_STORAGE_KEY"&lt;/span&gt;
  &lt;span class="na"&gt;AZBLOB_ACCOUNT_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_STORAGE_NAME"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Apply the secret to the cluster using the following command.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; longhorn-azure-blob-backup.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Next, update the backup information in the defaultSettings section of the &lt;code&gt;value.yaml&lt;/code&gt; file:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;csi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kubeletRootDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/lib/kubelet"&lt;/span&gt;
&lt;span class="na"&gt;defaultSettings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;diskType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flesystem"&lt;/span&gt;
  &lt;span class="na"&gt;backupTargetCredentialSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longhorn-azure-blob-backup"&lt;/span&gt; &lt;span class="c1"&gt;#The name of the secret created above.&lt;/span&gt;
  &lt;span class="na"&gt;backupTarget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;azblob://YOUR_CONTAINER_NAME@core.windows.net/"&lt;/span&gt; &lt;span class="c1"&gt;# Trailer slash is important&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Upgrade the Longhorn Helm chart with the updated values using the following command:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade longhorn longhorn/longhorn &lt;span class="nt"&gt;-f&lt;/span&gt; value.yaml &lt;span class="nt"&gt;--namespace&lt;/span&gt; longhorn-system

&lt;span class="c"&gt;# You should see a message indicating that the upgrade was successful&lt;/span&gt;
Release &lt;span class="s2"&gt;"longhorn"&lt;/span&gt; has been upgraded. Happy Helming!
NAME: longhorn
LAST DEPLOYED: Wed Feb 28 08:57:56 2024
NAMESPACE: longhorn-system
STATUS: deployed
REVISION: 2
TEST SUITE: None
NOTES:
Longhorn is now installed on the cluster!

Please &lt;span class="nb"&gt;wait &lt;/span&gt;a few minutes &lt;span class="k"&gt;for &lt;/span&gt;other Longhorn components such as CSI deployments, Engine Images, and Instance Managers to be initialized.

Visit our documentation at https://longhorn.io/docs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Once the upgrade is complete, the backup information under &lt;code&gt;Setting&lt;/code&gt; → &lt;code&gt;General&lt;/code&gt; should reflect the changes made.&lt;br&gt;
&lt;a href="/assets/ks-hosting-longhorn-on-kubernetes/long-horn-azure-backup.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-hosting-longhorn-on-kubernetes/long-horn-azure-backup.png" width="600px"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After creating a sys-backup under &lt;code&gt;Setting&lt;/code&gt; → &lt;code&gt;System Backup&lt;/code&gt;, you should see the following&lt;br&gt;
&lt;a href="/assets/ks-hosting-longhorn-on-kubernetes/azure-sys-bakup.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-hosting-longhorn-on-kubernetes/azure-sys-bakup.png" width="600px"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You should be able to see the data being backed up into the Azure Storage Container.&lt;br&gt;
&lt;a href="/assets/ks-hosting-longhorn-on-kubernetes/azure-storage-account.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-hosting-longhorn-on-kubernetes/azure-storage-account.png" width="600px"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>k3s</category>
      <category>kubernetes</category>
      <category>longhorn</category>
      <category>storage</category>
    </item>
    <item>
      <title>[k8s] Step-By-Step Guide: Hosting Outline VPN on Kubernetes</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:26:16 +0000</pubDate>
      <link>https://forem.com/baoduy2412/k8s-step-by-step-guide-hosting-outline-vpn-on-kubernetes-2dhj</link>
      <guid>https://forem.com/baoduy2412/k8s-step-by-step-guide-hosting-outline-vpn-on-kubernetes-2dhj</guid>
      <description>&lt;p&gt;In our &lt;a href="https://dev.to/posts/ks-02-Install-nginx-on-pi-cluster"&gt;previous article&lt;/a&gt;,&lt;br&gt;
we were successfully installed NGINX on Kubernetes.Now, we're going to take NGINX for a spin and use it to host the Outline VPN on Kubernetes and open up our connection ports.&lt;/p&gt;

&lt;p&gt;You're probably aware that by default, Outline VPN changes the client port each time a new connection is made.&lt;br&gt;
It can be a bit of a challenge when we need to expose the ports through NGINX and get the outbound port on the whitelist at the firewall level.&lt;/p&gt;

&lt;p&gt;However, there is a feature that allows us to modify the Outline VPN's default configuration during deployment.&lt;br&gt;
With this little tweak, we can get all connections to pass through a single port and manage to expose both the management and client ports through NGINX.&lt;br&gt;
Stick with us as we walk you through this process.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install Outline VPN
&lt;/h2&gt;

&lt;p&gt;Before proceeding with the installation of the Outline, it's essential to define some variables as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Management Port (60000)&lt;/strong&gt;: This port facilitates the connection between the Outline Manager and the VPN server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Port (40000)&lt;/strong&gt;: This port is assigned for client devices to establish a connection with the VPN server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hostname (vpn.drunkcoding.net)&lt;/strong&gt;: This DNS (Domain Name Server) allows client devices to communicate with the VPN server from the public internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Let's start with a new &lt;em&gt;Outline-system&lt;/em&gt; namespace creation.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace outline-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Create a Self-sign certificate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To install Outline VPN, a required certificate plays multiple crucial roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server Authentication&lt;/strong&gt;: This certificate is key in verifying the VPN server you're connecting to is the genuine one.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Encryption&lt;/strong&gt;: This certificate establishes the encryption for all data transmissions between your device and the VPN server.&lt;br&gt;
If someone happens to intercept the data, they won't be able to decipher it due to this encryption.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Secured Connection Assurance&lt;/strong&gt;: This essential certificate ensures the connection between the client devices and the VPN server is both private and secure.&lt;br&gt;
This is particularly crucial when connecting from insecure networks like public Wi-Fi.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's generate a self-signed certificate.&lt;br&gt;
It can be accomplished through various methods, each tailored to specific environments.&lt;/p&gt;

&lt;p&gt;Here, we'll be using OpenSSL as an example. The commands illustrating this process are detailed below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Create private key&lt;/span&gt;
openssl genrsa &lt;span class="nt"&gt;-out&lt;/span&gt; private.key 2048

&lt;span class="c"&gt;# 2. Create a Certificate Signing Request (CSR) using the private key with some parameters as below:&lt;/span&gt;
&lt;span class="c"&gt;#-----&lt;/span&gt;
&lt;span class="c"&gt;# - Country Name (2 letter code) [AU]:VN&lt;/span&gt;
&lt;span class="c"&gt;# - State or Province Name (full name) [Some-State]:VN&lt;/span&gt;
&lt;span class="c"&gt;# - Locality Name (eg, city) []:HCM&lt;/span&gt;
&lt;span class="c"&gt;# - Organization Name (eg, company) [Internet Widgits Pty Ltd]:drunkcoding&lt;/span&gt;
&lt;span class="c"&gt;# - Organizational Unit Name (eg, section) []:DCN&lt;/span&gt;
&lt;span class="c"&gt;# - Common Name (e.g. server FQDN or YOUR name) []:vpn.drunkcoding.net&lt;/span&gt;
&lt;span class="c"&gt;# - Email Address []:system@drunkcoding.net&lt;/span&gt;

&lt;span class="c"&gt;# - Please enter the following 'extra' attributes&lt;/span&gt;
&lt;span class="c"&gt;# - to be sent with your certificate request&lt;/span&gt;
&lt;span class="c"&gt;# - A challenge password []:123456&lt;/span&gt;
&lt;span class="c"&gt;# - An optional company name []:DCN&lt;/span&gt;
openssl req &lt;span class="nt"&gt;-new&lt;/span&gt; &lt;span class="nt"&gt;-key&lt;/span&gt; private.key &lt;span class="nt"&gt;-out&lt;/span&gt; csr_request.csr

&lt;span class="c"&gt;# 3. Create a self-signed certificate&lt;/span&gt;
openssl x509 &lt;span class="nt"&gt;-req&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 365 &lt;span class="nt"&gt;-in&lt;/span&gt; csr_request.csr &lt;span class="nt"&gt;-signkey&lt;/span&gt; private.key &lt;span class="nt"&gt;-out&lt;/span&gt; cert.crt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the certificate is generated, following command to import into kubernetes cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret tls tls-outline-vpn-imported &lt;span class="nt"&gt;--cert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cert.crt &lt;span class="nt"&gt;--key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;private.key &lt;span class="nt"&gt;--namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;outline-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Deploy Outline Service with following &lt;code&gt;outline-deployment.yaml&lt;/code&gt; file.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PersistentVolumeClaim&lt;/strong&gt;:
A PersistentVolumeClaim named &lt;em&gt;outline-vpn-claim&lt;/em&gt; is created
which requests storage is being created in the namespace &lt;em&gt;outline-system&lt;/em&gt; to store the configuration of Outline VPN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment&lt;/strong&gt;: A Deployment named &lt;em&gt;outline-vpn&lt;/em&gt; is being created in the namespace &lt;em&gt;outline-system&lt;/em&gt;.
With this Deployment, a Pod is created with a container that uses the image quay.io/outline/shadowbox:stable. The container defines two TCP ports (40000, 60000) to expose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service&lt;/strong&gt;: A Service named &lt;em&gt;outline-vpn&lt;/em&gt; is configured to expose the Pods and expose the ports (40000 and 60000) for both TCP and UDP protocols.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 1. PersistentVolume&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn-claim&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-system&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;phase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bound&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class="na"&gt;capacity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5Gi&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5Gi&lt;/span&gt;
  &lt;span class="c1"&gt;#TODO: Remember to change `storageClassName` according to your environment.&lt;/span&gt;
  &lt;span class="na"&gt;storageClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local-path&lt;/span&gt;
  &lt;span class="na"&gt;volumeMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Filesystem&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;#2. Pod Deployment&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;server-config-volume&lt;/span&gt;
          &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shadowbox-config&lt;/span&gt;
          &lt;span class="na"&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;claimName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn-claim&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tls&lt;/span&gt;
          &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tls-outline-vpn-imported&lt;/span&gt;
            &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tls.crt&lt;/span&gt;
                &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shadowbox.crt&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tls.key&lt;/span&gt;
                &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shadowbox.key&lt;/span&gt;
            &lt;span class="na"&gt;defaultMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;420&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quay.io/outline/shadowbox:stable&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40000&lt;/span&gt;
              &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60000&lt;/span&gt;
              &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SB_API_PORT&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;60000"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SB_API_PREFIX&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;b782eecb-bb9e-58be-614a-d5de1431d6b3&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SB_CERTIFICATE_FILE&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/shadowbox.crt&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SB_PRIVATE_KEY_FILE&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/shadowbox.key&lt;/span&gt;
          &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;server-config-volume&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/cache&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shadowbox-config&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/outline&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shadowbox-config&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/root/shadowbox&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tls&lt;/span&gt;
              &lt;span class="na"&gt;readOnly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/shadowbox.crt&lt;/span&gt;
              &lt;span class="na"&gt;subPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shadowbox.crt&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tls&lt;/span&gt;
              &lt;span class="na"&gt;readOnly&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
              &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/shadowbox.key&lt;/span&gt;
              &lt;span class="na"&gt;subPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shadowbox.key&lt;/span&gt;
          &lt;span class="na"&gt;lifecycle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;postStart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;exec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/bin/sh&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
                    &lt;span class="s"&gt;echo&lt;/span&gt;
                    &lt;span class="s"&gt;'{"rollouts":[{"id":"single-port","enabled":true}],"portForNewAccessKeys":40000,"hostname":"vpn.drunkcoding.net"}'&lt;/span&gt;
                    &lt;span class="s"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="s"&gt;/root/shadowbox/persisted-state/shadowbox_server_config.json;&lt;/span&gt;
          &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Always&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Always&lt;/span&gt;
      &lt;span class="na"&gt;terminationGracePeriodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
  &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RollingUpdate&lt;/span&gt;
    &lt;span class="na"&gt;rollingUpdate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;maxUnavailable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;25%&lt;/span&gt;
      &lt;span class="na"&gt;maxSurge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;25%&lt;/span&gt;
  &lt;span class="na"&gt;revisionHistoryLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;progressDeadlineSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;# 3. Service&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-system&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apiport-tcp&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60000&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60000&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apiport-udp&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UDP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60000&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60000&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;accessport-tcp&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40000&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40000&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;accessport-udp&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UDP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40000&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40000&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;outline-vpn&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIP&lt;/span&gt;
  &lt;span class="na"&gt;internalTrafficPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cluster&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it on the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; outline-deployment.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon successful deployment, you should be able to see a pod along with its corresponding logs as in the screenshot below.&lt;br&gt;
&lt;a href="/assets/ks-hosting-outline-vpn-kubernetes/outline-vpn-deployed-successfully.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-hosting-outline-vpn-kubernetes/outline-vpn-deployed-successfully.png" alt="outline-vpn-deployed-successfully.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  PostStart command &amp;amp; Environment variables explanation
&lt;/h2&gt;

&lt;p&gt;In the deployment lifecycle configuration above, there is a postStart command&lt;br&gt;
that creates a JSON file located at &lt;strong&gt;/root/shadowbox/persisted-state/shadowbox_server_config.json&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The structure of the JSON object should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rollouts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"single-port"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"portForNewAccessKeys"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hostname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vpn.drunkcoding.net"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break down the components of the JSON configuration for the Outline VPN:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;rollouts&lt;/strong&gt;: This has an ID 'single-port', indicating to the Outline VPN that client connections are to be allowed on this single port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;portForNewAccessKeys&lt;/strong&gt;: This represents the port number (40000), where new access keys will be created.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hostname&lt;/strong&gt;: This refers to the domain name or IP address of the VPN server the clients will connect to, in this case being "vpn.drunkcoding.net".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also a few environment variables to acknowledge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SB_API_PORT&lt;/strong&gt;: It denotes the port exposed by the Outline Management API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SB_API_PREFIX&lt;/strong&gt;: It's a random GUID to be used as a prefix on the Outline Management API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SB_CERTIFICATE_FILE&lt;/strong&gt;: It points to a file, /tmp/shadowbox.crt, which corresponds to the certificate we created earlier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SB_PRIVATE_KEY_FILE&lt;/strong&gt;: This points to the path of the private key file. In this case, it's /tmp/shadowbox.key. This file matches the aforementioned certificate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exposing connection ports through nginx
&lt;/h2&gt;

&lt;p&gt;Once we've completed the installation as mentioned above, our Outline Container will be active and providing services on ports 60000 and 40000.&lt;br&gt;
Currently, this service is confined to the Kubernetes network, meaning they can't be accessed directly from external sources, such as the internet.&lt;/p&gt;

&lt;p&gt;In order to make these services receptive to outside connections, we'll need to expose the ports through NGINX.&lt;br&gt;
The following illustration provides a visual representation on how to do this.&lt;br&gt;
&lt;a href="/assets/ks-hosting-outline-vpn-kubernetes/outline-nginx-kubernetes.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-hosting-outline-vpn-kubernetes/outline-nginx-kubernetes.png" alt="outline-nginx-kubernetes.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To begin with, we need to update the &lt;code&gt;values.yaml&lt;/code&gt; file from our previous NGINX deployment. This configuration will open up the needed ports.&lt;/p&gt;

&lt;p&gt;Here's the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Refer to line 155 and 160 here for details&lt;/span&gt;
&lt;span class="c1"&gt;# https://github.com/kubernetes/ingress-nginx/blob/main/charts/ingress-nginx/values.yaml&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Expose the TCP with convention tcp: 'port':'namespace/service:port'&lt;/span&gt;
&lt;span class="na"&gt;tcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;60000&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outline-system/outline-vpn:60000"&lt;/span&gt;
  &lt;span class="na"&gt;40000&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outline-system/outline-vpn:40000"&lt;/span&gt;

&lt;span class="c1"&gt;# 2. Expose the UDP with convention udp: 'port':'namespace/service:port'&lt;/span&gt;
&lt;span class="na"&gt;udp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;40000&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outline-system/outline-vpn:40000"&lt;/span&gt;

&lt;span class="na"&gt;controller&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;loadBalancerIP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.1.85"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After updating the values.yaml with the correct information, re-upgrade the helm chart using the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. To install brand new nginx&lt;/span&gt;
helm &lt;span class="nb"&gt;install &lt;/span&gt;nginx ingress-nginx/ingress-nginx &lt;span class="nt"&gt;--values&lt;/span&gt; values.yaml &lt;span class="nt"&gt;-n&lt;/span&gt; nginx-ingress

&lt;span class="c"&gt;# 2. To update the existing nginx&lt;/span&gt;
helm upgrade nginx ingress-nginx/ingress-nginx &lt;span class="nt"&gt;--values&lt;/span&gt; values.yaml &lt;span class="nt"&gt;-n&lt;/span&gt; nginx-ingress

&lt;span class="c"&gt;# 3. TO delete existing nginx and re-install with step 1 above.&lt;/span&gt;
helm delete nginx &lt;span class="nt"&gt;-n&lt;/span&gt; nginx-ingress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Managing the Outline VPN
&lt;/h2&gt;

&lt;p&gt;Follow these steps to manage your Outline VPN:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Download the Outline Manager&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Download the Outline Manager from &lt;a href="https://getoutline.org/get-started/#step-1" rel="noopener noreferrer"&gt;Outline's official website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Prepare the Connection Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prepare the connection configuration with the following parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;apiUrl&lt;/strong&gt;: Use the following format &lt;code&gt;https://{hostname}:{management-port}/{SB_API_PREFIX}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;certSha256&lt;/strong&gt;: This is the thumbprint of the certificate created. Use the following command to access the thumbprint:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;&lt;span class="nv"&gt;SHA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-fingerprint&lt;/span&gt; &lt;span class="nt"&gt;-sha256&lt;/span&gt; &lt;span class="nt"&gt;-inform&lt;/span&gt; pem &lt;span class="nt"&gt;-in&lt;/span&gt; cert.crt | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s2"&gt;"s/://g"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/.*=//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Generate the Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Our configuration should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apiUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://vpn.drunkcoding.net:60000/b782eecb-bb9e-58be-614a-d5de1431d6b3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"certSha256"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"34B3C8EB1C6EC9B5335556D7E8DC73A30152D27C66B054BAB8ACF5D11AE0C810"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Setup Outline Anywhere&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open the Outline Manager App and click &lt;strong&gt;Setup Outline Anywhere&lt;/strong&gt;. Paste the configuration into the second input box and click Done. .&lt;br&gt;
&lt;a href="/assets/ks-hosting-outline-vpn-kubernetes/outline-manager-config.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-hosting-outline-vpn-kubernetes/outline-manager-config.png" alt="outline-manager-config.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Connect to the Outline VPN Server&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On successful configuration, you should be able to connect to the Outline VPN server as shown below:&lt;br&gt;
&lt;a href="/assets/ks-hosting-outline-vpn-kubernetes/outline-manager-server.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-hosting-outline-vpn-kubernetes/outline-manager-server.png" alt="outline-manager-server.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Create a Connection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To allow the clients to connect to the server, click Add new key to create a connection and note down the access key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpQc1YxY0V0ZkhzSVFueEJFVEVsMFRF@vpn.drunkcoding.net:40000/?outline=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting Started with the Outline Client
&lt;/h2&gt;

&lt;p&gt;To get started with the VPN, follow the steps below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Begin by navigating to the &lt;a href="https://getoutline.org/get-started/#step-3" rel="noopener noreferrer"&gt;Outline official website&lt;/a&gt; to download the client application. Be sure to select the version compatible with your platform architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Launch the client application. Select &lt;strong&gt;Add Access Key&lt;/strong&gt;, then input the access key mentioned above. Afterward, click on &lt;strong&gt;Add Server&lt;/strong&gt;.&lt;br&gt;
&lt;a href="/assets/ks-hosting-outline-vpn-kubernetes/outline-client-config.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-hosting-outline-vpn-kubernetes/outline-client-config.png" width="400px"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Proceed by clicking on the 'Connect' button to establish a connection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To verify the VPN server's capability, access &lt;a href="https://www.myip.info" rel="noopener noreferrer"&gt;myip.info&lt;/a&gt;. Upon successful connection, your public IP address should reflect the Kubernetes outbound public IP. This means the VPN server is functioning as expected.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>k3s</category>
      <category>kubernetes</category>
      <category>nginx</category>
      <category>outline</category>
    </item>
    <item>
      <title>[k8s] Step-By-Step Guide: Nginx Alternative with Cloudflare Tunnel, Enables services to internet a public static IP address</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:25:42 +0000</pubDate>
      <link>https://forem.com/baoduy2412/k8s-step-by-step-guide-nginx-alternative-with-cloudflare-tunnel-enables-services-to-internet-a-1nmb</link>
      <guid>https://forem.com/baoduy2412/k8s-step-by-step-guide-nginx-alternative-with-cloudflare-tunnel-enables-services-to-internet-a-1nmb</guid>
      <description>&lt;p&gt;In the previous articles we successfully:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/posts/ks-01-Install-k3s-on-pi-cluster"&gt;Install kubernetes cluster&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/posts/ks-02-Install-nginx-on-pi-cluster"&gt;Install Ngix on cluster&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/posts/ks-03-install-cert-manager-free-ssl-kubernetes-cluster"&gt;Install SSL for Ingress with Cert-Manager&lt;/a&gt; or with &lt;a href="https://dev.to/posts/ks-04-cert-manager-alternative-with-cloudflare"&gt;Cloudflare SSL&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, to expose our services to the internet, it's essential to obtain a &lt;strong&gt;Static Public IP Address&lt;/strong&gt; and configure the &lt;strong&gt;router/firewall to open ports 80 and 443&lt;/strong&gt;.&lt;br&gt;
If for any reason we're unable to meet these requirements, we won't be able to expose our services online.&lt;/p&gt;

&lt;p&gt;In addition, if our organization relies on private Kubernetes on cloud platforms such as AKS, EKS, or GKS, and we wish to expose a select set of services to the internet&lt;br&gt;
without jeopardizing security, we might consider using &lt;a href="https://www.cloudflare.com/products/tunnel/" rel="noopener noreferrer"&gt;Cloudflare Tunnel&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's delve into this topic further.&lt;/p&gt;

&lt;p&gt;&lt;a href="/assets/ks-public-services-with-cloudflare-tunnel/argo-tunnel-network-diagram-1024x491.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-public-services-with-cloudflare-tunnel/argo-tunnel-network-diagram-1024x491.png" width="600px"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Tunnel Configuration
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;This guide assumes that we have followed the instructions from our &lt;a href="https://dev.to/posts/ks-03-install-cert-manager-free-ssl-kubernetes-cluster"&gt;previous post&lt;/a&gt;, and configured Cloudflare account with at least one onboarded domain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The next step is to create a &lt;a href="https://one.dash.cloudflare.com" rel="noopener noreferrer"&gt;Cloudflare Zero Trust Account&lt;/a&gt;. While creating this account might require adding a payment method, please note that there are no charges for the first 50 users. You can proceed to register without any hesitation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="/assets/ks-public-services-with-cloudflare-tunnel/cloudflare-zero-trust-dashboard.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-public-services-with-cloudflare-tunnel/cloudflare-zero-trust-dashboard.png" width="600px"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Once logged in, navigate to &lt;strong&gt;Access =&amp;gt; Tunnel&lt;/strong&gt; and create a new tunnel. For the purposes of this guide, we'll name it &lt;code&gt;pi-k3s&lt;/code&gt;. After creating the tunnel, make sure to copy the tunnel token that appears (similar to the one below):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhIjoiYWVlMGFjYzZiYejTkz....yzCfgm7oqfnhz2DrJDKyL8PBDr9hR5FvYgDR45TxPiAxbmVW=
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Finally, ensure we click the Save button to confirm the creation of the tunnel.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Kubernetes Tunnel Installation
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create cloudflare Namespace. It's a good practice to install the tunnel in its own namespace. Use this command:
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create the &lt;code&gt;cloudflare-tunnel.yaml&lt;/code&gt; deployment file as below:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apiVersion: v1
kind: Secret
metadata:
  name: cloudflare-tunnel
  namespace: cloudflare
  labels:
    app: cloudflare-tunnel
&lt;span class="nb"&gt;type&lt;/span&gt;: Opaque
stringData:
  &lt;span class="c"&gt;# the cloudflare tunnel token here&lt;/span&gt;
  token: &lt;span class="s1"&gt;'eyJhIjoiYWVlMGFjYzZiYejTkz....yzCfgm7oqfnhz2DrJDKyL8PBDr9hR5FvYgDR45TxPiAxbmVW='&lt;/span&gt;
&lt;span class="nt"&gt;---&lt;/span&gt;
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cloudflare-tunnel
  namespace: cloudflare
  labels:
    app: tunnel
spec:
  replicas: 2
  selector:
    matchLabels:
      app: cloudflare-tunnel
  template:
    metadata:
      labels:
        app: cloudflare-tunnel
    spec:
      containers:
        - name: cloudflare-tunnel
          image: cloudflare/cloudflared:latest
          args:
            - tunnel
            - &lt;span class="s1"&gt;'--no-autoupdate'&lt;/span&gt;
            - run
            - &lt;span class="s1"&gt;'--token'&lt;/span&gt;
            - &lt;span class="si"&gt;$(&lt;/span&gt;token&lt;span class="si"&gt;)&lt;/span&gt;
          envFrom:
            - secretRef:
                name: cloudflare-tunnel
          resources:
            limits:
              cpu: 500m
              memory: 512Mi
            requests:
              cpu: 1m
              memory: 10Mi
          imagePullPolicy: Always
          securityContext:
            readOnlyRootFilesystem: &lt;span class="nb"&gt;true
      &lt;/span&gt;restartPolicy: Always
      terminationGracePeriodSeconds: 30
      dnsPolicy: ClusterFirst
      automountServiceAccountToken: &lt;span class="nb"&gt;false
  &lt;/span&gt;strategy:
    &lt;span class="nb"&gt;type&lt;/span&gt;: RollingUpdate
    rollingUpdate:
      maxUnavailable: 25%
      maxSurge: 25%
  revisionHistoryLimit: 1
  progressDeadlineSeconds: 600
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Install Cloudflare tunnel:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; cloudflare-tunnel.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Verify running pods after deployed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="/assets/ks-public-services-with-cloudflare-tunnel/cloudflare-tunnel-pods.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-public-services-with-cloudflare-tunnel/cloudflare-tunnel-pods.png" alt="cloudflare-tunnel-pods.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Back to the Cloudflare Zero Trust the tunnel status should be in &lt;code&gt;Health&lt;/code&gt; also.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="/assets/ks-public-services-with-cloudflare-tunnel/cloudflare-tunnel-status.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-public-services-with-cloudflare-tunnel/cloudflare-tunnel-status.png" alt="cloudflare-tunnel-status.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Exposing the Application to the Internet
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to the tunnel configuration page. Under the 'Public Host' section, add a public host name to expose our &lt;code&gt;echo service&lt;/code&gt; to the internet. See the example below.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note: Kubernetes internal service URLs follow the convention: &lt;code&gt;http://{service-name}.{namespace}.svc.cluster.local&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="/assets/ks-public-services-with-cloudflare-tunnel/cloudflare-echo-service-config.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-public-services-with-cloudflare-tunnel/cloudflare-echo-service-config.png" alt="Cloudflare Public Host Configuration"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Verify the performance of the application by visiting &lt;code&gt;https://echo.drunkcoding.net&lt;/code&gt;. The service should be accessible without any constraints.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I have established two endpoints as follows and conducted a load test using Postman.&lt;br&gt;
The results were astonishing as the Cloudflare tunnel delivered speeds even greater than direct access via Nginx.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;echo-nginx.drunkcoding.net&lt;/strong&gt;: This endpoint provides access to the echo application through Nginx ingress and with a public IP address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;echo.drunkcoding.net&lt;/strong&gt;: This endpoint offers access to the echo application through the Cloudflare tunnel.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please see the results in the attached image.&lt;br&gt;
&lt;a href="/assets/ks-public-services-with-cloudflare-tunnel/load-test.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-public-services-with-cloudflare-tunnel/load-test.png" alt="load-test.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: The cluster hosting this application is based in Singapore, while the test was conducted from a terminal in Vietnam.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Concluding Remarks
&lt;/h2&gt;

&lt;p&gt;Leveraging Cloudflare tunnels simplifies application exposure to the internet without the necessity of below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A public IP Address.&lt;/li&gt;
&lt;li&gt;Port forwarding.&lt;/li&gt;
&lt;li&gt;Firewall whitelisting.&lt;/li&gt;
&lt;li&gt;Nginx proxy/ingress.&lt;/li&gt;
&lt;li&gt;A Cert-manager or Cloudflare origin server certificate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This results in a significantly simplified infrastructure setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>k3s</category>
      <category>kubernetes</category>
      <category>cloudflare</category>
      <category>tunnel</category>
    </item>
    <item>
      <title>[k8s] Step-By-Step Guide: Cert-Manager Alternative with Cloudflare, Implementing Free SSL Certificates for Kubernetes Clusters</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:25:09 +0000</pubDate>
      <link>https://forem.com/baoduy2412/k8s-step-by-step-guide-cert-manager-alternative-with-cloudflare-implementing-free-ssl-4kkk</link>
      <guid>https://forem.com/baoduy2412/k8s-step-by-step-guide-cert-manager-alternative-with-cloudflare-implementing-free-ssl-4kkk</guid>
      <description>&lt;p&gt;In our &lt;a href="https://dev.to/posts/ks-03-install-cert-manager-free-ssl-kubernetes-cluster"&gt;previous post&lt;/a&gt;, we walked through the process of successfully installing Cert-Manager to handle SSL certificate assignments for all ingresses.&lt;br&gt;
While advantageous, this approach comes with a few noteworthy challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Limited validity period&lt;/strong&gt;: Certificates provided by Cert-Manager are valid for a short period (90 days), meaning any third-party systems utilizing the services may need to update their certificate whitelist every 90 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inappropriate for production environments&lt;/strong&gt;: Certificates provided by Let's Encrypt, while useful, may not be suitable for production environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opening Port 80&lt;/strong&gt;: To validate and issue the certificate, Cert-Manager requires the use of insecure HTTP on port 80 during the process. However, if for any reason, the infrastructure team is reluctant to expose port 80 to the internet, then the operation of Cert-Manager may be compromised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Potential Cert-Manager failures&lt;/strong&gt;: There can be instances where Cert-Manager encounters issues and fails to renew the certificate. In such cases, third-party systems may be unable to access our service due to failed certificate verification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By understanding these issues, We'll explore a better solution to replace the Cert-Manager with Cloudflare. More detailed information will follow in subsequent posts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cert-Manager Alternative
&lt;/h2&gt;

&lt;p&gt;Cloudflare provides several valuable services:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Domain Management&lt;/strong&gt;: Makes it simple to purchase, move, and control domain names. It centralised DNS record setup, subdomain management in once place.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Universal SSL (Free)&lt;/strong&gt;: Provides automatic secure HTTPS access to our website. This saves time from the need to buy and manage SSL certificates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Advanced Certificate Manager&lt;/strong&gt;: Perfect for businesses wanting more control over their SSL certificates. We can customize the certificates, including those for multiple subdomain levels.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Proxy&lt;/strong&gt;: Sits between the website's server and its visitors, with &lt;a href="https://www.cloudflare.com/application-services/products/waf/" rel="noopener noreferrer"&gt;WAF enabled&lt;/a&gt; through proxy giving protection from threats like DDoS attacks and bots, while also enhancing performance.&lt;br&gt;
All requests appear to be coming from Cloudflare IP addresses, enabling us to enhance site security by just whitelisting &lt;a href="https://www.cloudflare.com/en-in/ips/" rel="noopener noreferrer"&gt;Cloudflare IP Addresses&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Kickstart with Cloudflare
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Before we get started, ensure that we have a Cloudflare account that can register for one, free of charge, &lt;a href="https://www.cloudflare.com" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The next essential step is to onboard a Domain into Cloudflare's management system. Rest assured, this process is straightforward and involves zero downtime.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For demonstration, I have onboarded my &lt;code&gt;drunkcoding.net&lt;/code&gt; to Cloudflare. Once a Domain is onboarded, it should resemble the following demonstration.&lt;br&gt;
&lt;a href="/assets/ks-cert-manager-alternative-with-cloudflare/cloudflare-drunkcoding-domain.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-cert-manager-alternative-with-cloudflare/cloudflare-drunkcoding-domain.png" alt="Cloudflare Drunkcoding Domain Setup"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Moving forward to the &lt;strong&gt;DNS&lt;/strong&gt; Records management, I have added an A record with the proxy option enabled, as shown below.&lt;br&gt;
&lt;a href="/assets/ks-cert-manager-alternative-with-cloudflare/cloudflare-druncoding-records.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-cert-manager-alternative-with-cloudflare/cloudflare-druncoding-records.png" alt="Cloudflare DrunkCoding Records"&gt;&lt;/a&gt;&lt;br&gt;
Once set up, all requests to subdomains such as &lt;code&gt;echo.drunkcoding.net&lt;/code&gt;, &lt;code&gt;wiki.drunkcoding.net&lt;/code&gt;, etc., will be redirected to my public IP address.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next, under &lt;strong&gt;SSL/TLS&lt;/strong&gt;, I created an Origin Server for the domains &lt;code&gt;drunkcoding.net and *.drunkcoding.net&lt;/code&gt; and chose the certificate validity from 7 days to 15 years.&lt;br&gt;
&lt;a href="/assets/ks-cert-manager-alternative-with-cloudflare/cloudflare-drunkcoding-server-ssl.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-cert-manager-alternative-with-cloudflare/cloudflare-drunkcoding-server-ssl.png" alt="Cloudflare Drunkcoding Server SSL"&gt;&lt;/a&gt;&lt;br&gt;
After setting this up, please ensure to download the certificate and private key immediately and save it to local files as we won't be able to access the private key in the future.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Following this, download the Cloudflare Root CA certificate from &lt;a href="https://developers.cloudflare.com/ssl/static/origin_ca_rsa_root.pem" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once all the above steps are complete, we should have the following three files:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cert.crt&lt;/code&gt;: This public key certification is in PEM format.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;private.key&lt;/code&gt;: This private key of the certificate is also in PEM format.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Root CA&lt;/code&gt;: This root CA certificate is also in PEM format.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Switch the SSL/TLS encryption mode of the domain to &lt;code&gt;Full (strict)&lt;/code&gt;
&lt;img src="/assets/ks-cert-manager-alternative-with-cloudflare/cloudflare-domain-tls-mode.png" alt="cloudflare-domain-tls-mode.png"&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Cloudflare Certificate Installation
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Certificate preparation:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before to proceeding, it is necessary to append the contents of the &lt;code&gt;Root CA&lt;/code&gt; file to the &lt;code&gt;cert.crt&lt;/code&gt; file, as illustrated in the following example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-----BEGIN CERTIFICATE-----
    Content of cert.crt file here
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
    Content of ca file here
-----END CERTIFICATE-----
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Create a secret certificate on kubernetes:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Update the name based on your naming convention. In this context, we're using &lt;code&gt;tls&lt;/code&gt; as the prefix and &lt;code&gt;import&lt;/code&gt; as the suffix.&lt;br&gt;
This denotes that it's a TLS certificate secret imported from a third party, as opposed to being auto-generated by Cert-Manager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret tls tls-drunkcoding-net-import &lt;span class="nt"&gt;--cert&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cert.crt &lt;span class="nt"&gt;--key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;private.key &lt;span class="nt"&gt;--namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;our-namespace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to do this for each namespace in the cluster if there are services utilizing this domain.&lt;/p&gt;

&lt;p&gt;After the successful creation, a secret, similar to the one illustrated below, should be identifiable.&lt;br&gt;
&lt;a href="/assets/ks-cert-manager-alternative-with-cloudflare/ks-drunkcoding-tls-secret.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-cert-manager-alternative-with-cloudflare/ks-drunkcoding-tls-secret.png" alt="ks-drunkcoding-tls-secret.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Update Application Ingress:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Leverage the existing &lt;code&gt;echo-app&lt;/code&gt; ingress. We'll revise the ingress configuration use the imported certificate by change the secretName of the ingress as below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo-ingress&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;kubernetes.io/tls-acme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo.drunkcoding.net&lt;/span&gt;
      &lt;span class="c1"&gt;# only need to change this secret name.&lt;/span&gt;
      &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tls-drunkcoding-net-imported&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo.drunkcoding.net&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt;
            &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo-service&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application will be protected with cloudflare cert, and below is the detailed certificate information as seen from the browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="/assets/ks-cert-manager-alternative-with-cloudflare/cert-details.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-cert-manager-alternative-with-cloudflare/cert-details.png" alt="Browser certificate details"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You may observe that the certificates provided by Cloudflare and Cert-Manager bear a resemblance, owing to their shared use of Let's Encrypt for free SSL certificate generation.&lt;br&gt;
However, it's important to note that for production applications, an upgrade to &lt;a href="https://www.cloudflare.com/lp/pg-advanced-certificate-manager/" rel="noopener noreferrer"&gt;&lt;strong&gt;Cloudflare Advanced Certificate Manager&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
is recommended for Production environment which provides a certificate from a standard, trusted third-party authority and boasts an extended validity period.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After completing these steps, we can proceed with uninstalling the Cert-Manager, as it is no longer needed.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>k3s</category>
      <category>kubernetes</category>
      <category>certmanager</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>[k8s] Step-By-Step Guide: Installation of Cert-Manager, Implementing Free SSL Certificates for Kubernetes Clusters</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:24:36 +0000</pubDate>
      <link>https://forem.com/baoduy2412/k8s-step-by-step-guide-installation-of-cert-manager-implementing-free-ssl-certificates-for-g1i</link>
      <guid>https://forem.com/baoduy2412/k8s-step-by-step-guide-installation-of-cert-manager-implementing-free-ssl-certificates-for-g1i</guid>
      <description>&lt;p&gt;Welcome back to our ongoing dialogue about Kubernetes. In the &lt;a href="https://dev.to/posts/ks-02-Install-nginx-on-pi-cluster"&gt;previous article&lt;/a&gt;, we successfully executed the Nginx installation and made our applications internet-accessible.&lt;br&gt;
However, you might've observed that the applications are operating under the HTTP protocol, which is not secure at present.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As of July 2018, with the release of Chrome 68, Google started marking all non-HTTPS websites as 'Not secure' in the Chrome browser.&lt;br&gt;
This means that if a website doesn't use HTTPS, Chrome displays a warning to users in the address bar, indicating that the connection is not secure.&lt;br&gt;
The goal of this move was to push more webmasters to secure their websites with SSL/TLS certificates, providing a safer browsing experience for users.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Securing our applications through SSL encryption is crucial, particularly in production environments. For enhanced security, it's recommended to procure and implement valid SSL certificates for all production applications.&lt;/p&gt;

&lt;p&gt;However, for the development or testing environments, which may not require paid SSL certificates, you can make use of Cert-Manager.&lt;br&gt;
This tool utilizes Let's Encrypt to generate SSL certificates free of cost, providing a secure and cost-effective solution for &lt;strong&gt;non-production&lt;/strong&gt; environments.&lt;/p&gt;
&lt;h2&gt;
  
  
  Cert-Manager installation
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prerequisite&lt;/strong&gt; - Kubernetes cluster with admin access is required. Make sure you have kubectl installed and configured to interact with your cluster.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add the Jetstack Helm repository&lt;/strong&gt; - Jetstack is the organization that maintains cert-manager, and they provide a Helm repository that we can use to install it:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add jetstack https://charts.jetstack.io
helm repo update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install cert-manager CustomResourceDefinitions&lt;/strong&gt; (CRDs) - These are the resources that cert-manager uses to store its configuration. Run the following command to install the CRDs:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Please make certain that you are utilizing the most up-to-date version. You can achieve this by substituting &lt;code&gt;v1.13.0&lt;/code&gt; with the newest release from the cert-manager official GitHub repository.&lt;br&gt;
Visit the following link to obtain the latest version: &lt;a href="https://github.com/cert-manager/cert-manager/releases" rel="noopener noreferrer"&gt;Cert-Manager Releases&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.crds.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create cert-manager Namespace&lt;/strong&gt; - It's a good practice to install cert-manager in its own namespace. Use this command:
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;value.yaml&lt;/code&gt; file with the content below
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ingressShim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;defaultIssuerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;letsencrypt-prod"&lt;/span&gt;
  &lt;span class="na"&gt;defaultIssuerKind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ClusterIssuer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install cert-manager&lt;/strong&gt; Helm chart - This will install cert-manager along with its components:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;cert-manager jetstack/cert-manager &lt;span class="nt"&gt;--values&lt;/span&gt; values.yaml &lt;span class="nt"&gt;-n&lt;/span&gt; cert-manager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify the Installation&lt;/strong&gt; - Check if the cert-manager pods are running:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="/assets/ks-install-cert-manager-free-ssl-kubernetes-cluster/cert-manager-pod.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-install-cert-manager-free-ssl-kubernetes-cluster/cert-manager-pod.png" alt="cert-manager-pod.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cluster Issuer configuration&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# File name is `cluster-issuer.yaml`&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cert-manager.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIssuer&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# The name should be the same with `defaultIssuerName` above&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cert-manager&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;acme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://acme-v02.api.letsencrypt.org/directory&lt;/span&gt;
    &lt;span class="c1"&gt;# Replace with your domain email.&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support@drunkcoding.net&lt;/span&gt;
    &lt;span class="na"&gt;privateKeySecretRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;letsencrypt-prod&lt;/span&gt;
    &lt;span class="na"&gt;solvers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# The ingress class name of nginx.&lt;/span&gt;
            &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply it on the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; cluster-issuer.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Update echo-app ingress.
&lt;/h2&gt;

&lt;p&gt;Leverage the existing &lt;code&gt;echo-app&lt;/code&gt; to enhance your project. We'll revise the ingress configuration to enable HTTPS for secure communication in our application.&lt;br&gt;
Additionally, we'll integrate with cert-manager for automated SSL certificate generation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo-ingress&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. enable cert-manager for this ingress&lt;/span&gt;
    &lt;span class="na"&gt;kubernetes.io/tls-acme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="c1"&gt;# 3. Config the tls secret name. Replace the domain and secretName below with your config accordingly.&lt;/span&gt;
  &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo.drunkcoding.net&lt;/span&gt;
      &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tls-drunkcoding-net&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo.drunkcoding.net&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt;
            &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo-service&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The certificate generated from &lt;code&gt;Let's Encrypt&lt;/code&gt; has validity of 90 days. However, the &lt;code&gt;cert-manager&lt;/code&gt; will automatically renew the certificate as it nears its expiration date.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Verify application
&lt;/h2&gt;

&lt;p&gt;Once the configuration is properly set up and the certificate has been successfully issued, you should be able to locate a certificate named &lt;code&gt;tls-drunkcoding-net&lt;/code&gt; housed under the &lt;code&gt;default&lt;/code&gt; namespace secrets.&lt;br&gt;
&lt;a href="/assets/ks-install-cert-manager-free-ssl-kubernetes-cluster/echo-app-with-cert.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-install-cert-manager-free-ssl-kubernetes-cluster/echo-app-with-cert.png" alt="tls-drunkcoding-net certificate"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application is also fully functional with SSL for secure communication.&lt;br&gt;
&lt;a href="/assets/ks-install-cert-manager-free-ssl-kubernetes-cluster/cert-drunkcoding-net.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-install-cert-manager-free-ssl-kubernetes-cluster/cert-drunkcoding-net.png" alt="Secure application communication"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below is the detailed certificate information as seen from the browser.&lt;br&gt;
&lt;a href="/assets/ks-install-cert-manager-free-ssl-kubernetes-cluster/cert-details.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-install-cert-manager-free-ssl-kubernetes-cluster/cert-details.png" alt="Browser certificate details"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>k3s</category>
      <category>kubernetes</category>
      <category>certmanager</category>
      <category>ssl</category>
    </item>
    <item>
      <title>[k8s] Step-By-Step Guide: Installing Nginx Ingress on K3s Pi 4 Cluster</title>
      <dc:creator>Steven Hoang</dc:creator>
      <pubDate>Mon, 18 May 2026 14:24:03 +0000</pubDate>
      <link>https://forem.com/baoduy2412/k8s-step-by-step-guide-installing-nginx-ingress-on-k3s-pi-4-cluster-4gbe</link>
      <guid>https://forem.com/baoduy2412/k8s-step-by-step-guide-installing-nginx-ingress-on-k3s-pi-4-cluster-4gbe</guid>
      <description>&lt;p&gt;In our &lt;a href="https://dev.to/posts/ks-01-Install-k3s-on-pi-cluster"&gt;previous article&lt;/a&gt;, we successfully set up a k3s Pi cluster. We will now build upon that foundation. Let's dive in!&lt;/p&gt;

&lt;p&gt;&lt;a href="/assets/ks-Install-k3s-on-pi-cluster/pi-cluster-diagram.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-Install-k3s-on-pi-cluster/pi-cluster-diagram.png" width="600px"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;pi-master&lt;/strong&gt;: 192.168.1.85 (Running Pi OS Lite 64Bit)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pi-node-1&lt;/strong&gt;: 192.168.1.86 (Running Pi OS Lite 64Bit)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pi-node-2&lt;/strong&gt;: 192.168.1.87 (Running Pi OS Lite 64Bit)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Router Port Forwarding Setup.
&lt;/h3&gt;

&lt;p&gt;In order to make the internal applications accessible via the internet, we need to set up port forwarding on our router.&lt;br&gt;
This routing process will redirect internet requests coming to ports 80 and 443 to our master private IP node (192.168.1.85).&lt;/p&gt;

&lt;p&gt;Please note, the configuration interface may vary among different routers. Nonetheless, most broadband routers should offer the same functionality pertaining to port forwarding.&lt;/p&gt;

&lt;p&gt;&lt;a href="/assets/ks-Install-nginx-on-pi-cluster/pi-cluster-port-forwarding-diagram.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-Install-nginx-on-pi-cluster/pi-cluster-port-forwarding-diagram.png" width="600px"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are my current router settings.&lt;/p&gt;

&lt;p&gt;&lt;a href="/assets/ks-Install-nginx-on-pi-cluster/router-port-forwarding-config.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-Install-nginx-on-pi-cluster/router-port-forwarding-config.png" width="550px"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Nginx installation
&lt;/h3&gt;

&lt;p&gt;We're going to start by installing Nginx on our cluster. In the following guide, we will illustrate how to set up and run Nginx on K3s.&lt;br&gt;
At its core, Nginx will listen to inbound requests on the master node's IP address and subsequently forward these requests to the services operating within our cluster.&lt;/p&gt;

&lt;p&gt;&lt;a href="/assets/ks-Install-nginx-on-pi-cluster/pi-cluster-nginx-diagram.png" class="article-body-image-wrapper"&gt;&lt;img alt="pi-cluster-nginx-diagram" src="/assets/ks-Install-nginx-on-pi-cluster/pi-cluster-nginx-diagram.png" width="600px"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Config Ip address&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Helm charts come with a file called &lt;code&gt;values.yaml&lt;/code&gt; which contains the default configuration values.&lt;br&gt;
We can override these values by creating your own values.yaml file. Here is an example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Refer to line 434 here for details&lt;/span&gt;
&lt;span class="c1"&gt;# https://github.com/kubernetes/ingress-nginx/blob/main/charts/ingress-nginx/values.yaml&lt;/span&gt;

&lt;span class="na"&gt;controller&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Our primary node ip address here.&lt;/span&gt;
    &lt;span class="c1"&gt;# Do remember replacing this ip address with your once accordingly.&lt;/span&gt;
    &lt;span class="na"&gt;loadBalancerIP&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.1.85"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Download the Helm chart:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Download the Nginix helm chart. You can do this by adding the Nginx repo to the Helm. Run the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Create namespace&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Install the Helm chart:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now you can install the Helm chart using your custom values.yaml file to override the default configuration values. Run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm &lt;span class="nb"&gt;install &lt;/span&gt;nginx ingress-nginx/ingress-nginx &lt;span class="nt"&gt;--values&lt;/span&gt; values.yaml &lt;span class="nt"&gt;-n&lt;/span&gt; nginx-ingress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Verify nginx pod:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After installed successfully, we should be able to find a pod running there:&lt;br&gt;
&lt;a href="/assets/ks-Install-nginx-on-pi-cluster/nginx-installed-successfully.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-Install-nginx-on-pi-cluster/nginx-installed-successfully.png" alt="nginx-installed-successfully.png"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Nginx Verification
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Deploy the echo application:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can deploy an echo server application using a simple Kubernetes deployment and service.&lt;br&gt;
The echo server will respond with the same request it receives.&lt;/p&gt;

&lt;p&gt;Here is a sample YAML file you can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo-deployment&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ealen/echo-server&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo-service&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this YAML into a file, let's say &lt;code&gt;echo-app.yaml&lt;/code&gt;, and apply it to &lt;code&gt;default&lt;/code&gt; namespace (&lt;em&gt;using &lt;code&gt;default&lt;/code&gt; namespace in Production is not recommended&lt;/em&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; echo-app.yaml &lt;span class="nt"&gt;-n&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create an ingress rule:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now that your echo server is running, you can create an ingress rule to route traffic to it.&lt;/p&gt;

&lt;p&gt;Here is a sample ingress YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo-ingress&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ingressClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Replace the domain below with your domain accordingly.&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo.drunkcoding.net&lt;/span&gt;
      &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/"&lt;/span&gt;
            &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo-service&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Update your domain DNS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To ensure you can access from the internet, you need to point a domain to your public address.&lt;/p&gt;

&lt;p&gt;Here is my &lt;code&gt;drunkcoding.net&lt;/code&gt; DNS configuration on Cloudflare for reference purposes.&lt;br&gt;
&lt;a href="/assets/ks-Install-nginx-on-pi-cluster/drunkcoding-cloudflare-dns.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-Install-nginx-on-pi-cluster/drunkcoding-cloudflare-dns.png" alt="drunkcoding-cloudflare-dns.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After all these configurations now we should be able to access your application hosting on our k3s cluster from the internet.&lt;br&gt;
When accessing to &lt;code&gt;http://echo.drunkcoding.net&lt;/code&gt; you able to see the JSON response from the echo pod as below.&lt;/p&gt;

&lt;p&gt;&lt;a href="/assets/ks-Install-nginx-on-pi-cluster/echo-app-response.png" class="article-body-image-wrapper"&gt;&lt;img src="/assets/ks-Install-nginx-on-pi-cluster/echo-app-response.png" alt="echo-app-response.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank You
&lt;/h2&gt;

&lt;p&gt;Thank you for taking the time to read this guide! I hope it has been helpful, feel free to explore further, and happy coding! 🌟✨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steven&lt;/strong&gt; | &lt;em&gt;&lt;a href="https://github.com/baoduy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>k3s</category>
      <category>kubernetes</category>
      <category>picluster</category>
      <category>nginx</category>
    </item>
  </channel>
</rss>
