<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pahri</title>
    <description>The latest articles on DEV Community by Pahri (@morizal).</description>
    <link>https://dev.to/morizal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057470%2F1f961442-69a6-4b2e-9e6e-ce01e55b424e.jpg</url>
      <title>DEV Community: Pahri</title>
      <link>https://dev.to/morizal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/morizal"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Fri, 09 Oct 2026 04:36:45 +0000</pubDate>
      <link>https://dev.to/morizal/-1i2d</link>
      <guid>https://dev.to/morizal/-1i2d</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/morizal/designing-a-reliable-kubernetes-deployment-with-gitops-464b" class="crayons-story__hidden-navigation-link"&gt;Designing a Reliable Kubernetes Deployment with GitOps&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/morizal" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057470%2F1f961442-69a6-4b2e-9e6e-ce01e55b424e.jpg" alt="morizal profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/morizal" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Pahri
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Pahri
                
                
              
              &lt;div id="story-author-preview-content-4821056" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/morizal" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057470%2F1f961442-69a6-4b2e-9e6e-ce01e55b424e.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Pahri&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/morizal/designing-a-reliable-kubernetes-deployment-with-gitops-464b" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 9&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/morizal/designing-a-reliable-kubernetes-deployment-with-gitops-464b" id="article-link-4821056"&gt;
          Designing a Reliable Kubernetes Deployment with GitOps
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kubernetes"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kubernetes&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/morizal/designing-a-reliable-kubernetes-deployment-with-gitops-464b" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/morizal/designing-a-reliable-kubernetes-deployment-with-gitops-464b#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Designing a Reliable Kubernetes Deployment with GitOps</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Fri, 09 Oct 2026 04:36:34 +0000</pubDate>
      <link>https://dev.to/morizal/designing-a-reliable-kubernetes-deployment-with-gitops-464b</link>
      <guid>https://dev.to/morizal/designing-a-reliable-kubernetes-deployment-with-gitops-464b</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfq8wyggy0e3y6qz6a7h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftfq8wyggy0e3y6qz6a7h.png" alt=" " width="800" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deploying an application to a Kubernetes cluster is not just about building a container image and running it. A reliable deployment workflow must define how artifacts are built, where they are stored, which version is selected for release, and how the desired configuration is applied to the cluster.&lt;/p&gt;

&lt;p&gt;As these responsibilities grow, maintaining a clear relationship between application code, image versions, and deployment manifests becomes increasingly important.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Without an explicit workflow, deployments can become difficult to trace, inconsistent, and teams may lose visibility into which application version is actually running.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The problem becomes clearer when these responsibilities are not explicitly connected:&lt;/p&gt;

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

&lt;p&gt;These challenges motivate a workflow that makes image versions, deployment configuration, and release decisions explicit.&lt;/p&gt;

&lt;p&gt;This is where Continuous Integration (CI) and GitOps complement each other: CI automates building and publishing container images, while GitOps manages the desired deployment state through version-controlled configuration.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the source code in the repository below.&lt;br&gt;
&lt;a href="https://github.com/muhammadyulasfipahrizal/E2E-observability-stack.git" rel="noopener noreferrer"&gt;Application Repository&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/muhammadyulasfipahrizal/gitops.git" rel="noopener noreferrer"&gt;GItops Repository&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;In this project, we build a deployment that establish a workflow in which application artifacts are versioned, deployment changes are traceable, and Kubernetes continuously converges toward an explicitly defined desired state.&lt;/p&gt;

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

&lt;p&gt;This project uses two repositories with separate responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Application repository: Go API source code, Dockerfile, and CircleCI configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GitOps repository: Helm chart and production values that declare the desired Kubernetes configuration.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The supporting components are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;CircleCI builds the application image and pushes it to Harbor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Harbor stores versioned container images.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Helm templates the Kubernetes resources from chart and values.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Argo CD compares the GitOps configuration with the cluster and synchronizes it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kubernetes schedules and runs the workload.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipeline automates image delivery, while release promotion remains explicit: a person updates the GitOps image tag to deliberately select the version to deploy, then manually starts synchronization in Argo CD to control when that version is applied to the cluster.&lt;/p&gt;

&lt;p&gt;Automatically deploying every published image is not always desirable because a successful build does not necessarily mean the image is ready for release. New builds may still require validation, review, or coordination with the target environment.&lt;/p&gt;

&lt;p&gt;Separating image publishing from deployment provides clear release checkpoints and reduces the risk of unreviewed changes reaching users.&lt;/p&gt;

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

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

&lt;h3&gt;
  
  
  Artifact delivery and deployment
&lt;/h3&gt;

&lt;p&gt;The CI path produces an image. The deployment path declares which image should run and applies that desired state to Kubernetes. The GitOps repository is the handoff between them.&lt;/p&gt;

&lt;p&gt;The dashed connection is not an automated pipeline step. CircleCI does not commit to the GitOps repository. After an image is published, the image tag is updated manually in &lt;code&gt;environments/production/values-prod.yaml&lt;/code&gt;. Argo CD then shows the configuration difference; a user manually selects Sync to apply it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application repository
&lt;/h3&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/muhammadyulasfipahrizal/circle" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/circle&lt;/a&gt;&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app/main.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Implements the Go HTTP API, including &lt;code&gt;/health&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app/go.mod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines the Go module and dependencies.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Builds the binary and packages it into a minimal runtime image.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.circleci/config.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines the CircleCI build-and-push job.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  GitOps repository
&lt;/h3&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/muhammadyulasfipahrizal/gitops" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/gitops&lt;/a&gt;&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/app/Chart.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines Helm chart metadata.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/app/values.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Provides default chart values.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/app/templates/_helpers.tpl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines reusable template helpers.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/app/templates/deployment.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines the application Deployment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/app/templates/service.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines the Service.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;charts/app/templates/ingress.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Defines the Ingress resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;environments/production/values-prod.yaml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Supplies production-specific overrides, including image tag, replica count, service port, and resources.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The chart defines reusable resource templates; the production values supply the settings for this deployment. The separation makes it possible to review a release by examining a small Git diff instead of changing Kubernetes resources directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The Go application exposes a &lt;code&gt;/health&lt;/code&gt; endpoint so that we can check whether it responds after deployment.&lt;/p&gt;

&lt;p&gt;The Dockerfile uses a multi-stage build: the first stage compiles the Go application, and the final image contains the binary rather than the Go toolchain.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.22-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /src&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app/go.mod ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;go mod download

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app/ .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 &lt;span class="se"&gt;\
&lt;/span&gt;    go build &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-s -w"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /app .

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static-debian12:nonroot&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app /app&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; nonroot:nonroot&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps build tools out of the runtime image and runs the application as a non-root user. The application and Kubernetes Service are configured to use port &lt;code&gt;8080&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can test the container locally before involving 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;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; pahri-k8s:local &lt;span class="nb"&gt;.&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 pahri-k8s:local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8080/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected response:&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&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;
  
  
  Automate image delivery with CircleCI
&lt;/h3&gt;

&lt;p&gt;The CircleCI configuration lives in &lt;code&gt;.circleci/config.yml&lt;/code&gt;. The configured job uses the custom runner resource class &lt;code&gt;pahri/devops&lt;/code&gt; and an Ubuntu machine executor. It checks out the application repository, authenticates with Harbor, builds the image, and pushes it.&lt;/p&gt;

&lt;p&gt;Registry credentials are supplied through environment variables rather than committed to Git:&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="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login Harbor&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;echo "$HARBOR_PASSWORD" | docker login \&lt;/span&gt;
        &lt;span class="s"&gt;"$HARBOR_REGISTRY" \&lt;/span&gt;
        &lt;span class="s"&gt;-u "$HARBOR_USERNAME" \&lt;/span&gt;
        &lt;span class="s"&gt;--password-stdin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image is tagged with CircleCI's &lt;code&gt;$CIRCLE_SHA1&lt;/code&gt;, linking the published artifact to the source commit:&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="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Image&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;IMAGE="$HARBOR_REGISTRY/production/pahri-k8s"&lt;/span&gt;
      &lt;span class="s"&gt;docker build -t "$IMAGE:$CIRCLE_SHA1" .&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&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;Push Image&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;IMAGE="$HARBOR_REGISTRY/production/pahri-k8s"&lt;/span&gt;
      &lt;span class="s"&gt;docker push "$IMAGE:$CIRCLE_SHA1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The registry address and credentials must be configured in the CircleCI environment. The snippets above show the relevant commands; the complete job also needs its normal CircleCI job and workflow structure.&lt;/p&gt;

&lt;p&gt;The image name used by this project is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;harbor.k8s.local/production/pahri-k8s:&amp;lt;commit-sha&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  Confirm the artifact in Harbor
&lt;/h3&gt;

&lt;p&gt;Harbor is the private registry for the image that the Kubernetes workload will pull. After CircleCI finishes successfully, confirm that the repository contains the new commit-SHA tag.&lt;/p&gt;

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

&lt;p&gt;At this stage, the artifact exists in the registry, but the running deployment has not necessarily changed. Kubernetes uses the image reference declared in the GitOps configuration, not simply the most recently pushed image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define the workload with Helm
&lt;/h3&gt;

&lt;p&gt;The chart under &lt;code&gt;charts/app/&lt;/code&gt; defines the Kubernetes resources. The production values file declares the desired image, replica count, service port, and resource requests and limits.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;replicaCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;harbor.k8s.local/production/pahri-k8s&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;&amp;lt;tag&amp;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;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;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;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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The replica count expresses the desired number of application replicas. Requests inform scheduling, while limits define the configured CPU and memory ceilings for each container. The Service provides a stable in-cluster endpoint, and the Ingress template defines external HTTP routing when that resource is enabled and configured.&lt;/p&gt;

&lt;p&gt;Before synchronizing a change, inspect the rendered manifests from the root of the &lt;code&gt;GitOps&lt;/code&gt; repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm template pahri-k8s ./charts/app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; environments/production/values-prod.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review the rendered image reference, replica count, Service, and resource settings. Rendering the chart lets us inspect the intended resources without applying them to the cluster.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Promote an image by updating the GitOps repository
&lt;/h3&gt;

&lt;p&gt;This step is intentionally manual. CircleCI publishes the image, but it does not edit or commit to the GitOps repository. The operator selects the image version to promote by updating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;environments/production/values-prod.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For 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="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;harbor.k8s.local/production/pahri-k8s&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;&amp;lt;new-tag&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the exact tag that CircleCI pushed successfully. Then commit and push the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add environments/production/values-prod.yaml
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Update production application image"&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Git diff records which version is being promoted and preserves that change in the repository history. This is a deliberate control point: a successful image build does not automatically mean that the new image should be deployed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Synchronize in Argo CD
&lt;/h3&gt;

&lt;p&gt;Argo CD reads the GitOps repository and compares the desired configuration with the live resources. In this project, synchronization is initiated manually rather than automatically.&lt;/p&gt;

&lt;p&gt;After pushing the updated values file:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Open the application in Argo CD. &lt;/li&gt;
&lt;li&gt; Review the application status and the pending differences.&lt;/li&gt;
&lt;li&gt; Confirm that the intended image tag is shown in the desired configuration.&lt;/li&gt;
&lt;li&gt; Select Sync to apply the desired state.&lt;/li&gt;
&lt;li&gt; Wait for the workload rollout and check the application health.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;This manual Sync action is separate from the manual Git edit. The Git commit changes the desired state; clicking Sync requests that Argo CD apply it. Keeping both actions explicit provides a clear release decision and avoids suggesting that image publication alone deploys a new version.&lt;/p&gt;

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

&lt;p&gt;A &lt;code&gt;Synced&lt;/code&gt; status indicates that the tracked resources match the desired state Argo CD is using. Health status provides additional information about managed resources, but neither one alone proves that an application endpoint responds correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the deployed release
&lt;/h3&gt;

&lt;p&gt;Verify the deployment at three levels: Kubernetes workload readiness, the image reference, and the application endpoint.&lt;/p&gt;

&lt;p&gt;Check the Deployment, Pods, and Service in the 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 deployment,pods,svc &lt;span class="nt"&gt;-n&lt;/span&gt; pahri-k8s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Deployment should show the expected number of available replicas, and the Pods should become ready. If they do not, inspect the Pod and recent events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod &amp;lt;pod-name&amp;gt; &lt;span class="nt"&gt;-n&lt;/span&gt; pahri-k8s

kubectl get events &lt;span class="nt"&gt;-n&lt;/span&gt; pahri-k8s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.metadata.creationTimestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the image reference on the 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 get deployment &lt;span class="nt"&gt;-n&lt;/span&gt; pahri-k8s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{range .items[*]}{.metadata.name}{"\t"}{.spec.template.spec.containers[*].image}{"\n"}{end}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image should match the commit-SHA tag in &lt;code&gt;values-prod.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the application does not have a reachable Ingress or external endpoint, use port forwarding to test it through the Kubernetes Service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward &lt;span class="nt"&gt;-n&lt;/span&gt; pahri-k8s &lt;span class="se"&gt;\&lt;/span&gt;
  svc/pahri-k8s-app 8080:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://IP:8080/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected response:&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&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;If the application is exposed through Ingress, test that route separately as well. Port-forwarding validates the Service-to-application path, not the full external access path.&lt;/p&gt;

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

&lt;p&gt;The evidence should tell one consistent story: CircleCI published a specific image tag, the GitOps values file declares that tag, the operator manually synchronized it through Argo CD, and Kubernetes is running the declared image while the application responds successfully.&lt;/p&gt;

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

&lt;p&gt;This project demonstrates how Continuous Integration and GitOps can work together to create a deployment workflow that is traceable, predictable, and controlled.&lt;/p&gt;

&lt;p&gt;The key design decision is to separate artifact delivery from release promotion. Publishing an image does not automatically make it a production release. Instead, an operator explicitly selects the image version in Git and initiates synchronization through Argo CD. These two control points make release decisions visible, preserve configuration history, and reduce the risk of deploying unreviewed changes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This approach prioritizes release control and traceability over fully automated deployment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ultimately, a reliable deployment workflow is not defined by how many steps it automates, but by how clearly it connects artifacts, configuration, and release decisions. By making these relationships explicit, this project provides a practical foundation for managing Kubernetes deployments with greater confidence and accountability.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:12:56 +0000</pubDate>
      <link>https://dev.to/morizal/-41m</link>
      <guid>https://dev.to/morizal/-41m</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/morizal/understanding-kubernetes-networking-with-cilium-4be" class="crayons-story__hidden-navigation-link"&gt;Understanding Kubernetes Datapath With Cilium&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/morizal" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057470%2F1f961442-69a6-4b2e-9e6e-ce01e55b424e.jpg" alt="morizal profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/morizal" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Pahri
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Pahri
                
                
              
              &lt;div id="story-author-preview-content-4771299" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/morizal" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057470%2F1f961442-69a6-4b2e-9e6e-ce01e55b424e.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Pahri&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/morizal/understanding-kubernetes-networking-with-cilium-4be" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 29&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/morizal/understanding-kubernetes-networking-with-cilium-4be" id="article-link-4771299"&gt;
          Understanding Kubernetes Datapath With Cilium
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/architecture"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;architecture&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/kubernetes"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;kubernetes&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/networking"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;networking&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/morizal/understanding-kubernetes-networking-with-cilium-4be" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;2&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/morizal/understanding-kubernetes-networking-with-cilium-4be#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            17 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Understanding Kubernetes Datapath With Cilium</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Tue, 29 Sep 2026 12:06:56 +0000</pubDate>
      <link>https://dev.to/morizal/understanding-kubernetes-networking-with-cilium-4be</link>
      <guid>https://dev.to/morizal/understanding-kubernetes-networking-with-cilium-4be</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvb0dcmcywxtf8kj1c6s6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvb0dcmcywxtf8kj1c6s6.png" alt=" " width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes network might look simple when a cluster is small.&lt;/p&gt;

&lt;p&gt;A pod can communicate with another pod, an application can expose a service, and clients can connect to the application. &lt;/p&gt;

&lt;p&gt;However as the cluster grows, networking becomes more than just connecting one pod to another.&lt;/p&gt;

&lt;p&gt;For example, consider a simple application running inside Kubernetes. A request may start from one Pod and reach another Pod through a Kubernetes Service.&lt;/p&gt;

&lt;p&gt;Now consider a request coming from outside the cluster.&lt;/p&gt;

&lt;p&gt;The traffic needs to enter the cluster, reach a LoadBalancer address, be handled by the networking layer, and eventually reach the application Pod.&lt;/p&gt;

&lt;p&gt;This raises an important question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What actually happens to a packet as it travels from one Pod, through a Kubernetes Service, and eventually reaches an application from outside the cluster?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Answering this question requires looking beyond Kubernetes objects such as Pods and SFervices. We also need to understand the components responsible for turning those objects into actual network traffic.&lt;/p&gt;

&lt;p&gt;Traditionally, Kubernetes networking relies on multiple components working together to provide service connectivity and traffic forwarding.&lt;/p&gt;

&lt;p&gt;In this lab, we explore what changes when &lt;strong&gt;Cilium&lt;/strong&gt; becomes the networking layer.&lt;/p&gt;

&lt;p&gt;Rather than treating Cilium simply as a CNI plugin, we will look at how it participates in the Kubernetes networking datapath and how its components work together to handle different types of traffic.&lt;/p&gt;

&lt;p&gt;This article will cover several topics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cilium's networking model&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;eBPF based service handling&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;kube proxy replacement&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LoadBalancer IP allocation with LB-IPAM&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Layer 2 advertisement&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Envoy based ingress&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Kubernetes networking is not just about assigning IP addresses to Pods. It is about building a datapath that determines how packets move between workloads, Services, nodes, and external networks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Environment
&lt;/h2&gt;

&lt;p&gt;Before looking at the datapath, it is useful to establish the environment in which the experiments are performed.&lt;/p&gt;

&lt;p&gt;The Kubernetes cluster runs on Ubuntu virtual machines hosted by a physical server using KVM/libvirt. The cluster is provisioned with &lt;code&gt;kubeadm&lt;/code&gt;, while &lt;code&gt;containerd&lt;/code&gt; is used as the container runtime.&lt;/p&gt;

&lt;p&gt;The environment is intentionally kept close to a small on premises Kubernetes deployment rather than relying on a managed Kubernetes service. This makes the networking behavior, particularly LoadBalancer IP allocation and Layer 2 advertisement, visible from the underlying network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubernetes Cluster
&lt;/h3&gt;

&lt;p&gt;The cluster uses a multi control-plane architecture to provide a highly available Kubernetes control plane.&lt;/p&gt;

&lt;p&gt;The Kubernetes API is exposed through HAProxy to provide a stable endpoint for cluster administration and for nodes joining the control plane.&lt;/p&gt;

&lt;p&gt;The cluster consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;3 control plane nodes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;3 worker nodes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ubuntu virtual machines&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;kubeadm&lt;/code&gt; for cluster provisioning&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;containerd&lt;/code&gt; as the container runtime&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HAProxy&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Networking Environment
&lt;/h3&gt;

&lt;p&gt;The cluster runs inside a libvirt network hosted on a physical server.&lt;/p&gt;

&lt;p&gt;At a high level, the environment looks like this:&lt;/p&gt;

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

&lt;p&gt;The Kubernetes nodes communicate through the node network, while Pods use a separate Pod CIDR managed by Cilium.&lt;/p&gt;

&lt;p&gt;For the experiments in this article, the relevant network ranges are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Network&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node network&lt;/td&gt;
&lt;td&gt;&lt;code&gt;192.168.122.0/24&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod CIDR&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.244.0.0/16&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LoadBalancer IP range&lt;/td&gt;
&lt;td&gt;&lt;code&gt;192.168.122.241–192.168.122.254&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kubernetes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1.34.11&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cilium&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v1.20.2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The LoadBalancer addresses are allocated from the same Layer 2 network as the Kubernetes nodes.&lt;/p&gt;

&lt;p&gt;This is important because the LoadBalancer IP is not provided by an external cloud provider. Cilium must allocate an address from the configured pool and make that address reachable from the local network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Infrastructure Automation
&lt;/h3&gt;

&lt;p&gt;The underlying Kubernetes environment is provisioned and configured through Ansible.&lt;/p&gt;

&lt;p&gt;The automation covers the infrastructure and cluster setup, including the Kubernetes nodes, container runtime, control-plane configuration, HAProxy, and Cilium deployment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The implementation is maintained in the accompanying GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/cilium-k8s.git" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/cilium-k8s.git&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The repository is used to make the underlying Kubernetes environment reproducible, while this article focuses on understanding the networking behavior implemented on top of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes Networking Before Cilium
&lt;/h2&gt;

&lt;p&gt;Before looking at what Cilium changes, we need to establish the networking model that Kubernetes presents to applications.&lt;/p&gt;

&lt;p&gt;A Kubernetes application does not normally communicate using a single type of IP address.&lt;/p&gt;

&lt;p&gt;A Pod has its own IP address. A Service provides a stable virtual address in front of a group of Pods. Nodes have their own addresses on the underlying network, and applications exposed outside the cluster may use a LoadBalancer address or an Ingress endpoint.&lt;/p&gt;

&lt;p&gt;These addresses belong to different layers of the networking path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Different Networking Layers
&lt;/h3&gt;

&lt;p&gt;A simplified view of the path looks like this:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Pod network
&lt;/h3&gt;

&lt;p&gt;Each Pod receives an IP address from the cluster's Pod CIDR. This address identifies the Pod within the Kubernetes networking environment.&lt;/p&gt;

&lt;p&gt;For example, an application might have an address such as:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The important property is that the application does not need to know which physical or virtual machine is hosting the Pod. The networking layer is responsible for making Pod connectivity work across nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Service network&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A Service provides a stable virtual IP in front of one or more Pods. Instead of connecting directly to a changing Pod IP, clients can connect to the Service:&lt;/p&gt;

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

&lt;p&gt;The Service therefore introduces an abstraction between the client and the actual workload.&lt;/p&gt;

&lt;p&gt;The client does not need to know which Pod should receive the packet.&lt;/p&gt;

&lt;p&gt;Something inside the networking datapath must make that decision.&lt;/p&gt;

&lt;p&gt;A Service definition describes the desired connectivity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service
  name: nginx
  port: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But a packet arriving at the Service IP does not automatically know that it should be forwarded to one of the backend Pods.&lt;/p&gt;

&lt;p&gt;The networking implementation must translate that logical Service into forwarding behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Node network
&lt;/h3&gt;

&lt;p&gt;Every Kubernetes node has an IP address on the underlying network. This address allows the node to communicate with other nodes and with networks outside the cluster.&lt;/p&gt;

&lt;p&gt;At this point, the packet is no longer concerned only with Kubernetes abstractions. It has to interact with the actual network connecting the nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;External network&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;For externally exposed applications, Kubernetes can provide an address that is reachable outside the cluster. Depending on the environment, this may be a cloud provided LoadBalancer address, a manually assigned address, or an address advertised directly on the local network.&lt;/p&gt;

&lt;p&gt;In this lab, the LoadBalancer addresses come from the local node network.&lt;/p&gt;

&lt;p&gt;This creates a complete path across several networking layers:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Where kube-proxy Fits
&lt;/h3&gt;

&lt;p&gt;One of the key components in the traditional Kubernetes networking model is kube proxy.&lt;/p&gt;

&lt;p&gt;A Kubernetes Service can have multiple backend endpoints, while clients see only one stable Service IP.&lt;/p&gt;

&lt;p&gt;The networking layer therefore needs to perform a service translation:&lt;/p&gt;

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

&lt;p&gt;The exact implementation can vary, but the fundamental idea remains the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kubernetes needs a datapath mechanism that turns Service definitions and endpoints into packet forwarding rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When a request is sent to a Service, kube proxy is responsible for programming the node's networking layer so that traffic can be redirected&lt;/p&gt;

&lt;h2&gt;
  
  
  Cilium's Role
&lt;/h2&gt;

&lt;p&gt;Pods need connectivity, Services need to map stable virtual addresses to backend workloads, and externally exposed applications need a path between the Kubernetes network and the surrounding network.&lt;/p&gt;

&lt;p&gt;Traditionally, these responsibilities are implemented by multiple networking components.&lt;/p&gt;

&lt;p&gt;Cilium approaches the problem differently.&lt;/p&gt;

&lt;p&gt;It starts as a CNI plugin, but its role does not end after a Pod receives an IP address. Cilium provides the datapath responsible for processing network traffic and can take responsibility for Service handling, network policy, load balancing, observability, and other networking functions.&lt;/p&gt;

&lt;h3&gt;
  
  
  CNI vs Networking Datapath
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Container Network Interface (CNI)&lt;/strong&gt; defines how a container runtime invokes networking plugins when a Pod is created or removed.&lt;/p&gt;

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

&lt;p&gt;This is an important responsibility, but it represents only one part of Kubernetes networking.&lt;/p&gt;

&lt;p&gt;Once the Pod is running, packets continuously move through the networking datapath.&lt;/p&gt;

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

&lt;p&gt;The CNI lifecycle answers questions such as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How does this Pod become connected to the network?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The datapath answers a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What should happen to each packet after the Pod is connected?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cilium participates in both.&lt;/p&gt;

&lt;p&gt;Cilium can provide the initial Pod networking, while its agents and datapath continue to manage how traffic is processed after the Pod has started.&lt;/p&gt;

&lt;p&gt;This also allows several Kubernetes networking functions to be handled by the same networking system.&lt;/p&gt;

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

&lt;p&gt;The result is a networking layer that understands more than just Pod interfaces.&lt;/p&gt;

&lt;p&gt;It can observe Kubernetes resources and translate their desired state into actual packet processing behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  eBPF as the Datapath
&lt;/h3&gt;

&lt;p&gt;Cilium uses &lt;strong&gt;Extended Berkeley Packet Filter (eBPF&lt;/strong&gt;) to implement significant parts of its networking datapath.&lt;/p&gt;

&lt;p&gt;eBPF allows programs to run inside the Linux kernel at specific networking and system execution points.&lt;/p&gt;

&lt;p&gt;For Cilium, this provides a way to process packets close to where they enter and move through the kernel networking stack.&lt;/p&gt;

&lt;p&gt;The high-level architecture can be simplified to:&lt;/p&gt;

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

&lt;p&gt;The &lt;strong&gt;Cilium Agent&lt;/strong&gt; runs in userspace and is responsible for observing the Kubernetes environment and configuring the datapath.&lt;/p&gt;

&lt;p&gt;The eBPF programs then perform packet processing in the kernel.&lt;/p&gt;

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

&lt;p&gt;The agent does not need to make every packet decision itself.&lt;/p&gt;

&lt;p&gt;Instead, it configures the datapath so that packets can be processed directly in the kernel.&lt;/p&gt;

&lt;p&gt;This becomes particularly important for Kubernetes Services.&lt;/p&gt;

&lt;p&gt;With a traditional kube-proxy based architecture, Service definitions are translated into networking rules such as iptables or IPVS rules.&lt;/p&gt;

&lt;p&gt;With Cilium, Service handling can instead be implemented using eBPF-based load-balancing and forwarding.&lt;/p&gt;

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

&lt;p&gt;compared with:&lt;/p&gt;

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

&lt;p&gt;The important difference is not simply that one uses iptables while the other uses eBPF.&lt;/p&gt;

&lt;p&gt;The larger architectural difference is &lt;strong&gt;where networking decisions are implemented and how the datapath is programmed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Cilium moves important packet-processing logic into the kernel, allowing the datapath to make decisions without requiring a userspace proxy to process every connection.&lt;/p&gt;

&lt;p&gt;However, this does not mean that everything in Cilium runs inside the kernel.&lt;/p&gt;

&lt;p&gt;Cilium still has userspace components.&lt;/p&gt;

&lt;p&gt;For example, the Cilium Agent manages the datapath, while Envoy can handle application-layer traffic such as HTTP and TLS when Cilium's ingress functionality is used.&lt;/p&gt;

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

&lt;p&gt;This distinction will become important later.&lt;/p&gt;

&lt;p&gt;Not every networking problem should be solved at the same layer.&lt;/p&gt;

&lt;p&gt;Some decisions can be made at the kernel level, such as Service load balancing and packet forwarding.&lt;/p&gt;

&lt;p&gt;Other decisions require understanding application-level protocols, where a proxy such as Envoy becomes useful.&lt;/p&gt;

&lt;p&gt;This gives us a more useful mental model of Cilium:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cilium is not only responsible for connecting Pods to the network. It provides a programmable networking datapath that connects Kubernetes state with actual packet processing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From here, the individual components we enabled in the lab start to make more sense.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Kube-proxy replacement, changes how Kubernetes Services are implemented.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LB-IPAM, determines how LoadBalancer addresses are allocated.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;L2 Announcement, makes those addresses reachable from the local network.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Envoy, provides the application-layer entry point for Ingress traffic.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are not isolated features. They are different parts of the same networking path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Establishing the Baseline
&lt;/h2&gt;

&lt;p&gt;Before changing the Kubernetes networking datapath, we first need to establish what the cluster looks like in its initial state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify the Cilium Nodes
&lt;/h3&gt;

&lt;p&gt;Cilium maintains its own representation of Kubernetes nodes through &lt;code&gt;CiliumNode&lt;/code&gt; resources.&lt;/p&gt;

&lt;p&gt;We can inspect them with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get ciliumnodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;What this demonstrates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Cilium recognizes the Kubernetes nodes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each node participates in the Cilium networking environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kubernetes node networking state is represented through Cilium resources.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives us the first indication that Cilium is not simply providing connectivity during Pod creation.&lt;/p&gt;

&lt;p&gt;It maintains networking state for the cluster and uses that state to construct its datapath.&lt;/p&gt;




&lt;h3&gt;
  
  
  Pod-to-Pod Communication
&lt;/h3&gt;

&lt;p&gt;The next step is to verify basic workload connectivity.&lt;/p&gt;

&lt;p&gt;We create two lightweight test Pods and use them to observe how traffic moves through the cluster.&lt;/p&gt;

&lt;p&gt;The simplified communication path is:&lt;/p&gt;

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

&lt;p&gt;We first verify that a Pod can resolve a Kubernetes Service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nslookup kubernetes.default.svc.cluster.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;What this demonstrates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The Pod can reach Kubernetes DNS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kubernetes Service discovery is functioning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Pod network can communicate with the cluster's DNS service.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can then verify direct connectivity between the test Pods.&lt;/p&gt;

&lt;p&gt;Here i create two pods using &lt;code&gt;busybox:1.36&lt;/code&gt; and testing their connectivity inside the cluster&lt;/p&gt;

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

&lt;p&gt;What this demonstrates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Pods can communicate across the Kubernetes network layer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The CNI datapath is forwarding workload traffic successfully.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pod connectivity works before introducing the additional Cilium features examined later.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Replacing kube-proxy with Cilium
&lt;/h2&gt;

&lt;p&gt;The baseline confirms that the cluster can provide basic Pod networking and Kubernetes Service connectivity.&lt;/p&gt;

&lt;p&gt;However, there is still another component involved in the Service datapath: &lt;code&gt;kube-proxy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In a traditional Kubernetes deployment, &lt;code&gt;kube-proxy&lt;/code&gt; watches Services and their endpoints and programs the node networking layer so that traffic sent to a Service can reach one of its backend Pods.&lt;/p&gt;

&lt;p&gt;This model works, but it introduces another component responsible for translating Kubernetes Service state into packet forwarding behavior.&lt;/p&gt;

&lt;p&gt;Since Cilium already provides a programmable networking datapath, a natural question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happens if Cilium takes responsibility for Kubernetes Service handling as well?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why Replace kube-proxy?
&lt;/h3&gt;

&lt;p&gt;With the traditional Kubernetes networking model, kube-proxy programs the node's networking rules to implement Service forwarding.&lt;/p&gt;

&lt;p&gt;With Cilium's kube-proxy replacement, these Service networking functions can instead be handled directly by Cilium and eBPF, allowing traffic to be processed closer to the kernel networking layer.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

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

&lt;p&gt;we move toward a datapath where Cilium handles Service translation:&lt;/p&gt;

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

&lt;p&gt;This changes the architecture rather than simply changing a configuration flag.&lt;/p&gt;

&lt;p&gt;The main benefits are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A more direct datapath, Service translation and forwarding can be handled by eBPF in the kernel instead of relying on kube-proxy-managed networking rules.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lower networking overhead, Removing an additional Service-proxying layer can simplify packet processing, particularly in clusters with a large number of Services and endpoints.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Better integration with Cilium, Service handling becomes part of the same datapath used for networking, load balancing, security, and observability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scalable load balancing, Cilium can perform Service load balancing using eBPF-based mechanisms, including efficient handling of large endpoint sets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;More control over networking behavior, Cilium exposes features such as socket-level load balancing, direct routing, and other eBPF-based datapath capabilities that go beyond traditional kube-proxy behavior.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Enabling kube-proxy Replacement
&lt;/h3&gt;

&lt;p&gt;Cilium exposes kube-proxy replacement through its configuration.&lt;/p&gt;

&lt;p&gt;The relevant setting in this lab is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubeProxyReplacement=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration tells Cilium to take responsibility for Kubernetes Service handling through its own datapath.&lt;/p&gt;

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

&lt;p&gt;The Cilium Agent observes Kubernetes Service and endpoint state and programs the eBPF datapath accordingly.&lt;/p&gt;

&lt;p&gt;This means that the Service abstraction remains the same from the application's perspective.&lt;/p&gt;

&lt;p&gt;Applications still communicate with a Kubernetes Service.&lt;/p&gt;

&lt;p&gt;What changes is the mechanism underneath that abstraction.&lt;/p&gt;

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




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

&lt;h3&gt;
  
  
  Removing kube-proxy
&lt;/h3&gt;

&lt;p&gt;Once Cilium's kube-proxy replacement is active, we can inspect the existing &lt;code&gt;kube-proxy&lt;/code&gt; DaemonSet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system get ds kube-proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DaemonSet can then be removed from the cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl -n kube-system delete ds kube-proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Cilium is correctly providing the Service datapath, removing &lt;code&gt;kube-proxy&lt;/code&gt; should not prevent applications from communicating through Kubernetes Services.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  Verification
&lt;/h3&gt;

&lt;p&gt;Now we repeat the same networking tests used during the baseline.&lt;/p&gt;

&lt;p&gt;This is important because we want to compare behavior rather than introduce an entirely different test.&lt;/p&gt;

&lt;p&gt;The tests include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;DNS resolution&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pod-to-Pod connectivity&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kubernetes Service connectivity&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First, we verify that Kubernetes DNS is still reachable.&lt;/p&gt;

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

&lt;p&gt;We then repeat the Pod-to-Pod connectivity test.&lt;/p&gt;

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

&lt;p&gt;Finally, we test connectivity through a Kubernetes Service.&lt;/p&gt;

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

&lt;p&gt;The important observation is that the Kubernetes abstraction has not changed. What changed is the datapath underneath it.&lt;/p&gt;

&lt;h2&gt;
  
  
  From ClusterIP to LoadBalancer
&lt;/h2&gt;

&lt;p&gt;A Pod can communicate with another Pod, and a Kubernetes Service can provide a stable endpoint in front of those workloads.&lt;/p&gt;

&lt;p&gt;But what happens when the client is &lt;strong&gt;outside the cluster&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;LoadBalancer&lt;/code&gt; Service introduces another requirement. The Service needs an IP address that external clients can actually reach.&lt;/p&gt;

&lt;p&gt;In a cloud environment, this is usually handled by a cloud controller that provisions an external load balancer and assigns an address to the Service.&lt;/p&gt;

&lt;p&gt;Our environment does not have a cloud provider.&lt;/p&gt;

&lt;p&gt;Instead, we want Kubernetes to expose a Service using an address from the same Layer 2 network as the Kubernetes nodes:&lt;/p&gt;

&lt;p&gt;This creates two separate networking problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Which IP address should the LoadBalancer Service use?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How does the local network learn that this IP is reachable through the Kubernetes cluster?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cilium solves these problems with two different components:&lt;br&gt;&lt;br&gt;
LB-IPAM and L2 Announcement&lt;/p&gt;
&lt;h3&gt;
  
  
  LB-IPAM
&lt;/h3&gt;

&lt;p&gt;Cilium LB-IPAM provides IP address management for Kubernetes &lt;code&gt;LoadBalancer&lt;/code&gt; Services.&lt;/p&gt;

&lt;p&gt;In this lab, the available addresses come from a dedicated pool:&lt;/p&gt;

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

&lt;p&gt;The allocation process looks like this:&lt;/p&gt;

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

&lt;p&gt;LB-IPAM can assign:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service → 192.168.122.241
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but that does not automatically make the address reachable from the LAN.&lt;/p&gt;

&lt;p&gt;The network still needs to know where &lt;code&gt;192.168.122.241&lt;/code&gt; exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  L2 Announcement
&lt;/h3&gt;

&lt;p&gt;The LoadBalancer IP belongs to the same Layer 2 network as the Kubernetes nodes.&lt;/p&gt;

&lt;p&gt;When an external client wants to communicate with &lt;code&gt;192.168.122.241&lt;/code&gt;, it needs to resolve that IP to a MAC address using the local network's address-resolution mechanism.&lt;/p&gt;

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

&lt;p&gt;Cilium's &lt;strong&gt;L2 Announcement&lt;/strong&gt; functionality allows a Kubernetes node to announce that it can receive traffic for the LoadBalancer IP.  &lt;/p&gt;

&lt;h3&gt;
  
  
  Expressing the L2 Policy
&lt;/h3&gt;

&lt;p&gt;The L2 policy in this lab specifies which LoadBalancer addresses should be announced and which node interfaces may be used for that announcement.&lt;/p&gt;

&lt;p&gt;The relevant configuration is:&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;loadBalancerIPs&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;interfaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;^ens3$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy expresses two decisions:&lt;/p&gt;

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

&lt;p&gt;This allows Cilium to connect the Kubernetes LoadBalancer abstraction with the actual Layer 2 network interface through which the address becomes reachable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing External Connectivity
&lt;/h3&gt;

&lt;p&gt;At this point, the complete external path can be tested from outside the Service abstraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://192.168.122.241
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the request succeeds, several components have successfully worked together:&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Cilium Ingress and Envoy
&lt;/h2&gt;

&lt;p&gt;Cilium can allocate a LoadBalancer IP through LB-IPAM, advertise that address on the local network using L2 Announcement, and forward traffic into the Kubernetes networking datapath.&lt;/p&gt;

&lt;p&gt;But there is still a limitation.&lt;/p&gt;

&lt;p&gt;A LoadBalancer address tells us where traffic enters the cluster. It does not by itself define how HTTP requests should be routed between multiple applications.&lt;/p&gt;

&lt;p&gt;Consider a cluster running several applications:&lt;/p&gt;

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

&lt;p&gt;Exposing every application independently would require a separate external entry point for each application.&lt;/p&gt;

&lt;p&gt;Instead, we can introduce an HTTP aware entry point:&lt;/p&gt;

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

&lt;p&gt;Now a single LoadBalancer address can act as the entry point for multiple applications.&lt;/p&gt;

&lt;p&gt;The component responsible for understanding the HTTP request is &lt;strong&gt;Envoy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Cilium provides an Ingress controller that translates Kubernetes Ingress configuration into the configuration required by Envoy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubernetes Ingress and the Data Plane
&lt;/h3&gt;

&lt;p&gt;The Kubernetes &lt;code&gt;Ingress&lt;/code&gt; resource describes the desired HTTP routing behavior.&lt;/p&gt;

&lt;p&gt;For example, our test application uses the hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx.k8s.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Ingress&lt;/code&gt; resource describes the desired routing:&lt;/p&gt;

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

&lt;p&gt;The Cilium Ingress Controller observes this Kubernetes configuration and configures Envoy accordingly.&lt;/p&gt;

&lt;p&gt;Envoy then becomes the component that actually handles the HTTP request.&lt;/p&gt;

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

&lt;p&gt;The important point is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The Ingress resource describes the desired HTTP routing, while Envoy becomes the data-plane component handling the request.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is different from the Service datapath we examined earlier.&lt;/p&gt;

&lt;p&gt;At the Service layer, Cilium can make forwarding decisions based on network-level information such as IP addresses and ports.&lt;/p&gt;

&lt;p&gt;At the Ingress layer, the system needs to understand application-level information such as the HTTP &lt;code&gt;Host&lt;/code&gt; header and request path.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host: nginx.k8s.local
Path: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Envoy can inspect this information and determine which configured backend should receive the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Following One HTTP Request
&lt;/h2&gt;

&lt;p&gt;Rather than looking at each component independently, we can now follow a single HTTP request through the entire system.&lt;/p&gt;

&lt;p&gt;The request is generated with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--resolve&lt;/span&gt; nginx.k8s.local:80:192.168.122.241 &lt;span class="se"&gt;\&lt;/span&gt;
  http://nginx.k8s.local/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--resolve&lt;/code&gt; option makes the client connect to &lt;code&gt;192.168.122.241&lt;/code&gt; while still sending the HTTP request for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host: nginx.k8s.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful because the request now contains both pieces of information required by the architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Destination IP
192.168.122.241

Host
nginx.k8s.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can trace the request conceptually as:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The Client Targets the LoadBalancer IP
&lt;/h3&gt;

&lt;p&gt;The client starts with an HTTP request for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://nginx.k8s.local/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--resolve&lt;/code&gt; option maps the hostname to the LoadBalancer address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx.k8s.local: 192.168.122.241
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client therefore sends the packet toward the address allocated by Cilium LB-IPAM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Network Resolves the Destination
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;192.168.122.241&lt;/code&gt; belongs to the local Layer 2 network, the client needs to determine which MAC address should receive the traffic.&lt;/p&gt;

&lt;p&gt;The L2 Announcement mechanism allows Cilium to advertise the LoadBalancer address through an eligible Kubernetes node interface.&lt;/p&gt;

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

&lt;p&gt;The important distinction from the previous section is now visible:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;LB-IPAM provided the address. L2 Announcement made the address reachable on the network.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Packet Enters the Kubernetes Datapath
&lt;/h3&gt;

&lt;p&gt;Once the packet reaches the Kubernetes node, it enters the networking datapath managed by Cilium.&lt;/p&gt;

&lt;p&gt;At this point, the request has crossed from the physical or virtual network into Kubernetes. The packet can now be processed according to the networking configuration established by Cilium.&lt;/p&gt;

&lt;h3&gt;
  
  
  Envoy Handles the HTTP Request
&lt;/h3&gt;

&lt;p&gt;The request reaches the Envoy based Ingress path.&lt;/p&gt;

&lt;p&gt;Unlike a simple L3/L4 forwarding decision, Envoy can inspect HTTP information such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host: nginx.k8s.local
Path: /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Ingress configuration tells Cilium which HTTP routes should exist, while Envoy implements those routes in the data plane.&lt;/p&gt;

&lt;p&gt;Envoy therefore determines that the request should be forwarded to the Service associated with the Ingress rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Service Resolves the Backend
&lt;/h3&gt;

&lt;p&gt;The request is then forwarded toward the Kubernetes Service:&lt;/p&gt;

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

&lt;p&gt;The Service provides the stable abstraction between the Ingress layer and the actual application Pods.&lt;/p&gt;

&lt;p&gt;The client does not need to know which Pod is running the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cilium Selects the Backend
&lt;/h3&gt;

&lt;p&gt;With kube-proxy removed, Cilium's eBPF-based Service handling provides the forwarding mechanism for the Service.&lt;/p&gt;

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

&lt;p&gt;The request can therefore reach one of the NGINX Pods behind the Service.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Complete Path
&lt;/h3&gt;

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

&lt;p&gt;This single request connects the different networking mechanisms explored throughout the article.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The request starts as ordinary HTTP traffic on the network.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cilium L2 Announcement makes the LoadBalancer address reachable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cilium datapath brings the traffic into the cluster.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Envoy interprets the HTTP request and applies the Ingress routing configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The request then reaches a Kubernetes Service, where Cilium's eBPF Service datapath selects the backend workload.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Kubernetes networking becomes easier to understand when we stop looking at Pods, Services, LoadBalancers, and Ingress as isolated Kubernetes objects and instead follow the packet moving between them.&lt;/p&gt;

&lt;p&gt;In this lab, we started with basic Pod networking and Service connectivity, then progressively changed the networking datapath by introducing Cilium's kube-proxy replacement, LB-IPAM, L2 Announcement, and Envoy-based Ingress.&lt;/p&gt;

&lt;p&gt;Each component solves a different part of the networking problem:&lt;/p&gt;

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

&lt;p&gt;Together, these components form a layered networking architecture:&lt;/p&gt;

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

&lt;p&gt;This also gives us a more useful mental model of Cilium.&lt;/p&gt;

&lt;p&gt;Cilium is not simply the component that gives Pods IP addresses. It connects Kubernetes state with the Linux networking datapath and provides mechanisms for processing traffic at different layers. Some decisions can be handled in the kernel through eBPF, while application-layer traffic can be handled by Envoy in userspace.&lt;/p&gt;

&lt;p&gt;Ultimately, the most useful way to understand Kubernetes networking is to ask a simple question whenever traffic needs to move:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What component is responsible for making the next networking decision?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once that question can be answered, Kubernetes networking becomes less about memorizing individual components and more about understanding the datapath that connects them.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>kubernetes</category>
      <category>networking</category>
    </item>
    <item>
      <title>From Static Passwords to Dynamic Database Credentials with HashiCorp Vault</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Sat, 19 Sep 2026 04:24:44 +0000</pubDate>
      <link>https://dev.to/morizal/from-static-passwords-to-dynamic-database-credentials-with-hashicorp-vault-1lcd</link>
      <guid>https://dev.to/morizal/from-static-passwords-to-dynamic-database-credentials-with-hashicorp-vault-1lcd</guid>
      <description>&lt;p&gt;Almost every production app need credentials to access their database.&lt;/p&gt;

&lt;p&gt;A common approach is to create a database user with a password and provide that credential to the application through environment variable, configuration file, or secret management system.&lt;/p&gt;

&lt;p&gt;The application then uses the same credential until it is manually changed.&lt;/p&gt;

&lt;p&gt;This approach is simple, but the credential itself becomes a long lived security risk until someone rotates it.&lt;/p&gt;

&lt;p&gt;Though credential rotation can also become a new problem.&lt;/p&gt;

&lt;p&gt;A typical workflow might look like this:&lt;/p&gt;

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

&lt;p&gt;The problem is not only how the credential is stored.&lt;/p&gt;

&lt;p&gt;We also need to consider its entire lifecycle.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Who creates it?&lt;br&gt;&lt;br&gt;
How long should it remain valid?&lt;br&gt;&lt;br&gt;
How is it renewed while the application is running?&lt;br&gt;&lt;br&gt;
How do we replace it without interrupting the application?&lt;br&gt;&lt;br&gt;
What happens to the old credential after the replacement is ready?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We wanted to approach this differently.&lt;/p&gt;

&lt;p&gt;Instead of giving the application a permanent database password, we wanted the database credential to be temporary by design.&lt;/p&gt;

&lt;p&gt;This is where dynamic credentials become useful.&lt;/p&gt;

&lt;p&gt;With HashiCorp Vault, an application can request a database credential when it needs one. Vault generates a temporary database user and returns the credential together with a lease.&lt;/p&gt;

&lt;p&gt;The application can then use the credential while it is valid and renew its lease while the credential is still in use.&lt;/p&gt;

&lt;p&gt;When the application needs to replace the credential, it can request another one instead of modifying a permanent password.&lt;/p&gt;

&lt;p&gt;The workflow becomes:&lt;/p&gt;

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

&lt;p&gt;This changes credential management from a manually maintained configuration value into a lifecycle that can be managed by the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Goals
&lt;/h2&gt;

&lt;p&gt;We wanted an application whose database credentials are temporary by design, automatically renewed while in use, and replaced periodically without storing a static password.&lt;/p&gt;

&lt;p&gt;The application should be able to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Authenticate to Vault without using a Vault root token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Obtain database credentials dynamically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connect to Database using those credentials.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Renew the credential lease while the application is running.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;After three successful renewals, request a new credential.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Verify the new credential against Database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Switch to the new credential.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Revoke the old lease.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Continue serving requests without restarting the application.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The three renewal threshold is intentionally used as a simple rotation trigger for this project. It gives us a deterministic lifecycle that can be observed and tested without introducing a more complex rotation policy.&lt;/p&gt;

&lt;p&gt;The complete implementation will show how these pieces work together, from the initial authentication and credential request to lease renewal and the final zero downtime credential switch.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the full source code in the repository below.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/muhammadyulasfipahrizal/vault-setup.git" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/vault-setup.git&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;The project consists of three main components, each component has a specific responsibility in the credential lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vault
&lt;/h3&gt;

&lt;p&gt;HashiCorp Vault is responsible for managing the database credentials used by the application.&lt;/p&gt;

&lt;p&gt;Vault generates temporary credentials and manages the lease associated with each credential. The application communicates with Vault to authenticate, obtain credentials, and maintain the credential lease while it is running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application
&lt;/h3&gt;

&lt;p&gt;The application is a small Go API that consumes credentials provided by Vault.&lt;/p&gt;

&lt;p&gt;It authenticates to Vault, requests a dynamic PostgreSQL credential, and uses that credential to establish a connection to the database.&lt;/p&gt;

&lt;p&gt;The application also manages the runtime credential lifecycle. While the credential is being used, it renews the lease. When the rotation condition is reached, the application requests a new credential and verifies the new connection before switching away from the existing one.&lt;/p&gt;

&lt;p&gt;This allows the application to replace its database credential while continuing to run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is the database accessed by the application.&lt;/p&gt;

&lt;p&gt;The database is configured so that Vault can create temporary users for the application. These users are created with the permissions required by the application and are removed when their associated credentials are revoked.&lt;/p&gt;

&lt;p&gt;The application does not communicate with PostgreSQL through Vault. Instead, Vault provides the application with the credentials, and the application connects directly to PostgreSQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Component Interaction
&lt;/h3&gt;

&lt;p&gt;The interaction between the three components can be summarized as:&lt;/p&gt;

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

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

&lt;p&gt;This architecture keeps credential management separate from database access. Vault provides the temporary identity needed to access the database, while the application remains responsible for using that identity throughout its lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

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

&lt;p&gt;Each part of the repository has a specific responsibility.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;app/&lt;/code&gt; contains the Go application, including its source code, Go dependencies, Dockerfile, and Docker Compose configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;config/&lt;/code&gt; contains the Vault server configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;db/&lt;/code&gt; contains the PostgreSQL Docker Compose configuration and the SQL script used to initialize the database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; at the repository root defines the Vault service and connects it to the shared Docker network.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PostgreSQL
&lt;/h3&gt;

&lt;p&gt;The PostgreSQL service is defined in &lt;code&gt;db/docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The configuration uses PostgreSQL 17 and persists its data through a Docker volume. The initialization directory is mounted into PostgreSQL's &lt;code&gt;/docker-entrypoint-initdb.d&lt;/code&gt; directory so the database can be prepared when the container is initialized.&lt;/p&gt;

&lt;p&gt;The service is also attached to the external Docker network used by the other components, allowing Vault and the application to communicate with PostgreSQL through its container name.&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;postgres&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;postgres:17&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-postgres&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;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${YOUR_DB_NAME}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${YOUR_DB_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${YOUR_DB_PASSWORD}&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&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="s"&gt;postgres-data:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./postgres/init:/docker-entrypoint-initdb.d:ro&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${YOUR_DOCKER_NETWORK}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Database Initialization
&lt;/h3&gt;

&lt;p&gt;The initialization script is located at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;db/postgres/init/01-init.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its purpose is to create the administrative database user that Vault will use when managing dynamic PostgreSQL users.&lt;/p&gt;

&lt;p&gt;The script grants this user permission to connect to the target database and create objects in the &lt;code&gt;public&lt;/code&gt; schema.&lt;/p&gt;

&lt;p&gt;The repository contains the following initialization:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Vault
&lt;/h2&gt;

&lt;p&gt;Vault is defined in the root &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The service uses the official &lt;code&gt;hashicorp/vault&lt;/code&gt; image, exposes ports &lt;code&gt;8200&lt;/code&gt; and &lt;code&gt;8201&lt;/code&gt;, mounts the Vault data and configuration directories, and starts Vault using the repository's HCL configuration 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;vault&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;hashicorp/vault:latest&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault&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;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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8200:8200"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8201:8201"&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="s"&gt;./data:/vault/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config:/vault/config:ro&lt;/span&gt;
    &lt;span class="na"&gt;cap_add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;IPC_LOCK&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault server -config=/vault/config/vault.hcl&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;${YOUR_DOCKER_NETWORK}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Vault Configuration
&lt;/h3&gt;

&lt;p&gt;The Vault server configuration is stored under &lt;code&gt;config/&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;ui&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nx"&gt;disable_mlock&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nx"&gt;api_addr&lt;/span&gt;     &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://0.0.0.0:8200"&lt;/span&gt;
&lt;span class="nx"&gt;cluster_addr&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://0.0.0.0:8201"&lt;/span&gt;

&lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;address&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0:8200"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0:8201"&lt;/span&gt;
  &lt;span class="nx"&gt;tls_disable&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;storage&lt;/span&gt; &lt;span class="s2"&gt;"raft"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/vault/data"&lt;/span&gt;
  &lt;span class="nx"&gt;node_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vault-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration enables the Vault UI and uses Raft storage for Vault's persistent data. The TCP listener accepts connections on ports &lt;code&gt;8200&lt;/code&gt; and &lt;code&gt;8201&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable Database Secrets Engine
&lt;/h3&gt;

&lt;p&gt;The Database Secrets Engine allows Vault to generate database credentials.&lt;/p&gt;

&lt;p&gt;For this project, the database plugin is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql-database-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database connection is configured with the name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vault-postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vault connects to PostgreSQL through the Docker network using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql://{{username}}:{{password}}@vault-postgres:5432/appdb?sslmode=disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;{{username}}&lt;/code&gt; and &lt;code&gt;{{password}}&lt;/code&gt; values are provided by Vault's database plugin when it establishes the administrative connection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Database Role
&lt;/h3&gt;

&lt;p&gt;When the application requests &lt;code&gt;database/creds/app-role&lt;/code&gt;, Vault generates a new role and password.&lt;/p&gt;

&lt;p&gt;The role is configured as a dynamic role with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TTL:     1 hour
Max TTL: 1 day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the application requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database/creds/app-role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vault generates a new database username and password.&lt;/p&gt;

&lt;p&gt;The creation statements are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="nv"&gt;"{{name}}"&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt;
&lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'{{password}}'&lt;/span&gt;
&lt;span class="k"&gt;VALID&lt;/span&gt; &lt;span class="k"&gt;UNTIL&lt;/span&gt; &lt;span class="s1"&gt;'{{expiration}}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;YOUR_DATABASE_NAME&lt;/span&gt;
&lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="nv"&gt;"{{name}}"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vault replaces the template variables with generated values before executing the statements against PostgreSQL.&lt;/p&gt;

&lt;p&gt;The role also defines a revocation statement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="nv"&gt;"{{name}}"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what allows Vault to clean up the generated PostgreSQL user when the lease is revoked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Policy
&lt;/h3&gt;

&lt;p&gt;The application also needs permission to request them from Vault.&lt;/p&gt;

&lt;p&gt;We can create ACL policy named &lt;code&gt;app-policy&lt;/code&gt;:&lt;/p&gt;

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

&lt;p&gt;This policy deliberately exposes only the credential endpoint required by the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  AppRole Authentication
&lt;/h3&gt;

&lt;p&gt;The application should not authenticate to Vault using the root token.&lt;/p&gt;

&lt;p&gt;Instead, we enable the &lt;strong&gt;AppRole&lt;/strong&gt; authentication method.&lt;/p&gt;

&lt;p&gt;The configured authentication method is:&lt;/p&gt;

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

&lt;p&gt;The application receives two values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role ID
Secret ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These values are provided to the application through its runtime environment rather than embedding them directly. The application can therefore authenticate as a service identity without receiving the Vault root token.&lt;/p&gt;

&lt;h2&gt;
  
  
  App
&lt;/h2&gt;

&lt;p&gt;The application is defined in &lt;code&gt;app/docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The Compose configuration provides the Vault address, AppRole credentials, and PostgreSQL connection information through environment variables.&lt;/p&gt;

&lt;p&gt;The application communicates with Vault using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://vault:8200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and database with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;db-postgres:5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both services are connected through the same external Docker network.&lt;/p&gt;

&lt;p&gt;This keeps the application configuration independent from hard coded container IP addresses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application Lifecycle
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  Authenticate
&lt;/h3&gt;

&lt;p&gt;When the application starts, it creates a Vault client and authenticates using AppRole.&lt;/p&gt;

&lt;p&gt;The application sends its Role ID and Secret ID to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth/approle/login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vault validates the credentials and returns a Vault token.&lt;/p&gt;

&lt;p&gt;That token is then used for subsequent requests to the Vault API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request Dynamic Credentials
&lt;/h3&gt;

&lt;p&gt;After authentication, the application requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database/creds/app-role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vault creates a new Database role and returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;username&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;password&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;lease ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;lease duration&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application stores these values in its runtime state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect to Database
&lt;/h3&gt;

&lt;p&gt;The generated username and password are then used to create a database connection.&lt;/p&gt;

&lt;p&gt;The application verifies the credential by establishing the connection and performing a database ping.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lease Renewal
&lt;/h3&gt;

&lt;p&gt;The application obtains the lease duration directly from Vault when it requests the credentials.&lt;/p&gt;

&lt;p&gt;Instead of using a fixed renewal interval, the renewal loop waits for approximately half of the current lease duration before attempting a renewal.&lt;/p&gt;

&lt;p&gt;When the renewal succeeds, the application updates its lease information and increments its renewal counter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rotation
&lt;/h3&gt;

&lt;p&gt;For this project, rotation occurs after three successful lease renewals.&lt;/p&gt;

&lt;p&gt;The counter in this project is intentionally simple so the complete lifecycle can be observed during testing.&lt;/p&gt;

&lt;p&gt;After the third successful renewal:&lt;/p&gt;

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

&lt;p&gt;The application requests another credential from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database/creds/app-role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vault creates a second database user with a different username, password, and lease.&lt;/p&gt;

&lt;p&gt;At this point, two credentials can temporarily exist:&lt;/p&gt;

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

&lt;p&gt;The application does not immediately revoke the old credential.&lt;/p&gt;

&lt;p&gt;First, it verifies that the new credential actually works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero Downtime Credential Switch
&lt;/h3&gt;

&lt;p&gt;The application creates a new PostgreSQL connection using the newly generated credential.&lt;/p&gt;

&lt;p&gt;If the connection succeeds, the application switches its active database connection to the new one.&lt;/p&gt;

&lt;p&gt;Only after the new connection is verified does the application revoke the old lease.&lt;/p&gt;

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

&lt;p&gt;The old credential remains available until the replacement has been successfully verified. If the new credential cannot connect to database, the application keeps using the existing connection instead of disrupting the running application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observing the Rotation and Database Profile
&lt;/h3&gt;

&lt;p&gt;The application exposes its current lifecycle state through an API handler, allowing us to observe information such as the current credential identity, lease information, expiration, and renewal count.&lt;/p&gt;

&lt;p&gt;We can then compare that application state with the state visible from the Vault CLI.&lt;/p&gt;

&lt;p&gt;The observation focuses on two events:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Lease renewal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Credential rotation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Lease Renewal
&lt;/h3&gt;

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

&lt;p&gt;The application state should show that the lease is still associated with the same dynamic credential while the renewal count increases.&lt;/p&gt;

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

&lt;p&gt;This demonstrates the distinction between renewal and rotation: the lease is extended, but the database credential itself has not yet been replaced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Credential Rotation
&lt;/h3&gt;

&lt;p&gt;After the third successful renewal, the application requests a new credential.&lt;/p&gt;

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

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

&lt;p&gt;The final state should show that the application has successfully moved to the new credential while continuing to run.&lt;/p&gt;

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

&lt;p&gt;Managing database credentials is not only about keeping passwords secret, but also about managing their entire lifecycle.&lt;/p&gt;

&lt;p&gt;In this project, Vault provides temporary Database credentials through AppRole authentication, while the application handles lease renewal and credential rotation automatically.&lt;/p&gt;

&lt;p&gt;After three renewals, the application requests a new credential, verifies the connection, switches to it, and revokes the old lease. This allows credentials to be rotated without restarting the application or interrupting database access.&lt;/p&gt;

&lt;p&gt;The three renewal policy is only a demonstration. In production, the same pattern can be adapted to rotation intervals and security requirements.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The idea is to treat database credentials as temporary runtime rather than static configuration values.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the full source code in the repository below.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/vault-setup.git" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/vault-setup.git&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
    </item>
    <item>
      <title>Building Highly Available PostgreSQL Cluster With Patroni, ETCD, HAProxy, And Keepalived</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Sat, 12 Sep 2026 06:29:06 +0000</pubDate>
      <link>https://dev.to/morizal/building-highly-available-postgresql-cluster-with-patroni-etcd-haproxy-and-keepalived-4a8h</link>
      <guid>https://dev.to/morizal/building-highly-available-postgresql-cluster-with-patroni-etcd-haproxy-and-keepalived-4a8h</guid>
      <description>&lt;p&gt;A database is one of the most critical components in an application.&lt;/p&gt;

&lt;p&gt;When the database is running normally, the application can connect to it and continue serving requests. But when the database server becomes unavailable, the impact can quickly spread to the services that depend on it.&lt;/p&gt;

&lt;p&gt;For a system that needs to remain available, relying on a single database server creates an obvious risk.&lt;/p&gt;

&lt;p&gt;If that server fails, the application may lose its database connection until the server is recovered or another database server is manually brought into service.&lt;/p&gt;

&lt;p&gt;This raises an important question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happens when the database server that our application depends on suddenly fails?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A common approach is to run multiple PostgreSQL servers. However, having multiple servers does not automatically make the database highly available.&lt;/p&gt;

&lt;p&gt;A highly available database needs to continue serving applications even when part of the infrastructure fails. This means the system needs to handle failures without requiring the application to be manually reconfigured every time something goes wrong.&lt;/p&gt;

&lt;p&gt;In this article, we will build a highly available database cluster using PostgreSQL, Patroni, etcd, HAProxy, and Keepalived.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Need
&lt;/h2&gt;

&lt;p&gt;Before building the system, we first need to define what we expect from a highly available database.&lt;/p&gt;

&lt;p&gt;The goal is not simply to have multiple PostgreSQL servers running at the same time. The system should remain available and usable even when individual components fail.&lt;/p&gt;

&lt;p&gt;For this project, we define four main requirements:&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple PostgreSQL Nodes
&lt;/h3&gt;

&lt;p&gt;We need multiple PostgreSQL nodes so that the database service has another node available when one of the servers becomes unavailable.&lt;/p&gt;

&lt;p&gt;The nodes should maintain the same database state closely enough that one of them can take over when necessary.&lt;/p&gt;

&lt;p&gt;This gives us redundancy at the database layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic Failover
&lt;/h3&gt;

&lt;p&gt;When the current primary PostgreSQL nodes becomes unavailable, the cluster should be able to detect the failure and recover by promoting an eligible node without requiring to manually reconfigure the entire database cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stable Connection Endpoint
&lt;/h3&gt;

&lt;p&gt;We do not want application configuration to contain the address of a specific PostgreSQL server and require that address to be changed after every failover.&lt;/p&gt;

&lt;p&gt;Instead, the application should connect to a stable endpoint while the infrastructure handles which PostgreSQL node currently receives the connection.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Single Point of Failure
&lt;/h3&gt;

&lt;p&gt;If database traffic passes through another component, that component can become a new single point of failure.&lt;/p&gt;

&lt;p&gt;For example, having three PostgreSQL nodes does not provide end to end availability if all application traffic depends on one proxy server.&lt;/p&gt;

&lt;p&gt;The connection layer therefore also needs redundancy so that the failure of a single proxy does not make the database unreachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Replication Is Not Enough
&lt;/h2&gt;

&lt;p&gt;PostgreSQL provides replication mechanisms that allow data to be replicated from one instance to another.&lt;/p&gt;

&lt;p&gt;This gives us an important foundation for high availability.&lt;/p&gt;

&lt;p&gt;But having a replicated database does not automatically mean that the application can continue operating.&lt;/p&gt;

&lt;p&gt;Consider a simple setup:&lt;/p&gt;

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

&lt;p&gt;Under normal conditions, the application connects to the primary while the replica receives replicated data.&lt;/p&gt;

&lt;p&gt;Now imagine that the primary suddenly fails:&lt;/p&gt;

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

&lt;p&gt;The replica may still contain the database data, but it does not automatically solve the entire failure.&lt;/p&gt;

&lt;p&gt;We still need a mechanism to determine that the primary is no longer available and decide what should happen next.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Replica Is Not Automatically the New Primary
&lt;/h3&gt;

&lt;p&gt;Replication gives us another copy of the data, but the cluster still needs to coordinate the transition from the failed primary to another node.&lt;/p&gt;

&lt;p&gt;Without an automated mechanism, we need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Detect the failure&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Determine whether the primary is really unavailable&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Promote a suitable replica&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reconfigure clients to use the new primary&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Re establish replication for the remaining nodes&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The data may still be available, but the database service itself is not automatically recovered.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Connection Problem
&lt;/h3&gt;

&lt;p&gt;There is another problem after a failover.&lt;/p&gt;

&lt;p&gt;Suppose the application connects directly to the primary's address:&lt;/p&gt;

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

&lt;p&gt;After the primary fails and another node becomes primary:&lt;/p&gt;

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

&lt;p&gt;The new primary may be ready to accept writes activity, but the application is still trying to connect to the old address.&lt;/p&gt;

&lt;p&gt;This means that database failover and application connectivity are two different problems.&lt;/p&gt;

&lt;p&gt;We need the database cluster to recover and provide a consistent way for clients to reach whichever node is currently serving as primary.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Replication to High Availability
&lt;/h2&gt;

&lt;p&gt;This shows why a highly available PostgreSQL system requires more than PostgreSQL replication alone.&lt;/p&gt;

&lt;p&gt;We need additional components to coordinate the database nodes, manage the transition when a primary fails, and provide a reliable connection path for applications.&lt;/p&gt;

&lt;p&gt;The architecture therefore needs to solve three different concerns:&lt;/p&gt;

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

&lt;p&gt;Instead of treating the cluster as a collection of PostgreSQL servers, we separate it into two main layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database layer&lt;/strong&gt;, responsible for storing data and managing PostgreSQL availability&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connection layer&lt;/strong&gt;, responsible for providing a stable path from applications to the active PostgreSQL node&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture consists of five components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;PostgreSQL&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Patroni&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;etcd&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HAProxy&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keepalived&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each component has a specific responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  PostgreSQL
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is the database engine and the component that stores the application data.&lt;/p&gt;

&lt;p&gt;We will run PostgreSQL on three separate virtual machines:&lt;/p&gt;

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

&lt;p&gt;The three PostgreSQL instances form the database cluster.&lt;/p&gt;

&lt;p&gt;One node acts as the current primary, while the other nodes act as replicas.&lt;/p&gt;

&lt;p&gt;The primary handles write operations, while the replicas maintain copies of the database that can be used during failover.&lt;/p&gt;

&lt;h3&gt;
  
  
  Patroni
&lt;/h3&gt;

&lt;p&gt;Patroni runs alongside PostgreSQL on each database node and manages the PostgreSQL instances as a high availability cluster.&lt;/p&gt;

&lt;p&gt;Its responsibilities include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Monitoring PostgreSQL health&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Managing the PostgreSQL cluster state&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Coordinating leader election&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Promoting a replica when the primary fails&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Managing PostgreSQL configuration required for the HA setup&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  etcd
&lt;/h3&gt;

&lt;p&gt;etcd act as distributed system where cluster state can be stored and coordinated between the nodes.&lt;/p&gt;

&lt;p&gt;Each database VM runs an etcd member:&lt;/p&gt;

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

&lt;p&gt;The etcd members form a distributed consensus cluster.&lt;/p&gt;

&lt;p&gt;Patroni uses etcd to store and coordinate information about the PostgreSQL cluster, including which node currently holds the leader role.&lt;/p&gt;

&lt;h3&gt;
  
  
  HAProxy
&lt;/h3&gt;

&lt;p&gt;HAProxy monitors the PostgreSQL nodes and routes client connections to the node that is currently eligible to receive database traffic.&lt;/p&gt;

&lt;p&gt;This means the application does not need to know which PostgreSQL VM is currently the primary.&lt;/p&gt;

&lt;p&gt;In this project we will deploy two HAProxy instances:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Keepalived
&lt;/h3&gt;

&lt;p&gt;Using two HAProxy instances introduces another problem.&lt;/p&gt;

&lt;p&gt;If the application connects directly to one HAProxy server and that server fails, the PostgreSQL cluster may still be healthy but the application can no longer reach it.&lt;/p&gt;

&lt;p&gt;We therefore need a way to make the HAProxy layer highly available as well.&lt;/p&gt;

&lt;p&gt;Keepalived provides this through a Virtual IP (VIP).&lt;/p&gt;

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

&lt;p&gt;Under normal conditions, one HAProxy node owns the VIP and handles client connections.&lt;/p&gt;

&lt;p&gt;If that node fails, Keepalived can move the VIP to the other HAProxy node.&lt;/p&gt;

&lt;p&gt;The application therefore continues using the same database endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the Architecture Together
&lt;/h2&gt;

&lt;p&gt;With each component assigned to a specific responsibility, the complete architecture for this project looks like this:&lt;/p&gt;

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

&lt;p&gt;Each layer addresses a different failure scenario.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;If a PostgreSQL primary fails, Patroni manages the database failover.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the active HAProxy node fails, Keepalived moves the VIP to the other proxy.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the source code in the repository below.&lt;/p&gt;

&lt;p&gt;Github Repository: &lt;a href="https://github.com/muhammadyulasfipahrizal/postgres-ha.git" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/postgres-ha.git&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Building the PostgreSQL HA Cluster
&lt;/h2&gt;

&lt;p&gt;The database layer consists of three VM&lt;/p&gt;

&lt;p&gt;Each VM has its own PostgreSQL instance, its own Patroni process, and one member of the etcd cluster. The three nodes then work together to provide a PostgreSQL cluster with automatic failover.&lt;/p&gt;

&lt;h3&gt;
  
  
  PostgreSQL Nodes
&lt;/h3&gt;

&lt;p&gt;At any given time, one node is the primary and the other nodes are replicas.&lt;/p&gt;

&lt;p&gt;The primary handles write operations, while the replicas continuously receive changes from the primary.&lt;/p&gt;

&lt;p&gt;For example, the initial state might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VM1 -&amp;gt; Primary
VM2 -&amp;gt; Replica
VM3 -&amp;gt; Replica
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a failure, the cluster may transition to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VM1 -&amp;gt; Failed
VM2 -&amp;gt; Primary
VM3 -&amp;gt; Replica
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The node roles are therefore part of the cluster's dynamic state rather than something we manually configure once and leave unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  PostgreSQL Replication
&lt;/h3&gt;

&lt;p&gt;The PostgreSQL instances use streaming replication to maintain copies of the database across the nodes.&lt;/p&gt;

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

&lt;p&gt;This gives the cluster multiple copies of the database while allowing the primary to continue serving write traffic.&lt;/p&gt;

&lt;p&gt;The replication settings are defined through Patroni's configuration. For 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="na"&gt;postgresql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:5432&lt;/span&gt;
  &lt;span class="na"&gt;connect_address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${SERVER_IP}:5432&lt;/span&gt;

  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;hba_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/patroni/pg_hba.conf&lt;/span&gt;
    &lt;span class="na"&gt;unix_socket_directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/var/run/postgresql'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cluster is also configured with parameters required for streaming replication:&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;postgresql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;wal_level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replica&lt;/span&gt;
    &lt;span class="na"&gt;hot_standby&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on"&lt;/span&gt;
    &lt;span class="na"&gt;wal_keep_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;256MB&lt;/span&gt;
    &lt;span class="na"&gt;max_wal_senders&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;max_replication_slots&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These settings are part of the Patroni configuration rather than being managed independently on each PostgreSQL instance.&lt;/p&gt;

&lt;h3&gt;
  
  
  etcd Cluster
&lt;/h3&gt;

&lt;p&gt;Alongside PostgreSQL, each database VM runs an etcd member.&lt;/p&gt;

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

&lt;p&gt;The three etcd members form a distributed consensus cluster.&lt;/p&gt;

&lt;p&gt;Using three members is important because the cluster can tolerate the loss of one member while still maintaining a quorum.&lt;/p&gt;

&lt;p&gt;Patroni uses this cluster to coordinate the state of the PostgreSQL nodes.&lt;/p&gt;

&lt;p&gt;The etcd endpoints are configured in &lt;code&gt;patroni.yml&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="na"&gt;etcd3&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;192.168.122.1:2379&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;192.168.122.2:2379&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;192.168.122.3:2379&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Patroni also defines the cluster scope and namespace used to store its distributed state:&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;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-ha&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;/service/&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;postgres1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;name&lt;/code&gt; value is node specific, while the cluster &lt;code&gt;scope&lt;/code&gt; and namespace identify the Patroni cluster. On the other nodes, the node name and server address change accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Patroni on Each Node
&lt;/h3&gt;

&lt;p&gt;Each PostgreSQL node also runs a Patroni instance. Patroni connects the PostgreSQL instances with the distributed cluster state provided by etcd.&lt;/p&gt;

&lt;p&gt;Each Patroni instance monitors its local PostgreSQL server and participates in managing the cluster.&lt;/p&gt;

&lt;p&gt;An important part of the configuration is the timing used for leader management:&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;bootstrap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;dcs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ttl&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;loop_wait&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;retry_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These values control how Patroni interacts with the distributed configuration store and how frequently it checks the cluster state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cluster Configuration
&lt;/h3&gt;

&lt;p&gt;The repository keeps the database configuration inside the db-server folder.&lt;/p&gt;

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

&lt;p&gt;The Docker Compose configuration mounts the PostgreSQL data directory and the Patroni configuration into the database container:&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;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&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;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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8008:8008"&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="s"&gt;./data:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config/patroni.yml:/etc/patroni/patroni.yml:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config/pg_hba.conf:/etc/patroni/pg_hba.conf:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the database configuration version controlled while allowing the PostgreSQL data itself to remain in its persistent volume.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting the Cluster
&lt;/h3&gt;

&lt;p&gt;Once the three database nodes are configured, we can start the services across the VMs. We can verify the resulting cluster state through Patroni before moving to the next layer.&lt;/p&gt;

&lt;p&gt;The cluster should expose a state similar to:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The HAProxy Layer
&lt;/h2&gt;

&lt;p&gt;The PostgreSQL cluster can now handle database level failover, but there is still a connection problem.&lt;/p&gt;

&lt;p&gt;HAProxy receives PostgreSQL connections on port &lt;code&gt;5432&lt;/code&gt; and forwards them to the PostgreSQL node that is currently serving as the primary.&lt;/p&gt;

&lt;p&gt;This allows the application to use a single database endpoint while the cluster manages which PostgreSQL node is active.&lt;/p&gt;

&lt;h3&gt;
  
  
  HAProxy in TCP Mode
&lt;/h3&gt;

&lt;p&gt;PostgreSQL uses its own protocol, so HAProxy is configured in TCP mode rather than HTTP mode.&lt;/p&gt;

&lt;p&gt;The frontend listens for incoming PostgreSQL connections:&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="s"&gt;frontend postgres&lt;/span&gt;
    &lt;span class="s"&gt;bind *:5432&lt;/span&gt;

    &lt;span class="s"&gt;default_backend postgres_primary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application therefore only needs to connect to the HAProxy endpoint using the normal PostgreSQL port.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detecting the Current Primary
&lt;/h3&gt;

&lt;p&gt;We cannot simply configure HAProxy to always send traffic to one server because that server may become unavailable.&lt;/p&gt;

&lt;p&gt;Instead, HAProxy uses Patroni API to check the role of each PostgreSQL node.&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="s"&gt;backend postgres_primary&lt;/span&gt;
    &lt;span class="s"&gt;mode tcp&lt;/span&gt;

    &lt;span class="s"&gt;option httpchk GET /primary&lt;/span&gt;
    &lt;span class="s"&gt;http-check expect status &lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;

    &lt;span class="s"&gt;server postgres1 192.168.122.1:5432 check port &lt;/span&gt;&lt;span class="m"&gt;8008&lt;/span&gt;
    &lt;span class="s"&gt;server postgres2 192.168.122.2:5432 check port &lt;/span&gt;&lt;span class="m"&gt;8008&lt;/span&gt;
    &lt;span class="s"&gt;server postgres3 192.168.122.3:5432 check port &lt;/span&gt;&lt;span class="m"&gt;8008&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTTP request is only used as a health check to ask Patroni whether the node is currently the primary.&lt;/p&gt;

&lt;p&gt;Patroni &lt;code&gt;/primary&lt;/code&gt; endpoint returns HTTP &lt;code&gt;200&lt;/code&gt; when the node is the current primary. HAProxy uses that response to determine which backend server should receive write traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Routing Flow
&lt;/h3&gt;

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

&lt;p&gt;HAProxy periodically checks the Patroni endpoint on each database node.&lt;/p&gt;

&lt;p&gt;Only the node reporting itself as the current primary passes the &lt;code&gt;/primary&lt;/code&gt; health check.&lt;/p&gt;

&lt;p&gt;The result is that HAProxy can automatically direct new connections to the current primary without requiring configuration change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Keepalived
&lt;/h2&gt;

&lt;p&gt;Keepalived uses VRRP (Virtual Router Redundancy Protocol) to manage VIP (virtual ip) between the two proxy servers.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

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

&lt;p&gt;Under normal conditions, HAProxy 1 owns the VIP.&lt;/p&gt;

&lt;p&gt;The application therefore connects to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;192.168.122.200:5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;192.168.122.10:5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Choosing the Active Proxy
&lt;/h3&gt;

&lt;p&gt;Keepalived assigns a priority to each node to determine which node should normally own the VIP.&lt;/p&gt;

&lt;p&gt;The node with the higher priority becomes the active VRRP node and owns the VIP. If the active node becomes unavailable, the other node can take ownership of the VIP.&lt;/p&gt;

&lt;p&gt;This allows the connection endpoint to remain unchanged even though the physical server handling the traffic changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring HAProxy
&lt;/h3&gt;

&lt;p&gt;Simply checking whether the server itself is reachable is not enough.The proxy node could still be running while HAProxy itself has stopped.&lt;/p&gt;

&lt;p&gt;In this situation, Keepalived should not continue advertising the VIP from that node.&lt;/p&gt;

&lt;p&gt;The project therefore uses a health check script:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;if &lt;/span&gt;docker ps &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Names}}'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qx&lt;/span&gt; haproxy&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script checks whether the HAProxy container is running and returns a success or failure status accordingly.&lt;/p&gt;

&lt;p&gt;Keepalived then tracks this script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;vrrp_script&lt;/span&gt; &lt;span class="n"&gt;check_haproxy&lt;/span&gt; {
    &lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="s2"&gt;"/etc/keepalived/check_haproxy.sh"&lt;/span&gt;
    &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;fall&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;rise&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;weight&lt;/span&gt; -&lt;span class="m"&gt;50&lt;/span&gt;
}

&lt;span class="n"&gt;vrrp_instance&lt;/span&gt; &lt;span class="n"&gt;VI_1&lt;/span&gt; {
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="n"&gt;MASTER&lt;/span&gt;
    &lt;span class="n"&gt;interface&lt;/span&gt; &lt;span class="n"&gt;ens3&lt;/span&gt;

    &lt;span class="n"&gt;virtual_router_id&lt;/span&gt; &lt;span class="m"&gt;51&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="m"&gt;110&lt;/span&gt;
    &lt;span class="n"&gt;advert_int&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

    &lt;span class="n"&gt;virtual_ipaddress&lt;/span&gt; {
        &lt;span class="m"&gt;192&lt;/span&gt;.&lt;span class="m"&gt;168&lt;/span&gt;.&lt;span class="m"&gt;122&lt;/span&gt;.&lt;span class="m"&gt;200&lt;/span&gt;/&lt;span class="m"&gt;24&lt;/span&gt;
    }

    &lt;span class="n"&gt;track_script&lt;/span&gt; {
        &lt;span class="n"&gt;check_haproxy&lt;/span&gt;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the HAProxy check fails repeatedly, the configured negative weight reduces the node's VRRP priority.&lt;/p&gt;

&lt;p&gt;This allows the other Keepalived node to become the owner of the VIP.&lt;/p&gt;

&lt;h3&gt;
  
  
  HAProxy Failure
&lt;/h3&gt;

&lt;p&gt;Suppose the initial state is:&lt;/p&gt;

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

&lt;p&gt;When HAProxy 1 fails, Keepalived detects the HAProxy failure through the tracking script.&lt;/p&gt;

&lt;p&gt;The VIP can then move to HAProxy 2:&lt;/p&gt;

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

&lt;p&gt;The application does not need to change its database configuration, It continues connecting to the same address&lt;/p&gt;

&lt;p&gt;The physical proxy handling the connection has changed, but the application's endpoint has not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Setup
&lt;/h2&gt;

&lt;p&gt;With the complete architecture connected, we can now test whether the system actually behaves as expected.&lt;/p&gt;

&lt;p&gt;We will test five scenarios:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Normal cluster state&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PostgreSQL primary failure&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HAProxy failure&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;VIP failover&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Normal Cluster
&lt;/h3&gt;

&lt;p&gt;Before introducing any failures, we first verify that all components are operating normally.&lt;/p&gt;

&lt;p&gt;We can check the Patroni cluster state with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;patronictl &lt;span class="nt"&gt;-c&lt;/span&gt; /etc/patroni/patroni.yml list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We should see one node identified as the &lt;code&gt;Leader&lt;/code&gt; and the remaining nodes as replicas, in my setup VM2 is currently the &lt;code&gt;Leader&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;We can also verify that HAProxy recognizes the current primary through its statistics page or by checking the backend state.&lt;/p&gt;

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

&lt;p&gt;Finally, we verify that the VIP is currently owned by the active HAProxy node.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip addr show &amp;lt;network-interface&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected result is that the VIP is present on the active proxy.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  PostgreSQL Failure
&lt;/h3&gt;

&lt;p&gt;Next, we intentionally stop the current PostgreSQL primary.&lt;/p&gt;

&lt;p&gt;For example, if VM2 is currently the primary, we can stop the PostgreSQL container in VM2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop &amp;lt;postgres-container&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the failure, VM2 should no longer be available as the primary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before:
VM1 -&amp;gt; REPLICA
VM2 -&amp;gt; PRIMARY
VM3 -&amp;gt; REPLICA


After:
VM1 -&amp;gt; PRIMARY
VM3 -&amp;gt; REPLICA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then check the Patroni cluster again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;patronictl &lt;span class="nt"&gt;-c&lt;/span&gt; /etc/patroni/patroni.yml list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the cluster should detect that the previous primary is unavailable and begin the leader transition.&lt;/p&gt;

&lt;p&gt;Patroni should promote an eligible replica to become the new primary, here VM1 becomes the new primary.&lt;/p&gt;

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

&lt;p&gt;We can also verify the Patroni REST API directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://192.168.122.28:8008/primary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful primary check should return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 200 OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This confirms that the database layer has completed the failover.&lt;/p&gt;

&lt;h3&gt;
  
  
  HAProxy Detects the New Primary
&lt;/h3&gt;

&lt;p&gt;Once Patroni has promoted the new primary, HAProxy should discover the role change through its health checks.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  HAProxy Failure
&lt;/h3&gt;

&lt;p&gt;First, we identify which HAProxy node currently owns the VIP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip addr show &amp;lt;network-interface&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my setup VM4 currently owns the VIP:&lt;/p&gt;

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

&lt;p&gt;We then stop HAProxy on VM4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop haproxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the current Keepalived health check monitors the HAProxy container, the check should fail after the configured number of failed checks.&lt;/p&gt;

&lt;p&gt;Keepalived should then reduce the active node's VRRP priority and allow the other proxy to take ownership of the VIP.&lt;/p&gt;

&lt;h3&gt;
  
  
  VIP Failover
&lt;/h3&gt;

&lt;p&gt;After HAProxy on VM4 fails, the VIP is now on VM5.&lt;/p&gt;

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

&lt;p&gt;The application still uses the same endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;VIP&amp;gt;:5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This demonstrates that the proxy layer can fail independently from the PostgreSQL layer.&lt;/p&gt;

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

&lt;p&gt;Building a highly available PostgreSQL environment is not about simply running multiple database servers. It is about designing the system so that individual failures do not automatically become application outages.&lt;/p&gt;

&lt;p&gt;In the setup, we combined several components, each responsible for a different part of the availability problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;PostgreSQL provides the database layer and streaming replication&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Patroni manages cluster membership, leader election, and automatic failover&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;etcd provides the distributed state required by Patroni&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HAProxy provides a stable database endpoint and routes connections to the current primary&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keepalived provides a virtual IP so that the HAProxy layer does not depend on a single server&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important lesson is that high availability is not achieved by adding redundancy alone. Every component needs to have a clear role in detecting failures, making decisions, and redirecting traffic when something goes wrong.&lt;/p&gt;




&lt;p&gt;This project is intentionally built as a lab environment, but the concepts can be applied to more complex production deployments.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can reproduce the entire setup and experiment with the failure scenarios yourself by cloning my GitHub repository below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/postgres-ha.git" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/postgres-ha.git&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
    </item>
    <item>
      <title>How I Built End To End Observability Stack with OpenTelemetry, Prometheus, Grafana, Loki, and Jaeger</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Sat, 05 Sep 2026 05:00:39 +0000</pubDate>
      <link>https://dev.to/morizal/how-i-built-end-2-end-observability-stack-with-opentelemetry-prometheus-grafana-loki-and-jaeger-500n</link>
      <guid>https://dev.to/morizal/how-i-built-end-2-end-observability-stack-with-opentelemetry-prometheus-grafana-loki-and-jaeger-500n</guid>
      <description>&lt;p&gt;When an application is running, knowing that it is up does not necessarily mean knowing what it is doing.&lt;/p&gt;

&lt;p&gt;For example, imagine an API request that takes several seconds to complete. We would want to answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Is the application receiving more requests than usual?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What happened during the request?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How long does the request take?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Did the application encounter an error?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Which part of the request should we investigate?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of looking at behavior through a single source, we should collect different types of signal and make them available in one observability stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Need to Observe
&lt;/h2&gt;

&lt;p&gt;To understand the application from different perspectives, we need three types of telemetry: metrics, logs, and traces.&lt;/p&gt;

&lt;p&gt;Each signal provides a different view of the application, and each becomes useful at a different stage of an investigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metrics
&lt;/h3&gt;

&lt;p&gt;Metrics helps us to understand the overall behavior of the application.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How many requests are being processed?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Are request rates increasing?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Are errors becoming more frequent?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is the application behaving differently from normal?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives us the high level view we need to detect if something might be wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logs
&lt;/h3&gt;

&lt;p&gt;Logs give us details about individual events inside the application, such as a request starting, an operation completing, or an error occurring. They become especially useful when we need to understand the context behind an abnormal metric.&lt;/p&gt;

&lt;h3&gt;
  
  
  Traces
&lt;/h3&gt;

&lt;p&gt;A trace allows us to follow a request through its execution and understand its timing. This becomes particularly useful when investigating slow requests or determining which part of an operation deserves further attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Centralize Collection
&lt;/h3&gt;

&lt;p&gt;Instead of making the application communicate independently with several observability backends, we can place a telemetry collection layer between the application.&lt;/p&gt;

&lt;p&gt;This provide a consistent way to receive telemetry from the application and route it to the systems responsible for storing and analyzing each signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visualization
&lt;/h3&gt;

&lt;p&gt;Jumping between different interfaces makes investigation slower, especially when we need to correlate information from multiple signals.&lt;/p&gt;

&lt;p&gt;Therefore we need a visualization layer where metrics, logs, and traces can be explored together.&lt;/p&gt;

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

&lt;p&gt;Now that we know what we need to collect, the next question is how these signals should move through the system.&lt;/p&gt;

&lt;p&gt;The architecture looks like this:&lt;/p&gt;

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

&lt;p&gt;The application is the source of the telemetry. When the application generates metrics, logs, and traces, these signals are sent to the OpenTelemetry Collector using OTLP.&lt;/p&gt;

&lt;p&gt;The Collector then routes each signal to the appropriate backend.&lt;/p&gt;

&lt;p&gt;Metrics are exposed for Prometheus to collect, logs are sent to Loki, and traces are forwarded to Jaeger. Grafana sits on top of these systems and gives us a single place to explore the data.&lt;/p&gt;

&lt;p&gt;This creates a clear separation between the application and the systems that store and analyze its telemetry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application to Collector
&lt;/h3&gt;

&lt;p&gt;Consider a request entering the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application processes the request and generates telemetry while doing so.&lt;/p&gt;

&lt;p&gt;Instead of sending that telemetry directly to multiple backends, the application sends it to the Collector The application therefore only needs to know where the telemetry collection layer is located.&lt;/p&gt;

&lt;p&gt;This becomes especially useful as the observability stack grows. Additional processing or exporters can be introduced at the Collector without requiring the application to establish a new connection to every backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collector to Backends
&lt;/h3&gt;

&lt;p&gt;Once telemetry reaches the Collector, the signals follow different paths depending on their type.&lt;/p&gt;

&lt;p&gt;Each backend therefore has a specific responsibility.&lt;/p&gt;

&lt;p&gt;Prometheus handles the metrics data, Loki handles application logs, and Jaeger handles traces.&lt;/p&gt;

&lt;h3&gt;
  
  
  From Backends to Grafana
&lt;/h3&gt;

&lt;p&gt;Once the telemetry reaches its respective backends, we need a way to explore and make sense of the data.&lt;/p&gt;

&lt;p&gt;Grafana provides this layer by connecting to Prometheus, Loki, and Jaeger, giving us a single interface to query and visualize the telemetry generated by our application.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Application
&lt;/h2&gt;

&lt;p&gt;Before sending any telemetry, we need an application that can generate it.&lt;/p&gt;

&lt;p&gt;For this project, we use a small Go API as the application being observed. The API exposes several endpoints that represent different application behaviors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /health
GET /api/orders
GET /api/transaction
GET /api/users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each endpoint gives us a different scenario to observe.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/health&lt;/code&gt; represents a simple successful request&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/api/orders&lt;/code&gt; returns application data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/api/transaction&lt;/code&gt; simulates a slow operation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/api/users&lt;/code&gt; intentionally returns an error&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives us enough variety to see how metrics, logs, and traces behave when we test the observability stack later.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the source code in the repository below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/E2E-observability-stack.git" rel="noopener noreferrer"&gt;Github Repository&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Initializing Opentelemetry
&lt;/h3&gt;

&lt;p&gt;The telemetry setup is separated from the HTTP handlers in &lt;code&gt;telemetry.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The application first defines its service identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;semconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"observability-api"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the telemetry a consistent service name: &lt;code&gt;observability-api&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;More importantly, each telemetry provider is configured to send its data to the OpenTelemetry Collector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;otlptracegrpc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"otel-collector:4317"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same Collector endpoint is used for metrics and logs.&lt;/p&gt;

&lt;p&gt;This means the application does not need to know where Prometheus, Loki, or Jaeger are running. It only needs to know the address of the Collector.&lt;/p&gt;

&lt;p&gt;The trace, metric, and log providers are then registered globally in the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Instrumenting HTTP Requests
&lt;/h3&gt;

&lt;p&gt;The next step is making the HTTP server itself observable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;health&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/transaction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of passing this multiplexer directly to the HTTP server, we wrap it with OpenTelemetry's HTTP instrumentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;otelhttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"HTTP Server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting handler is then used by the HTTP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Addr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This instrumentation sits in front of all four endpoints:&lt;/p&gt;

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

&lt;p&gt;As requests pass through this layer, OpenTelemetry can capture HTTP request telemetry and associate it with the request context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logging Inside Each Endpoint
&lt;/h3&gt;

&lt;p&gt;For application specific events, we use a small helper called &lt;code&gt;emitLog&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The helper receives the request context, severity, message, and additional attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;emitLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Severity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KeyValue&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="o"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each handler passes &lt;code&gt;r.Context()&lt;/code&gt; into &lt;code&gt;emitLog&lt;/code&gt;, so the log is emitted using the context of the current HTTP request.&lt;/p&gt;

&lt;p&gt;The endpoint therefore does not need to know how the log will eventually reach Loki. It only creates the log event through the OpenTelemetry logging API.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/health&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The simplest example is the health endpoint.&lt;/p&gt;

&lt;p&gt;When we call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;health&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the handler generates an informational log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;emitLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeverityInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"health check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application therefore produces telemetry as part of processing the request rather than requiring a separate logging pipeline inside the handler.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/api/orders&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The orders endpoint follows the same pattern, but adds more context to the log.&lt;/p&gt;

&lt;p&gt;After creating the orders, the handler emits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;emitLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeverityInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"orders fetched successfully"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/api/orders"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order_count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the telemetry contains not only the event itself, but also structured attributes such as the endpoint and number of orders.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;fetched&lt;/span&gt; &lt;span class="n"&gt;successfully&lt;/span&gt;
&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="n"&gt;order_count&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These attributes become useful later when we query the logs with Loki.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;/api/transaction&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The transaction endpoint gives us a more interesting example because it intentionally takes three seconds to complete.&lt;/p&gt;

&lt;p&gt;First, the handler records the start of the operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;emitLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeverityInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"transaction started"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/api/transaction"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the simulated operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the handler calculates the duration and emits another log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;emitLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeverityInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"transaction completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/api/transaction"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Milliseconds&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;
  
  
  &lt;code&gt;/api/users&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Finally, we have an endpoint that intentionally returns an error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;emitLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SeverityError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"failed to fetch users"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/api/users"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status_code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusInternalServerError&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request therefore produces an error-level log before returning HTTP 500.&lt;/p&gt;

&lt;p&gt;This gives us a different scenario from &lt;code&gt;/api/transaction&lt;/code&gt;: instead of investigating latency, we can later use the observability stack to investigate an application error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Observability Pipeline
&lt;/h2&gt;

&lt;p&gt;With the application ready, we can now configure how that telemetry is processed and delivered to each backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring the OpenTelemetry Collector
&lt;/h3&gt;

&lt;p&gt;The Collector accepts OTLP through gRPC:&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;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;protocols&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;grpc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4317&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Collector then uses the batch processor:&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;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;batch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than defining one large pipeline for every signal, the configuration separates metrics, logs, and traces:&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;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;otlp&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;batch&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;prometheus&lt;/span&gt;

    &lt;span class="na"&gt;traces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;otlp&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;batch&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;otlp/jaeger&lt;/span&gt;

    &lt;span class="na"&gt;logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;otlp&lt;/span&gt;
      &lt;span class="na"&gt;processors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;batch&lt;/span&gt;
      &lt;span class="na"&gt;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;otlphttp/loki&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Collector to Prometheus
&lt;/h3&gt;

&lt;p&gt;Metrics use the Prometheus exporter:&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;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prometheus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;endpoint&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.0.0.0:8889"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Collector exposes the metrics endpoint on port &lt;code&gt;8889&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is slightly different from the way telemetry enters the Collector.&lt;/p&gt;

&lt;p&gt;The application sends telemetry to the Collector, but Prometheus does not receive metrics through a push connection. Instead, Prometheus periodically scrapes the endpoint exposed by the Collector.&lt;/p&gt;

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

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

&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_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;otel-collector"&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;otel-collector:8889"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prometheus therefore scrapes the Collector every five seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collector to Loki
&lt;/h3&gt;

&lt;p&gt;Logs use the &lt;code&gt;otlphttp/loki&lt;/code&gt; exporter:&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;otlphttp/loki&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://loki:3100/otlp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Collector sends the processed logs to Loki through its OTLP HTTP endpoint.&lt;/p&gt;

&lt;p&gt;This works particularly well with the structured information we added to the application logs.&lt;/p&gt;

&lt;p&gt;For example, the &lt;code&gt;/api/orders&lt;/code&gt; endpoint produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;message:     orders fetched successfully
endpoint:    /api/orders
order_count: 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of treating the log as only a text message, these additional attributes provide context that can be queried later.&lt;/p&gt;

&lt;p&gt;Loki is configured to support structured metadata:&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;limits_config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;allow_structured_metadata&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;volume_enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project also uses filesystem storage for Loki, backed by the &lt;code&gt;loki-data&lt;/code&gt; Docker volume.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collector to Jaeger
&lt;/h3&gt;

&lt;p&gt;Traces use a separate OTLP exporter:&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;otlp/jaeger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jaeger:4317&lt;/span&gt;
  &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The traces pipeline connects this exporter to the OTLP receiver and batch processor.&lt;/p&gt;

&lt;p&gt;Jaeger is configured to receive OTLP through gRPC:&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;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;otlp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;protocols&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;grpc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.0.0.0:4317&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this project Jaeger uses an in memory backend:&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;extensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jaeger_query&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="na"&gt;traces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;memory&lt;/span&gt;

  &lt;span class="na"&gt;jaeger_storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;backends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;max_traces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trace exporter writes into that memory storage:&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;exporters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jaeger_storage_exporter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;trace_storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;memory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running the Pipeline
&lt;/h3&gt;

&lt;p&gt;All of these components are defined as separate Docker Compose services.&lt;/p&gt;

&lt;p&gt;The Collector mounts our configuration file into the container and exposes its OTLP and Prometheus endpoints:&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;otel-collector&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;otel/opentelemetry-collector-contrib: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;--config=/etc/otelcol-contrib/otel-collector.yml"&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="s"&gt;./otel/otel-collector.yml:/etc/otelcol-contrib/otel-collector.yml:ro&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4317:4317"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8889:8889"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The services are connected through the &lt;code&gt;research-observability&lt;/code&gt; Docker network, allowing the Collector to reach services such as &lt;code&gt;loki&lt;/code&gt; and &lt;code&gt;jaeger&lt;/code&gt; directly by their Docker service names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bringing Everything Together with Grafana
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;grafana&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;grafana/grafana:latest&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;grafana&lt;/span&gt;
  &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SECURITY_ADMIN_USER=admin&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_SECURITY_ADMIN_PASSWORD=admin&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;GF_AUTH_ANONYMOUS_ENABLED=false&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="s"&gt;grafana-data:/var/lib/grafana&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3001:3000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container exposes Grafana on port &lt;code&gt;3001&lt;/code&gt; of the host, while Grafana itself listens on port &lt;code&gt;3000&lt;/code&gt; inside the container.&lt;/p&gt;

&lt;p&gt;Because Grafana, Prometheus, Loki, and Jaeger are connected to the same &lt;code&gt;research-observability&lt;/code&gt; Docker network, Grafana can communicate with the other services using their Docker service names.&lt;/p&gt;

&lt;p&gt;This means we can configure the data sources using addresses such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://prometheus:9090
http://loki:3100
http://jaeger:16686
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adding Prometheus as a Data Source
&lt;/h3&gt;

&lt;p&gt;From the Grafana dashboard, open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connections → Data sources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then select &lt;strong&gt;Add data source&lt;/strong&gt; and choose &lt;strong&gt;Prometheus&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the server URL, we use the Docker service name instead of &lt;code&gt;localhost&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://prometheus:9090
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;After entering the URL, select &lt;strong&gt;Save &amp;amp; test&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the connection is successful, Grafana confirms that it can communicate with Prometheus.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Loki
&lt;/h3&gt;

&lt;p&gt;We repeat the same process for Loki.&lt;/p&gt;

&lt;p&gt;Go to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connections → Data sources → Add data source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then select &lt;strong&gt;Loki&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the URL, use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://loki:3100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After entering the URL, select &lt;strong&gt;Save &amp;amp; test&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once Grafana confirms the connection, Loki is available as a data source for exploring application logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Jaeger
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connections → Data sources → Add data source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;select &lt;strong&gt;Jaeger&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the URL, use Jaeger's query endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://jaeger:16686
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is different from the OTLP endpoint used by the Collector.&lt;/p&gt;

&lt;p&gt;The Collector sends traces to Jaeger through its OTLP gRPC endpoint, while Grafana connects to Jaeger's query interface to retrieve traces.&lt;/p&gt;

&lt;p&gt;After entering the URL, select &lt;strong&gt;Save &amp;amp; test&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At this point, Grafana has access to all three observability backends.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying the Data Sources
&lt;/h3&gt;

&lt;p&gt;Before building any dashboard, we can verify that Grafana can actually query the data.&lt;/p&gt;

&lt;p&gt;Open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the data source selector, we can choose between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prometheus
Loki
Jaeger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, selecting Prometheus allows us to run a metrics query directly from Grafana. This gives us a simple way to verify each connection before creating dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Investigation
&lt;/h2&gt;

&lt;p&gt;Now that Grafana is connected to our observability backends, we can use the stack to investigate actual application problems.&lt;/p&gt;

&lt;p&gt;Instead of simply checking whether telemetry is being collected, we want to answer a more practical question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What happens when something goes wrong?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this example, we will investigate &lt;code&gt;/api/transaction&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A Slow Transaction
&lt;/h3&gt;

&lt;p&gt;As mention before our &lt;code&gt;/api/transaction&lt;/code&gt; endpoint intentionally waits for three seconds before returning a response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the users perspective, the problem is simple: the API feels slow.&lt;/p&gt;

&lt;p&gt;However, knowing that a request is slow does not immediately tell us why.&lt;/p&gt;

&lt;p&gt;We start by sending a request to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/transaction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After generating several requests, we can open our Grafana dashboard and look at the request duration.&lt;/p&gt;

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

&lt;p&gt;The metric tells us that the request is taking significantly longer than a normal API request.&lt;/p&gt;

&lt;p&gt;But metrics alone do not provide much context about what happened inside the request.&lt;/p&gt;

&lt;p&gt;This is where we move to the logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Looking at the Logs
&lt;/h3&gt;

&lt;p&gt;In Grafana, we can open the Loki data source and search for logs generated by the transaction endpoint.&lt;/p&gt;

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

&lt;p&gt;The application produces two important events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transaction started
transaction completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The completion log also contains the duration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;message:     transaction completed
endpoint:    /api/transaction
duration_ms: 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we know that the request really did spend approximately three seconds processing the transaction.&lt;/p&gt;

&lt;p&gt;The logs give us more context than the metric alone, but we still do not have a detailed view of the request execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Following the Trace
&lt;/h3&gt;

&lt;p&gt;The same request also generates a trace that is exported through the OpenTelemetry Collector and stored in Jaeger.&lt;/p&gt;

&lt;p&gt;From Grafana, we can open the corresponding trace and inspect its timeline.&lt;/p&gt;

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

&lt;p&gt;The trace gives us a view of how long the request took and where that time was spent.&lt;/p&gt;

&lt;h3&gt;
  
  
  From Symptom to Cause
&lt;/h3&gt;

&lt;p&gt;This scenario demonstrate the main reason we built the observability stack.&lt;/p&gt;

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

&lt;p&gt;Each signal gives us a different level of information.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Metrics help us detect that something unusual is happening.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Logs give us additional application context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Traces allow us to follow the request and investigate its execution.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important part is not simply having all three signals available. The real value comes from being able to use them together when investigating a problem.&lt;/p&gt;

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

&lt;p&gt;Building this observability stack gave us a practical way to understand what is happening inside an application instead of only knowing whether it is running.&lt;/p&gt;

&lt;p&gt;We started with a simple Go API and added OpenTelemetry to generate metrics, logs, and traces. The OpenTelemetry Collector then provided a central layer for processing and routing those signals to Prometheus, Loki, and Jaeger.&lt;/p&gt;

&lt;p&gt;Grafana brought everything together into a single interface.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/api/transaction&lt;/code&gt; example showed why having multiple signals matters. Metrics helped us identify that the request was slow. Logs provided additional context about the transaction, while the trace allowed us to inspect the request execution and its timing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the source code in the repository below.&lt;/p&gt;

&lt;p&gt;GitHub Repository: &lt;a href="https://github.com/muhammadyulasfipahrizal/E2E-observability-stack.git" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/E2E-observability-stack.git&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>architecture</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>tooling</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:44:45 +0000</pubDate>
      <link>https://dev.to/morizal/-550g</link>
      <guid>https://dev.to/morizal/-550g</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/morizal/how-to-secure-your-api-with-cloudflare-waf-17a9" class="crayons-story__hidden-navigation-link"&gt;A Practical Approach To Securing API With Cloudflare WAF&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/morizal" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057470%2F1f961442-69a6-4b2e-9e6e-ce01e55b424e.jpg" alt="morizal profile" class="crayons-avatar__image" width="396" height="396"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/morizal" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Pahri
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Pahri
                
                
              
              &lt;div id="story-author-preview-content-4552824" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/morizal" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4057470%2F1f961442-69a6-4b2e-9e6e-ce01e55b424e.jpg" class="crayons-avatar__image" alt="" width="396" height="396"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Pahri&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/morizal/how-to-secure-your-api-with-cloudflare-waf-17a9" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 2&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/morizal/how-to-secure-your-api-with-cloudflare-waf-17a9" id="article-link-4552824"&gt;
          A Practical Approach To Securing API With Cloudflare WAF
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/morizal/how-to-secure-your-api-with-cloudflare-waf-17a9" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/morizal/how-to-secure-your-api-with-cloudflare-waf-17a9#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>A Practical Approach To Securing API With Cloudflare WAF</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:22:39 +0000</pubDate>
      <link>https://dev.to/morizal/how-to-secure-your-api-with-cloudflare-waf-17a9</link>
      <guid>https://dev.to/morizal/how-to-secure-your-api-with-cloudflare-waf-17a9</guid>
      <description>&lt;p&gt;Building an API is relatively easy. The harder problem starts when the API becomes publicly accessible.&lt;/p&gt;

&lt;p&gt;Once an API is exposed to the Internet, it can receive much more than the requests we expect. Attackers can send malicious requests, repeatedly hit endpoints, use unexpected HTTP methods, send suspicious query parameters, or simply generate enough traffic to consume application resources.&lt;/p&gt;

&lt;p&gt;A common approach is to handle all of these concerns inside the application. The application receives the request, validates it, checks authentication, and finally decides whether the request should be processed.&lt;/p&gt;

&lt;p&gt;But this raises another question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if we could reject unwanted requests before they reach the application?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where a Web Application Firewall (WAF) can help. A WAF is a security layer that inspects incoming web traffic and applies rules to determine whether a request should be allowed, challenged, or blocked.&lt;/p&gt;

&lt;p&gt;Consider a simple request to an orders API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/orders?id=1
Authorization: Bearer &amp;lt;token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without an edge security layer, the request travels directly to the application:&lt;/p&gt;

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

&lt;p&gt;With an Web Application Firewall such as Cloudflare WAF, the request can be evaluated before it reaches the application:&lt;/p&gt;

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

&lt;p&gt;If a request violates one of the configured policies, Cloudflare can reject it instead of forwarding it to the application.&lt;/p&gt;

&lt;p&gt;This is the idea behind an edge security layer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the source code in the repository below.&lt;/p&gt;

&lt;p&gt;Github Repository: &lt;a href="https://github.com/muhammadyulasfipahrizal/WAF-API.git" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/WAF-API.git&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;In this project, we will build a small Go API and place Cloudflare WAF in front of it to demonstrate several edge security controls.&lt;/p&gt;

&lt;p&gt;The application is intentionally simple. It provides enough endpoints and request patterns to demonstrate different WAF policies without introducing unnecessary application complexity.&lt;/p&gt;

&lt;p&gt;The application exposes three endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/login
/api/orders
/admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  API
&lt;/h3&gt;

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

&lt;p&gt;The &lt;code&gt;/api/login&lt;/code&gt; endpoint is used to authenticate a user. For this demo, valid credentials return a JWT that can then be used when accessing &lt;code&gt;/api/orders&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/api/orders&lt;/code&gt; endpoint represents a simple API that returns order data. It supports several query parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id
item
price
status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These parameters give us different request patterns to test against the WAF.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/admin&lt;/code&gt; endpoint represents an administrative page that should not be publicly accessible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Controls
&lt;/h3&gt;

&lt;p&gt;The WAF configuration demonstrates the following protections:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Security Control&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main page challenge&lt;/td&gt;
&lt;td&gt;Challenge requests when accessing the application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization header enforcement&lt;/td&gt;
&lt;td&gt;Reject protected API requests without an &lt;code&gt;Authorization&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP method filtering&lt;/td&gt;
&lt;td&gt;Allow only expected methods for an endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP restriction&lt;/td&gt;
&lt;td&gt;Restrict access to the restricted page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suspicious request filtering&lt;/td&gt;
&lt;td&gt;Block requests containing suspicious patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;td&gt;Limit excessive request traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API schema validation&lt;/td&gt;
&lt;td&gt;Ensure requests follow the defined API contract&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

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

&lt;p&gt;Cloudflare sits between the Internet and the application. This allows incoming requests to be evaluated against edge level security policies before they reach the origin.&lt;/p&gt;

&lt;p&gt;For example, since &lt;code&gt;/api/orders&lt;/code&gt; is a read only endpoint, requests using unsupported methods such as &lt;code&gt;POST&lt;/code&gt; can be rejected by Cloudflare before they are forwarded to the application:&lt;/p&gt;

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

&lt;p&gt;The responsibility is divided between two layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare WAF&lt;/td&gt;
&lt;td&gt;Challenge, rate limiting, method filtering, IP restrictions, suspicious request filtering, schema validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go API&lt;/td&gt;
&lt;td&gt;JWT authentication, application validation, business logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Passing the WAF does not mean that a request is trusted. It only means that the request passed the configured edge level policies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Application Structure
&lt;/h2&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;cmd&lt;/code&gt; directory contains the application entry point, while &lt;code&gt;internal&lt;/code&gt; contains the authentication, middleware, and HTTP handlers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;schema&lt;/code&gt; directory contains the OpenAPI definition used to describe the API contract and demonstrate schema validation with Cloudflare.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;web&lt;/code&gt; directory contains the frontend used to interact with the API and test the WAF scenarios.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The application is containerized using the &lt;code&gt;Dockerfile&lt;/code&gt;, and&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;.github/workflows&lt;/code&gt; is used to build and publish the Docker image with Github Action.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Cloudflare WAF
&lt;/h2&gt;

&lt;p&gt;Cloudflare WAF provides several mechanisms that can be used to enforce policies at the edge. In this project, each rule is designed to address a specific type of unwanted request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge the Main Page
&lt;/h3&gt;

&lt;p&gt;The first rule protects the main application page with an interactive challenge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(http.request.uri.path eq "/")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When accessing &lt;code&gt;/&lt;/code&gt;, the client must pass the Cloudflare challenge before the request continues to the application.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This rule prevent automated clients from accessing the application page without first passing a basic verification step. This is useful for reducing unwanted bot traffic before it reaches the origin.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Require Authorization Header for APIs
&lt;/h3&gt;

&lt;p&gt;Protected API endpoints should not receive requests that do not contain an &lt;code&gt;Authorization&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;The login endpoint is excluded because clients need to access it before they have a token.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;starts_with(http.request.uri.path, "/api/")
and not any(http.request.headers["authorization"][*] ne "")
and http.request.uri.path ne "/api/login"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behavior is:&lt;/p&gt;

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

&lt;p&gt;This does not replace authentication.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The WAF checks for the presence of the Authorization header. The Go application validates the JWT itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, a request containing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may pass the WAF rule, but the application still needs to determine whether the token is valid, expired, and properly signed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This provides a simple defense-in-depth mechanism: the edge performs an initial check, while the application performs the actual authentication.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Enforce HTTP Methods
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/api/orders&lt;/code&gt; endpoint is designed as a read-only API, so only &lt;code&gt;GET&lt;/code&gt; requests are expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(http.request.uri.path eq "/api/orders" 
and http.request.method ne "GET")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The behavior is:&lt;/p&gt;

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

&lt;p&gt;The reasoning is straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;/api/orders&lt;/code&gt; endpoint is designed as a read-only API, so unsupported write methods provide no value.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Protect Admin Page By IP
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/admin&lt;/code&gt; page represents an administrative interface and should not be publicly accessible.&lt;/p&gt;

&lt;p&gt;For this project, access is restricted to a specific trusted IP address.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(http.request.uri.path eq "/admin" 
and ip.src ne ADMIN_IP)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow is:&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;This provides a simple way to restrict an administrative endpoint at the edge.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Block Suspicious Requests
&lt;/h3&gt;

&lt;p&gt;This rule filters API requests containing patterns commonly associated with SQL injection attempts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;starts_with(http.request.uri.path, "/api/")
and (
  lower(http.request.uri.query) contains " or "
  or lower(http.request.uri.query) contains "' or "
  or lower(http.request.uri.query) contains "union select"
  or lower(http.request.uri.query) contains "select "
  or lower(http.request.uri.query) contains "drop table"
  or lower(http.request.uri.query) contains "insert into"
  or lower(http.request.uri.query) contains "delete from"
  or lower(http.request.uri.query) contains "--"
  or lower(http.request.uri.query) contains "/*"
  or lower(http.request.uri.query) contains "*/"
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/orders?id=1' OR '1'='1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can be detected as suspicious and therefore blocked by Cloduflare WAF.&lt;/p&gt;

&lt;p&gt;Application-level protection is still required, including input validation, parameterized queries, and secure database practices.&lt;/p&gt;




&lt;h3&gt;
  
  
  Rate Limiting
&lt;/h3&gt;

&lt;p&gt;An API can also be abused through repeated requests.&lt;/p&gt;

&lt;p&gt;Even when individual requests are valid, a large number of requests can consume CPU, memory, bandwidth, connection pools, and other application resources.&lt;/p&gt;

&lt;p&gt;Cloudflare can apply rate limiting before this traffic reaches the origin.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Excessive requests can be stopped before they consume resources at the origin.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is particularly useful when the origin has limited resources or when an endpoint is frequently targeted by bots.&lt;/p&gt;




&lt;h3&gt;
  
  
  API Schema Validation
&lt;/h3&gt;

&lt;p&gt;An API schema describes what a valid API request should look like.&lt;/p&gt;

&lt;p&gt;For example, the &lt;code&gt;/api/orders&lt;/code&gt; endpoint define the &lt;code&gt;id&lt;/code&gt; query parameter as an integer:&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;/api/orders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;parameters&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;id&lt;/span&gt;
        &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;query&lt;/span&gt;
        &lt;span class="na"&gt;schema&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;integer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/orders?id=10
matches the expected type.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The OpenAPI definition can be uploaded to Cloudflare WAF and used for API schema validation.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;/p&gt;

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

&lt;p&gt;This is useful because the API contract can become part of the security boundary.&lt;/p&gt;

&lt;p&gt;Instead of being used only as documentation, the OpenAPI definition can also help Cloudflare determine whether incoming requests conform to the expected API structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;After deploying the Go API behind Cloudflare WAF, the configured policies were tested against different request scenarios.&lt;/p&gt;

&lt;p&gt;The following results show the behavior of each control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge the Main Page
&lt;/h3&gt;

&lt;p&gt;Accessing &lt;code&gt;/&lt;/code&gt; triggers the configured Cloudflare interactive challenge.&lt;/p&gt;

&lt;p&gt;After successfully passing the challenge, the request continues to the application.&lt;/p&gt;

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




&lt;h3&gt;
  
  
  Require Authorization Header for APIs
&lt;/h3&gt;

&lt;p&gt;A request to a protected API without an &lt;code&gt;Authorization&lt;/code&gt; header is rejected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/orders?id=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A request containing the header can continue to the application, where the JWT is validated.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Enforce HTTP Methods
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/api/orders&lt;/code&gt; endpoint accepts &lt;code&gt;GET&lt;/code&gt;, while unsupported methods are rejected by Cloudflare.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /api/orders
→ Blocked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h3&gt;
  
  
  Protect the Admin Page by IP
&lt;/h3&gt;

&lt;p&gt;Requests to &lt;code&gt;/admin&lt;/code&gt; from the configured administrator IP are allowed, while requests from other IP addresses are blocked.&lt;/p&gt;

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




&lt;h3&gt;
  
  
  Block Suspicious Requests
&lt;/h3&gt;

&lt;p&gt;A request containing a suspicious SQL-like pattern is rejected by the WAF.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/orders?id=1' OR '1'='1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is detected and blocked by the configured rule.&lt;/p&gt;

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




&lt;h3&gt;
  
  
  Rate Limiting
&lt;/h3&gt;

&lt;p&gt;Repeated requests eventually trigger the configured rate-limiting policy.&lt;/p&gt;

&lt;p&gt;Normal traffic continues to the origin, while traffic exceeding the configured policy is stopped by Cloudflare.&lt;/p&gt;

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




&lt;h3&gt;
  
  
  API Schema Validation
&lt;/h3&gt;

&lt;p&gt;Requests that conform to the OpenAPI contract can reach the origin, while requests that do not match the defined schema can be rejected.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/orders?id=10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;matches the expected integer type for &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

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




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

&lt;p&gt;Securing an API is not only about adding authentication to the application.&lt;/p&gt;

&lt;p&gt;For an Internet-facing service, there is value in having an additional security layer between the client and the origin. Cloudflare WAF can inspect and control incoming traffic at the edge, helping protect the application from a wide range of unwanted or potentially malicious requests before they reach the origin.&lt;/p&gt;

&lt;p&gt;In this project, we demonstrate several of these capabilities, including challenges, rate limiting, HTTP method restrictions, IP restrictions, suspicious request filtering, and API schema validation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The important lesson from this project is that security responsibilities do not have to live entirely inside the application.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The edge can handle policies that are suitable for inspection before the request reaches the origin, while the application remains responsible for authentication, business logic, and application level validation.&lt;/p&gt;

&lt;p&gt;The code snippets in this article focus on the important parts of the implementation. For the complete source code, you can find it through repository below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/WAF-API.git" rel="noopener noreferrer"&gt;Github Repository&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How I Built A Blue Green Deployment Pipeline With Jenkins</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Sat, 29 Aug 2026 07:26:43 +0000</pubDate>
      <link>https://dev.to/morizal/how-i-built-a-blue-green-deployment-pipeline-with-jenkins-5lj</link>
      <guid>https://dev.to/morizal/how-i-built-a-blue-green-deployment-pipeline-with-jenkins-5lj</guid>
      <description>&lt;p&gt;Deploying a new application version can be simple when the application is small and downtime is acceptable.&lt;/p&gt;

&lt;p&gt;An engineer can stop the current application, deploy the new version, and start it again. The problem appears when the application is expected to remain available while a new version is being released.&lt;/p&gt;

&lt;p&gt;This creates a window where the application may become unavailable.&lt;/p&gt;

&lt;p&gt;The problem becomes more visible when the new version also has an issue.&lt;/p&gt;

&lt;p&gt;A deployment might complete successfully from the infrastructure's perspective, but the application itself could still be unhealthy. A container may start but fail its health check, the application may not respond correctly, or a configuration problem may only become visible after the new version receives traffic.&lt;/p&gt;

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

&lt;p&gt;In this situation, simply replacing the old version is risky.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Deployment process needs a way to prepare the new version without immediately exposing it to users, verify that it is working, and only then switch traffic to it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where blue-green deployment becomes useful.&lt;/p&gt;

&lt;p&gt;Instead of replacing the running application directly, two environments are maintained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Blue, the currently active version receiving production traffic&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Green, the new version being prepared and validated&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The new release is deployed to the inactive environment first. Once it passes the required checks, the reverse proxy switches traffic from the active environment to the new one.&lt;/p&gt;

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

&lt;p&gt;The new application version can be started and tested while the existing version continues serving users. If the new version fails validation, traffic does not need to move at all. The existing environment can simply remain active.&lt;/p&gt;

&lt;p&gt;Now Kubernetes is one way to implement this kind of deployment strategies. It provides powerful primitives for managing containers, service discovery, traffic routing, health checks, scaling, and automated rollouts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;However, Kubernetes also introduces additional infrastructure and operational complexity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not every application needs that level of orchestration, and not every company wants to operate a Kubernetes cluster.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For smaller applications with simpler operational requirements, a combination of Docker, reverse proxy, and a CI/CD tool can provide a much simpler alternative.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By the end of this project, we will have a Jenkins based deployment workflow that demonstrates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Automated application testing and image building&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Blue-green application environments&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Health checks before traffic switching&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reverse proxy based traffic switching&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SSH based remote deployment&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Safe deployment path&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The complete implementation is available in repository bellow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/blue-green-jenkins.git" rel="noopener noreferrer"&gt;Github Repository&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal of this project is to build a simple deployment environment where Jenkins can deploy a new application version alongside the currently running version, verify it, and then switch traffic to it.&lt;/p&gt;

&lt;p&gt;The architecture consists of a Jenkins server and a separate deployment server.&lt;/p&gt;

&lt;p&gt;The deployment server runs the application containers and the reverse proxy, while Jenkins acts as the automation layer responsible for executing the deployment workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

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

&lt;p&gt;There are four main pieces involved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Jenkins, orchestrates the deployment process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Blue environment, runs one version of the application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Green environment, runs the other version of the application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Nginx, receives user traffic and determines which environment should receive it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Application
&lt;/h3&gt;

&lt;p&gt;To demonstrate the deployment workflow, For this project, we use a small Go HTTP application.&lt;/p&gt;

&lt;p&gt;The application does not contain any complex business logic because the main focus of this project is the deployment process, not the application itself.&lt;/p&gt;

&lt;p&gt;The application exposes two endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /health
GET /version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Health Endpoint
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/health&lt;/code&gt; endpoint is used to determine whether the newly deployed application is ready to receive traffic.&lt;/p&gt;

&lt;p&gt;A successful response returns HTTP &lt;code&gt;200 OK&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /health

HTTP/1.1 200 OK

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

&lt;/div&gt;



&lt;p&gt;Jenkins uses this endpoint after starting the new environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Version Identification
&lt;/h3&gt;

&lt;p&gt;The second endpoint &lt;code&gt;/version&lt;/code&gt;, allows us to identify which application version is currently serving the request.&lt;/p&gt;

&lt;p&gt;The application reads the version from the &lt;code&gt;APP_VERSION&lt;/code&gt; environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_VERSION=v1
APP_VERSION=v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint then returns the configured version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /version

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

&lt;/div&gt;



&lt;p&gt;Or after deploying v2 release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /version

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

&lt;/div&gt;



&lt;p&gt;This makes the deployment behavior easy to observe.&lt;/p&gt;

&lt;p&gt;Instead of simply checking whether the application responds, we can verify exactly which version is receiving traffic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jenkins
&lt;/h3&gt;

&lt;p&gt;Jenkins is the automation engine for the deployment process.&lt;/p&gt;

&lt;p&gt;Instead of manually connecting to the deployment server and executing commands such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker build
docker compose up
curl /health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jenkins executes these steps as a repeatable pipeline.&lt;/p&gt;

&lt;p&gt;The pipeline implemented in this project performs the following stages:&lt;/p&gt;

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

&lt;p&gt;The repository contains separate Jenkinsfiles for the two deployment directions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deploy/
├── Jenkinsfile.blue
└── Jenkinsfile.green
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is because the deployment target alternates between the two environments.&lt;/p&gt;

&lt;p&gt;For example, &lt;a href="http://Jenkinsfile.blue" rel="noopener noreferrer"&gt;&lt;code&gt;Jenkinsfile.blue&lt;/code&gt;&lt;/a&gt; deploys the new image to the Blue environment and then switches Nginx to Blue. &lt;a href="http://Jenkinsfile.green" rel="noopener noreferrer"&gt;&lt;code&gt;Jenkinsfile.green&lt;/code&gt;&lt;/a&gt; does the same thing for Green.&lt;/p&gt;

&lt;p&gt;The pipeline also uses the Jenkins build number as the Docker image tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;environment {
    APP_NAME   = 'zero-downtime-app'
    IMAGE_TAG  = "${BUILD_NUMBER}"
    DEPLOY_DIR = '/home/ubuntu/projects/zero-downtime-jenkins'
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives every Jenkins build its own image tag instead of continuously overwriting a generic &lt;code&gt;latest&lt;/code&gt; image.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build #1 → zero-downtime-app:1
Build #2 → zero-downtime-app:2
Build #3 → zero-downtime-app:3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it easier to identify exactly which build is running in an environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blue Environment
&lt;/h2&gt;

&lt;p&gt;The Blue environment represents one side of the deployment pair.&lt;/p&gt;

&lt;p&gt;In this project, the application runs inside a Docker container named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app-blue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application itself listens on port &lt;code&gt;8080&lt;/code&gt; inside the container, while Docker publishes it on port &lt;code&gt;8081&lt;/code&gt; on the deployment server.&lt;/p&gt;

&lt;p&gt;The corresponding Docker Compose configuration is kept in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deploy/docker-compose.blue.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the Blue environment to be started independently from the Green environment.&lt;/p&gt;

&lt;p&gt;When Blue is the active environment, Nginx routes application traffic to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app-blue:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and is deployed using &lt;a href="http://Jenkinsfile.blue" rel="noopener noreferrer"&gt;Jenkinsfile.blue&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Green Environment
&lt;/h2&gt;

&lt;p&gt;The Green environment provides the second application slot.&lt;/p&gt;

&lt;p&gt;It runs in a separate Docker container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app-green
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like Blue, the application listens on port &lt;code&gt;8080&lt;/code&gt; inside the container. Docker publishes it on a different host port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8082
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Green environment is defined by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deploy/docker-compose.green.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Green is the active environment, Nginx routes application traffic to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app-green:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and is deployed using &lt;a href="http://Jenkinsfile.green" rel="noopener noreferrer"&gt;&lt;code&gt;Jenkinsfile.green&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse Proxy
&lt;/h2&gt;

&lt;p&gt;In this project we use Nginx, who acts as the entry point for application traffic.&lt;/p&gt;

&lt;p&gt;Users do not need to know whether Blue or Green is currently active. They simply connect to the application through Nginx.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server {
    listen 80;

    location / {
        proxy_pass http://app;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For           $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nginx uses an upstream configuration to determine which container should receive the traffic.&lt;/p&gt;

&lt;p&gt;For example, when Blue is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upstream app {
    server app-blue:8080;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when Green has been successfully deployed and verified, Jenkins changes the upstream configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upstream app {
    server app-green:8080;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline does not simply reload Nginx after changing the configuration.&lt;/p&gt;

&lt;p&gt;It first validates the configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec app-nginx nginx -t
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and only then reloads Nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec app-nginx nginx -s reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This is important because an invalid Nginx configuration should not be allowed to replace the currently working configuration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The actual traffic switching logic is implemented directly in the Jenkins pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSH Connection
&lt;/h3&gt;

&lt;p&gt;Jenkins and the deployment environment are intentionally separated.&lt;/p&gt;

&lt;p&gt;The Jenkins server does not run the application containers. Instead, Jenkins connects to the deployment server through SSH and executes the required deployment commands there.&lt;/p&gt;

&lt;p&gt;The pipeline uses two Jenkins credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deploy-host
ssh-vm1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;deploy-host&lt;/code&gt; credential provides the deployment server address, while &lt;code&gt;ssh-vm1&lt;/code&gt; is used by the Jenkins SSH agent for authentication.&lt;/p&gt;

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

&lt;p&gt;For example, when Jenkins needs to deploy the Green environment, it connects to the deployment server and executes Docker Compose there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APP_IMAGE_TAG=${IMAGE_TAG} \
docker compose \
    -f docker-compose.green.yml \
    up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same SSH connection is also used for testing, health verification, and traffic switching.&lt;/p&gt;

&lt;p&gt;This means Jenkins acts as the &lt;strong&gt;&lt;em&gt;orchestrator&lt;/em&gt;&lt;/strong&gt;, while the deployment server remains responsible for running the actual workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;The deployment workflow was tested through three scenarios: deploying the initial version (Blue), deploying the new version (Green), and handling a failed Green deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying v1
&lt;/h3&gt;

&lt;p&gt;The first deployment starts the application in the Blue environment.&lt;/p&gt;

&lt;p&gt;Jenkins runs the test, builds the Docker image, deploys &lt;code&gt;app-blue&lt;/code&gt;, and verifies its health endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Pipeline] { (Deploy Blue)

Container app-blue Creating
Container app-blue Created
Container app-blue Starting
Container app-blue Started
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The health check then succeeds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl --fail http://127.0.0.1:8081/health

{"status":"ok"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the initial deployment, a request through Nginx confirms that &lt;code&gt;v1&lt;/code&gt; is serving traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&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;"5f6a41482e36"&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;At this point, the application is running successfully and Blue is the active environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying v2
&lt;/h3&gt;

&lt;p&gt;The next deployment uses the Green environment.&lt;/p&gt;

&lt;p&gt;Jenkins first runs the application tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ok      jenkins-zero-downtime    0.002s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It then builds a new image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zero-downtime-app:6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and starts the Green environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Container app-green Creating
Container app-green Created
Container app-green Starting
Container app-green Started
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point &lt;code&gt;app-blue&lt;/code&gt; is still running. The new version is therefore deployed without first stopping the existing application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying v2
&lt;/h3&gt;

&lt;p&gt;Before changing production traffic, Jenkins checks the new environment directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--fail&lt;/span&gt; http://127.0.0.1:8082/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check succeeds:&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"ok"&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;This confirms that the new container is responding successfully before it is exposed through Nginx.&lt;/p&gt;

&lt;h3&gt;
  
  
  Switching Traffic
&lt;/h3&gt;

&lt;p&gt;After the health check succeeds, Jenkins updates the Nginx upstream configuration to point to Green:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;app-green&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration is validated before Nginx is reloaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nginx is then reloaded successfully.&lt;/p&gt;

&lt;p&gt;A request through the normal application entry point now returns &lt;code&gt;v2&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v2"&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;"9a86f0f3486"&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;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6dlelopv7uf60ro22ec.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6dlelopv7uf60ro22ec.png" alt=" " width="456" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The deployment is now complete, and Jenkins removes the old Blue container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker rm -f app-blue

app-blue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Happens When Deployment Fails?
&lt;/h3&gt;

&lt;p&gt;The workflow was also tested with an intentionally broken application version.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;failed&lt;/code&gt; branch, the &lt;code&gt;/health&lt;/code&gt; behavior causes the existing Go unit test to fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- FAIL: TestHealth (0.00s)
    main_test.go:16: expected status 200, got 500
FAIL
FAIL    jenkins-zero-downtime    0.002s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the test stage fails, Jenkins stops the deployment before creating or exposing the new version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build Image       → SKIPPED
Deploy Green      → SKIPPED
Verify Green      → SKIPPED
Switch Traffic    → SKIPPED
Stop Blue         → SKIPPED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pipeline finishes with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: script returned exit code 1
Finished: FAILURE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most importantly, the &lt;strong&gt;Switch Traffic&lt;/strong&gt; stage is never executed.&lt;/p&gt;

&lt;p&gt;The existing version therefore remains active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1"&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;"5f6a41482e36"&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;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7jh62lfk6pswx0gzpxy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7jh62lfk6pswx0gzpxy.png" alt=" " width="483" height="99"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This demonstrates the failure behavior we wanted from the deployment workflow:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A release that fails validation does not automatically replace the currently running version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The result is a simple deployment workflow that provides the core safety properties of blue-green deployment.&lt;/p&gt;

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

&lt;p&gt;This project demonstrated how Jenkins, Docker, and Nginx can be combined to implement a simple blue-green deployment workflow without Kubernetes.&lt;/p&gt;

&lt;p&gt;The new version is tested and verified before receiving traffic, while the existing version remains available during deployment. When validation fails, Jenkins stops the pipeline and leaves the current version untouched.&lt;/p&gt;

&lt;p&gt;For smaller applications, this provides a simple and practical way to reduce deployment downtime and the risk of releasing a broken version.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the full source code in the repository below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/blue-green-jenkins.git" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/blue-green-jenkins.git&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
    </item>
    <item>
      <title>Building A Secure CI Pipeline With GitHub Actions, SonarQube, And Trivy</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Thu, 27 Aug 2026 05:25:07 +0000</pubDate>
      <link>https://dev.to/morizal/building-a-secure-ci-pipeline-with-github-actions-sonarqube-and-trivy-24d1</link>
      <guid>https://dev.to/morizal/building-a-secure-ci-pipeline-with-github-actions-sonarqube-and-trivy-24d1</guid>
      <description>&lt;p&gt;A CI pipeline can tell us that an application works, but can it tell us that the application is safe to ship?&lt;/p&gt;

&lt;p&gt;A successful build does not necessarily mean that the software is secure.&lt;/p&gt;

&lt;p&gt;An application can pass its tests while still containing insecure and bad code patterns. Other than that container image built from the application may inherit vulnerable packages.&lt;/p&gt;

&lt;p&gt;The problem is that these security issues exist at different layers:&lt;/p&gt;

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

&lt;p&gt;Checking only one of these layers is not enough.&lt;/p&gt;

&lt;p&gt;For example, static code analysis can help identify security issues in the source code, but it does not tell us whether the packages inside our container image contain known vulnerabilities.&lt;/p&gt;

&lt;p&gt;Likewise, scanning a container image does not tell us whether the application source itself contains problematic code.&lt;/p&gt;

&lt;p&gt;A traditional CI workflow might look like this:&lt;/p&gt;

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

&lt;p&gt;The application is tested and, if everything works, the resulting artifact is published. But there is no explicit security check between the code and the artifact.&lt;/p&gt;

&lt;p&gt;That is what we are going to address in this article.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The complete implementation is available in repository bellow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/secure-ci-pipeline.git" rel="noopener noreferrer"&gt;Github Repository&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;We will build a small Go API and place it behind an automated CI pipeline.&lt;/p&gt;

&lt;p&gt;The application itself is intentionally simple. There is no database, authentication system, or external service. The purpose of the application is to provide an artifact that we can test, analyze, containerize, and scan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Application
&lt;/h3&gt;

&lt;p&gt;The application is a small HTTP API written in Go.&lt;/p&gt;

&lt;p&gt;It provides two endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /health
GET /api/users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/health&lt;/code&gt; endpoint is used to verify that the application is running, while &lt;code&gt;/api/users&lt;/code&gt; returns a small list of users.&lt;/p&gt;

&lt;p&gt;This gives us enough application code to run unit tests and static analysis without introducing unnecessary application complexity.&lt;/p&gt;

&lt;p&gt;The application is located under the &lt;code&gt;cmd/&lt;/code&gt; directory, with the HTTP handlers implemented in &lt;code&gt;cmd/main.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The application is then packaged as a Docker image.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI Pipeline
&lt;/h3&gt;

&lt;p&gt;The application is connected to GitHub Actions that automate the entire process.&lt;/p&gt;

&lt;p&gt;The final pipeline looks like this:&lt;/p&gt;

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

&lt;p&gt;Each stage has a specific responsibility.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;API Test, verifies that the application behaves as expected before any further analysis or packaging takes place.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SonarQube Scan, analyzes the source code for potential security, reliability, and maintainability issues.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker Build, packages the application and its runtime environment into a Docker image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trivy Scan, scans the resulting container image for known vulnerabilities in the image's operating-system packages, dependencies, and other components.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Push, publishes the container image to GitHub Container Registry after it has passed the preceding stages.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;

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

&lt;p&gt;Each part of the repository has a specific responsibility.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; defines the CI workflow and orchestrates the entire process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;cmd/main.go&lt;/code&gt; contains the Go API itself, including the HTTP server and API endpoints.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt; defines how the Go API is compiled and packaged into a Docker image.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://sonar-project.properties" rel="noopener noreferrer"&gt;&lt;code&gt;sonar-project.properties&lt;/code&gt;&lt;/a&gt; contains the SonarQube project configuration, including which directories should be analyzed and how test files should be identified.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;The application in this project intentionally contains several code patterns that can be detected by static analysis.&lt;/p&gt;

&lt;p&gt;The Docker image also uses an intentionally outdated base image so that the container scan has meaningful vulnerabilities to report.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The goal is not to demonstrate how to write perfect Go code. The goal is to demonstrate how this CI pipeline can identify problematic code and image automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Go API
&lt;/h3&gt;

&lt;p&gt;The API implementation itself is straightforward, but several functions in &lt;code&gt;main.go&lt;/code&gt; contain intentionally problematic code.&lt;/p&gt;

&lt;p&gt;For example the &lt;code&gt;processUser&lt;/code&gt; function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"invalid-user"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"invalid-user"&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="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"invalid-user"&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="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Alice"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"alice@example.com"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"valid-user"&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="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Bob"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"bob@example.com"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"valid-user"&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="s"&gt;"valid-user"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"invalid-user"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function intentionally contains deeply nested conditional statements and duplicated logic.&lt;/p&gt;

&lt;p&gt;For example, the same &lt;a href="http://user.ID" rel="noopener noreferrer"&gt;&lt;code&gt;user.ID&lt;/code&gt;&lt;/a&gt; &lt;code&gt;&amp;gt; 0&lt;/code&gt; condition is evaluated in multiple branches.&lt;/p&gt;

&lt;p&gt;This makes the function harder to understand and maintain than necessary.&lt;/p&gt;

&lt;p&gt;The same idea is used in &lt;code&gt;getUserRole&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;getUserRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"admin"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Administrator"&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"manager"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Manager"&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"developer"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Developer"&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Unknown"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function contains a long list of similar branches.&lt;/p&gt;

&lt;p&gt;This is not necessarily a security vulnerability by itself. It is included to give the static analyzer code that can be evaluated for maintainability and code-quality issues.&lt;/p&gt;

&lt;p&gt;Another example is &lt;code&gt;unreachableCode&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;unreachableCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"valid"&lt;/span&gt;

    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"this code is never reached"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"unreachable"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The statements after the first &lt;code&gt;return&lt;/code&gt; can never be executed.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of implementation problem that static analysis can identify without executing the application.&lt;/p&gt;

&lt;p&gt;We also intentionally introduce duplicated return values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;duplicatedStrings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"invalid-user"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"invalid-user"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"invalid-user"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"valid-user"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same string literal is repeated across multiple branches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Introduce Problems Intentionally?
&lt;/h3&gt;

&lt;p&gt;At this point, it may seem strange to deliberately write bad code.&lt;/p&gt;

&lt;p&gt;In a normal development project, we would try to remove these problems before merging the code.&lt;/p&gt;

&lt;p&gt;But this project has a different purpose.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If a developer introduces problematic code into the repository, will our CI pipeline detect it automatically?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That means the vulnerable or low quality code becomes part of our test scenario.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container Image
&lt;/h3&gt;

&lt;p&gt;The same approach is used for the container image.&lt;/p&gt;

&lt;p&gt;The Dockerfile intentionally uses an older Go base image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM golang:1.20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the application is built, the final image contains more than our own code. It also contains the software and packages provided by the base image.&lt;/p&gt;

&lt;p&gt;This gives Trivy something different to analyze from what SonarQube sees.&lt;/p&gt;

&lt;p&gt;SonarQube is primarily concerned with the source code, while Trivy examines the resulting container image and looks for known vulnerabilities in its components.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Test
&lt;/h3&gt;

&lt;p&gt;Before running the security analysis, the pipeline first runs the API test.&lt;/p&gt;

&lt;p&gt;The workflow uses the standard Go test command:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The repository includes tests for the API handlers, giving the pipeline a basic verification that the application still behaves as expected.&lt;/p&gt;

&lt;p&gt;If the tests fail, the following stages should not proceed.&lt;/p&gt;

&lt;h3&gt;
  
  
  SonarQube Scan
&lt;/h3&gt;

&lt;p&gt;After the test pass, the pipeline moves to static analysis.&lt;/p&gt;

&lt;p&gt;The SonarQube configuration is stored in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sonar-project.properties
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is defining the application source and test locations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sonar.sources=cmd
sonar.tests=cmd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because we intentionally introduced problematic code into &lt;code&gt;main.go&lt;/code&gt;, this stage gives us an opportunity to verify that the static analysis can actually identify those problems.&lt;/p&gt;

&lt;p&gt;For example, the unnecessary nesting in &lt;code&gt;processUser&lt;/code&gt;, unreachable statements in &lt;code&gt;unreachableCode&lt;/code&gt;, and repeated string literals in &lt;code&gt;duplicatedStrings&lt;/code&gt; provide concrete code quality problems for the analyzer to inspect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trivy Scan
&lt;/h3&gt;

&lt;p&gt;After the Docker image has been built, Trivy scans the resulting image for known vulnerabilities.&lt;/p&gt;

&lt;p&gt;The workflow specifically checks for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HIGH
CRITICAL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;severity vulnerabilities.&lt;/p&gt;

&lt;p&gt;The scan is performed against the image that was just built, rather than against an unrelated image.&lt;/p&gt;

&lt;p&gt;This is where the intentionally older base image becomes relevant.&lt;/p&gt;

&lt;p&gt;Even though the Go application itself is small, the container inherits software from its base image. Trivy can therefore identify vulnerabilities that exist below the application layer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;SonarQube tells us about problems in the code we write. Trivy tells us about vulnerabilities in the artifact we are about to distribute.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is one important configuration detail in this project.&lt;/p&gt;

&lt;p&gt;The Trivy action currently uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exit-code: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the scan reports the detected vulnerabilities but does not fail the workflow because of them.&lt;/p&gt;

&lt;p&gt;That is intentional for this demonstration.&lt;/p&gt;

&lt;p&gt;We want to see the vulnerabilities produced by our intentionally vulnerable image while still allowing the rest of the workflow to demonstrate the complete delivery process.&lt;/p&gt;

&lt;p&gt;This leads to an important distinction that we will revisit later:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A scanner can detect a vulnerability without necessarily enforcing a policy against it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Push the Image
&lt;/h3&gt;

&lt;p&gt;The final stage publishes the container image to GitHub Container Registry.&lt;/p&gt;

&lt;p&gt;The workflow is structured so that the push stage depends on the previous stages completing successfully.&lt;/p&gt;

&lt;p&gt;The workflow also defines limited permissions for the GitHub Actions job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permissions:
  contents: read
  packages: write
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow needs read access to the repository and write access to GitHub Packages in order to publish the image.&lt;/p&gt;

&lt;p&gt;With the image push as the final stage, the pipeline now has a complete path from source code to a published container artifact.&lt;/p&gt;

&lt;p&gt;The interesting part is that we can now observe whether the security checks actually detected the intentionally introduced problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;With everthing setup the scanners now can found the intentionally introduced problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  SonarQube Result
&lt;/h3&gt;

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

&lt;p&gt;The SonarQube analysis identified four issues in the Go application.&lt;/p&gt;

&lt;p&gt;The most significant finding was in &lt;code&gt;processUser&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Refactor this method to reduce its Cognitive Complexity from 21 to the 15 allowed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SonarQube assigned this issue a High maintainability impact.&lt;/p&gt;

&lt;p&gt;The function has a Cognitive Complexity score of &lt;strong&gt;21&lt;/strong&gt;, while the configured threshold is &lt;strong&gt;15&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is a good example of why static analysis is useful.&lt;/p&gt;

&lt;p&gt;The application can still compile and the API can still work correctly, but the implementation is unnecessarily difficult to reason about. The CI analysis makes this problem visible without requiring a reviewer to manually inspect every conditional branch.&lt;/p&gt;

&lt;p&gt;SonarQube also detected duplicated literals:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Define a constant instead of duplicating this literal&lt;/strong&gt; &lt;code&gt;"invalid-user"&lt;/code&gt; &lt;strong&gt;7 times.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These findings map directly to the intentionally duplicated values in &lt;code&gt;processUser&lt;/code&gt; and &lt;code&gt;duplicatedStrings&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, SonarQube identified the unreachable code in &lt;code&gt;unreachableCode&lt;/code&gt; function:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Refactor this piece of code to not have any dead code after this&lt;/strong&gt; &lt;code&gt;"return"&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This issue was classified as a &lt;strong&gt;Medium&lt;/strong&gt; reliability issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quality Gate Enforcement
&lt;/h3&gt;

&lt;p&gt;There is another important detail in the workflow.&lt;/p&gt;

&lt;p&gt;The SonarQube Quality Gate step is intentionally commented out to allow the pipeline to keep running, as this project is currently focused on detecting code quality issues rather than enforcing them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# - name: SonarQube quality gate
#   uses: SonarSource/sonarqube-quality-gate-action@cf038b0e0cdecfa9e56c198bbb7d21d751d62c3b # v1.2.0
#   timeout-minutes: 5
#   env:
#     SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
#     SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Trivy Results
&lt;/h3&gt;

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

&lt;p&gt;Trivy analyzed the container image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;secure-ci-pipeline:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image was based on Debian 12.4 and Trivy reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1641 vulnerabilities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;at the Debian package layer.&lt;/p&gt;

&lt;p&gt;Trivy also identified vulnerabilities in the Go binaries contained in the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/app       → 25 vulnerabilities
/usr/local/go/bin/go       → 25 vulnerabilities
/usr/local/go/bin/gofmt    → 25 vulnerabilities
/usr/local/go/pkg/tool/... → 25 vulnerabilities
---------------
---------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current Trivy configuration uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exit-code: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So although Trivy discovered a large number of vulnerabilities, the workflow does not stop because of those findings.&lt;/p&gt;

&lt;p&gt;A mature pipeline could eventually evolve from:&lt;/p&gt;

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

&lt;p&gt;Into:&lt;/p&gt;

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

&lt;p&gt;The benefit of transitioning to this approach is that security findings become part of the release decision instead of being treated as information that developers may review later.&lt;/p&gt;

&lt;p&gt;Without enforcement, a pipeline can report a critical vulnerability while still allowing the image to be published. This creates a gap between knowing about a problem and preventing the problem from reaching the next stage.&lt;/p&gt;

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

&lt;p&gt;In this project, we addressed the gap described in the introduction by adding security checks directly into the CI workflow.&lt;/p&gt;

&lt;p&gt;Instead of only asking whether the application works, the pipeline also checks whether the source code contains quality or reliability problems and whether the resulting container image contains known vulnerabilities.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The key benefit is moving security checks earlier in the delivery process, so problems can be identified before they become production problems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The code snippets in this article focus on the important parts of the implementation. For the complete source code, you can find it through repository below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/secure-ci-pipeline.git" rel="noopener noreferrer"&gt;Github Repository&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why Is Your Linux Server Slow? A Practical Guide to Performance Troubleshooting</title>
      <dc:creator>Pahri</dc:creator>
      <pubDate>Fri, 21 Aug 2026 03:21:50 +0000</pubDate>
      <link>https://dev.to/morizal/why-is-your-linux-server-slow-a-practical-guide-to-performance-troubleshooting-10fe</link>
      <guid>https://dev.to/morizal/why-is-your-linux-server-slow-a-practical-guide-to-performance-troubleshooting-10fe</guid>
      <description>&lt;p&gt;A Linux server can be running without any obvious errors and still feel painfully slow.&lt;/p&gt;

&lt;p&gt;Applications may take longer to respond, SSH sessions may become sluggish, users may suddenly experience high latency. The difficult part is that the root cause is not always obvious.&lt;/p&gt;

&lt;p&gt;A slow Linux server does not necessarily mean that the CPU is overloaded.&lt;/p&gt;

&lt;p&gt;It could be memory pressure, excessive disk I/O, network congestion, too many open file descriptors, or simply a process consuming resources unexpectedly.&lt;/p&gt;

&lt;p&gt;The challenge for a DevOps Engineer is not just finding that the server is slow, but identifying why it is slow.&lt;/p&gt;

&lt;p&gt;Instead of immediately restarting services or killing processes, we need to investigate the system, layer by layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Is the CPU under pressure?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is the system running out of memory?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is disk I/O becoming a bottleneck?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Are processes exhausting file descriptors?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Which process is actually responsible for the problem?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is a practical introduction to Linux performance troubleshooting using real reproducible scenarios.&lt;/p&gt;

&lt;p&gt;Rather than looking at isolated commands, we will build a troubleshooting mindset: observe the symptom, collect evidence, identify the bottleneck, apply the appropriate solution, and verify the result.&lt;/p&gt;

&lt;p&gt;To make the investigation reproducible, I created a companion lab repository containing scenarios that intentionally introduce problems:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/linux-performance-lab.git" rel="noopener noreferrer"&gt;github repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  High CPU
&lt;/h2&gt;

&lt;p&gt;High CPU usage is one of the most common Linux performance problems.&lt;/p&gt;

&lt;p&gt;In this scenario, we intentionally create a process that continuously executes an infinite loop.&lt;/p&gt;

&lt;p&gt;The reproduction script is available in the &lt;a href="https://github.com/muhammadyulasfipahrizal/linux-performance-lab.git" rel="noopener noreferrer"&gt;github repository&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;Run the reproduction script:&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;chmod&lt;/span&gt; +x ./high-cpu/reproduce.sh
./high-cpu/reproduce.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script will display a warning before starting the scenario.&lt;/p&gt;

&lt;p&gt;While it is running, open another terminal to investigate the system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify High CPU Usage
&lt;/h3&gt;

&lt;p&gt;The first step is to confirm whether the system is actually experiencing high CPU utilization.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;top&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;top
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the CPU summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%Cpu(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%Cpu(s): 95.2 us,  1.8 sy,  0.0 ni,  0.0 id,  2.5 wa, ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;us&lt;/code&gt; — CPU time spent running user-space processes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;sy&lt;/code&gt; — CPU time spent running kernel-space processes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;id&lt;/code&gt; — percentage of CPU time that is idle&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;wa&lt;/code&gt; — CPU time waiting for I/O&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;id&lt;/code&gt; is consistently very low while &lt;code&gt;us&lt;/code&gt; is high, the CPU is heavily utilized by processes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify the Process
&lt;/h3&gt;

&lt;p&gt;The next step is to find the process that consume the most CPU.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux &lt;span class="nt"&gt;--sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-%cpu | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root       38690 90.5  0.0   7740  3512 pts/4   R+   02:39   0:16 bash ./reproduce.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PID&lt;/code&gt; — Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;%CPU&lt;/code&gt; — CPU consumption&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;%MEM&lt;/code&gt; — Memory consumption&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;STAT&lt;/code&gt; — Current process state&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;COMMAND&lt;/code&gt; — Command used to start the process&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here &lt;code&gt;bash ./reproduce.sh&lt;/code&gt; process is clearly consuming a significant amount of CPU.&lt;/p&gt;

&lt;p&gt;We now have a candidate process, but before terminating it, we should inspect it further.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect the Process
&lt;/h3&gt;

&lt;p&gt;Use the PID identified in the previous step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-fp&lt;/span&gt; 38690
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UID          PID    PPID  C STIME TTY          TIME CMD
root       38690   37782 96 02:39 pts/4    00:00:45 bash ./reproduce.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides additional information about the process.&lt;/p&gt;

&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PID&lt;/code&gt; — Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PPID&lt;/code&gt; — Parent Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;STIME&lt;/code&gt; — Process start time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;TIME&lt;/code&gt; — Total CPU time consumed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;CMD&lt;/code&gt; — Command used to start the process&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Understand the Cause
&lt;/h3&gt;

&lt;p&gt;Inspect the process script:&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;cat &lt;/span&gt;reproduce.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is:&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="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    :
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates an infinite loop.&lt;/p&gt;

&lt;p&gt;The loop continuously executes without any &lt;code&gt;sleep&lt;/code&gt;, blocking operation, or other mechanism that would allow the process to stop consuming CPU therefore keeps the CPU busy.&lt;/p&gt;

&lt;p&gt;This is the root cause of the high CPU usage in this scenario.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix and Verify
&lt;/h3&gt;

&lt;p&gt;Once the responsible process and its cause have been identified, terminate the process using its PID:&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;kill &lt;/span&gt;38690
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the CPU usage again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;top
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process should no longer appear in the process list, and CPU utilization should return to its previous level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disk I/O
&lt;/h2&gt;

&lt;p&gt;High disk I/O is another common Linux performance problem.&lt;/p&gt;

&lt;p&gt;In this scenario, we intentionally create a process that continuously writes data to disk.&lt;/p&gt;

&lt;p&gt;The reproduction script is available in the &lt;a href="https://github.com/muhammadyulasfipahrizal/linux-performance-lab.git" rel="noopener noreferrer"&gt;github repository&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;Run the reproduction script:&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;chmod&lt;/span&gt; +x ./disk/reproduce.sh
./disk/reproduce.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Identify Disk I/O
&lt;/h3&gt;

&lt;p&gt;The first step is to confirm whether the system is actually experiencing high Disk I/O activity.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;iostat&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iostat &lt;span class="nt"&gt;-xz&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Device            r/s     w/s   rkB/s   wkB/s  %util
sda              0.00  125.00    0.00  51200.00  98.50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;r/s&lt;/code&gt; — Number of read requests per second&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;w/s&lt;/code&gt; — Number of write requests per second&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;rkB/s&lt;/code&gt; — Data read per second&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;wkB/s&lt;/code&gt; — Data written per second&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;%util&lt;/code&gt; — Percentage of time the device was busy&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;%util&lt;/code&gt; is consistently close to &lt;code&gt;100%&lt;/code&gt;, the storage device is heavily utilized.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify the Process
&lt;/h3&gt;

&lt;p&gt;The next step is to find which process is generating the disk activity.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pidstat &lt;span class="nt"&gt;-d&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
0     41043      0.00 259548.51      0.00       0  bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PID&lt;/code&gt; — Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;COMMAND&lt;/code&gt; — Command generating the I/O&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here the &lt;code&gt;PID&lt;/code&gt; 41043 process is clearly generating significant disk write activity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect the Process
&lt;/h3&gt;

&lt;p&gt;Use the PID identified from the previous step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-fp&lt;/span&gt; 41043
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UID          PID    PPID  C STIME TTY          TIME CMD
root       41043   37782  0 03:03 pts/4    00:00:00 bash ./reproduce.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides additional information about the process.&lt;/p&gt;

&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PID&lt;/code&gt; — Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PPID&lt;/code&gt; — Parent Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;STIME&lt;/code&gt; — Process start time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;TIME&lt;/code&gt; — Total CPU time consumed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;CMD&lt;/code&gt; — Command used to start the process&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command shows that the process is continuously writing data to &lt;code&gt;testfile&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understand the Cause
&lt;/h3&gt;

&lt;p&gt;Inspect the reproduction script:&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;cat &lt;/span&gt;reproduce.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is:&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="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;testfile &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1024 &lt;span class="nv"&gt;conv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fsync
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; testfile
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script continuously creates a file and writes data to it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;dd&lt;/code&gt; command reads zero-filled data from &lt;code&gt;/dev/zero&lt;/code&gt; and writes it to &lt;code&gt;testfile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;conv=fsync&lt;/code&gt; option forces &lt;code&gt;dd&lt;/code&gt; to flush the written data to the storage device before completing the operation. This makes the scenario generate actual disk write activity instead of relying entirely on the filesystem page cache.&lt;/p&gt;

&lt;p&gt;The file is then removed and the process repeats the operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix and Verify
&lt;/h3&gt;

&lt;p&gt;Once the responsible process and its cause have been identified, terminate the process using its PID:&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;kill &lt;/span&gt;41043
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the disk activity again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iostat &lt;span class="nt"&gt;-xz&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also verify the processes performing I/O:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iotop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;dd&lt;/code&gt; process should no longer appear, disk write throughput should drop significantly, and &lt;code&gt;%util&lt;/code&gt; should return to its previous level.&lt;/p&gt;

&lt;p&gt;Finally, verify that the test file has been removed:&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;ls&lt;/span&gt; &lt;span class="nt"&gt;-lh&lt;/span&gt; testfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Memory Pressure
&lt;/h2&gt;

&lt;p&gt;High memory usage is another common Linux performance problem.&lt;/p&gt;

&lt;p&gt;In this scenario, we create a process that allocates a large amount of memory and keeps it allocated.&lt;/p&gt;

&lt;p&gt;The reproduction script is available in the &lt;a href="https://github.com/muhammadyulasfipahrizal/linux-performance-lab.git" rel="noopener noreferrer"&gt;github repository&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;Run the reproduction script with a memory target:&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;chmod&lt;/span&gt; +x ./memory-pressure/reproduce.sh
./memory-pressure/reproduce.sh 2G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;2G&lt;/code&gt; with the amount of memory you want to allocate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify Memory Pressure
&lt;/h3&gt;

&lt;p&gt;The first step is to confirm whether the system is actually experiencing high memory utilization.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;free &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               total        used        free      shared  buff/cache   available
Mem:           3.8Gi       3.1Gi       141Mi       1.0Mi       822Mi       692Mi
Swap:             0B          0B          0B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;total&lt;/code&gt; — Total physical memory available&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;used&lt;/code&gt; — Memory currently used by the system&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;free&lt;/code&gt; — Completely unused memory&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;buff/cache&lt;/code&gt; — Memory used by the kernel for buffers and filesystem cache&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;available&lt;/code&gt; — Memory available for new applications&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;available&lt;/code&gt; value is particularly important when diagnosing memory pressure. If &lt;code&gt;available&lt;/code&gt; memory is consistently very low while &lt;code&gt;used&lt;/code&gt; memory is high, the system is under significant memory pressure.&lt;/p&gt;

&lt;p&gt;If the system has swap enabled, also pay attention to swap usage. Significant swap usage can indicate that the system does not have enough physical memory to satisfy current workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify the Process
&lt;/h3&gt;

&lt;p&gt;The next step is to find which process is consuming the most memory.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux &lt;span class="nt"&gt;--sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-%mem | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USER         PID %CPU %MEM    VSZ    RSS TTY      STAT START   TIME COMMAND
root       39937  0.1 52.7 2123808 2116080 pts/4 S+   02:56   0:03 python3 ./memory_hog.py 2G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PID&lt;/code&gt; — Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;%MEM&lt;/code&gt; — Percentage of physical memory used by the process&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;VSZ&lt;/code&gt; — Virtual memory size&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;RSS&lt;/code&gt; — Resident memory currently held in RAM&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;STAT&lt;/code&gt; — Current process state&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;COMMAND&lt;/code&gt; — Command used to start the process&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here the &lt;code&gt;PID&lt;/code&gt; 39937 process is clearly consuming a significant amount of memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect the Process
&lt;/h3&gt;

&lt;p&gt;Use the PID identified in the previous step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-fp&lt;/span&gt; 39937
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UID          PID    PPID  C STIME TTY          TIME CMD
root       39937   39910  0 02:56 pts/4    00:00:03 python3 ./memory_hog.py 2G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides additional information about the process.&lt;/p&gt;

&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PID&lt;/code&gt; — Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PPID&lt;/code&gt; — Parent Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;STIME&lt;/code&gt; — Process start time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;TIME&lt;/code&gt; — Total CPU time consumed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;CMD&lt;/code&gt; — Command used to start the process&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command shows that the process is running &lt;code&gt;memory_hog.py&lt;/code&gt; with a target allocation of &lt;code&gt;2G&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understand the Cause
&lt;/h3&gt;

&lt;p&gt;Inspect the reproduction script:&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;cat &lt;/span&gt;reproduce.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell script starts the Python memory allocation program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/memory_hog.py"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MEMORY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python script can then be inspected:&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;cat &lt;/span&gt;memory_hog.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;allocated_bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;allocated_bytes&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;chunk&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="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="n"&gt;allocated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;allocated_bytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script allocates memory in &lt;code&gt;1 MiB&lt;/code&gt; chunks until the requested memory amount is reached.&lt;/p&gt;

&lt;p&gt;Each allocated chunk is stored in the &lt;code&gt;allocated&lt;/code&gt; list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;allocated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps references to the allocated memory, preventing Python from releasing those objects.&lt;/p&gt;

&lt;p&gt;The script then remains running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This large memory allocation is the root cause of the memory pressure in this scenario.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix and Verify
&lt;/h3&gt;

&lt;p&gt;Once the responsible process and its cause have been identified, terminate the process using its PID:&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;kill &lt;/span&gt;39937
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the memory usage again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;free &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also verify the processes sorted by memory usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux &lt;span class="nt"&gt;--sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-%mem | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;memory_hog.py&lt;/code&gt; process should no longer appear in the process list.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;available&lt;/code&gt; memory should increase, and overall memory utilization should return closer to its previous level.&lt;/p&gt;

&lt;h2&gt;
  
  
  File Descriptor Exhaustion
&lt;/h2&gt;

&lt;p&gt;In this scenario, we intentionally create a process that continuously opens file descriptors without closing them.&lt;/p&gt;

&lt;p&gt;The reproduction script is available in the &lt;a href="https://github.com/muhammadyulasfipahrizal/linux-performance-lab.git" rel="noopener noreferrer"&gt;github repository&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;Run the reproduction script:&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;chmod&lt;/span&gt; +x ./file-descriptor/reproduce.sh
./file-descriptor/reproduce.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Identify File Descriptor Pressure
&lt;/h3&gt;

&lt;p&gt;The first step is to check the system-wide file descriptor usage.&lt;/p&gt;

&lt;p&gt;Use:&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;cat&lt;/span&gt; /proc/sys/fs/file-nr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2656    0    9223372036854775807
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The values represent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;allocated&lt;/code&gt; — Number of allocated file handles&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;unused&lt;/code&gt; — Number of unused allocated file handles&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;maximum&lt;/code&gt; — Maximum number of file handles allowed system-wide&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;allocated&lt;/code&gt; value represents file handles currently allocated by the kernel.&lt;/p&gt;

&lt;p&gt;A high number of allocated file handles can indicate file descriptor pressure, especially when applications are approaching their per-process or system-wide limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify the Process
&lt;/h3&gt;

&lt;p&gt;The next step is to find the process running &lt;code&gt;fd_exhaustion.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'[f]d_exhaustion'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root       42297  0.2  0.2  18488 10504 pts/4    S+   03:11   0:00 python3 ./fd_exhaustion.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PID&lt;/code&gt; — Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;%CPU&lt;/code&gt; — CPU usage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;%MEM&lt;/code&gt; — Memory usage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;COMMAND&lt;/code&gt; — Command used to start the process&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here the &lt;code&gt;python3 ./fd_exhaustion.py&lt;/code&gt; process is the candidate responsible for the file descriptor exhaustion.&lt;/p&gt;

&lt;p&gt;Use the identified PID for the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect the Process
&lt;/h3&gt;

&lt;p&gt;The next step is to determine how many file descriptors the process currently has open.&lt;/p&gt;

&lt;p&gt;Use:&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;ls&lt;/span&gt; /proc/42297/fd | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This indicates that the process currently has 1024 file descriptors open.&lt;/p&gt;

&lt;p&gt;You can inspect the actual descriptors with:&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;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /proc/&amp;lt;PID&amp;gt;/fd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lr-x------ 1 root root 64 Aug 21 03:11 0 -&amp;gt; /dev/pts/4
lrwx------ 1 root root 64 Aug 21 03:11 1 -&amp;gt; /dev/pts/4
lrwx------ 1 root root 64 Aug 21 03:11 2 -&amp;gt; /dev/pts/4
lr-x------ 1 root root 64 Aug 21 03:11 3 -&amp;gt; /dev/null
lr-x------ 1 root root 64 Aug 21 03:11 4 -&amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The large number of &lt;code&gt;/dev/null&lt;/code&gt; entries indicates that the process is continuously opening new file descriptors.&lt;/p&gt;

&lt;p&gt;Next, check the process limits:&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;cat&lt;/span&gt; /proc/42297/limits | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"open files"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Max open files            1024                 1048576                files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first value is the soft limit, while the second value is the hard limit.&lt;/p&gt;

&lt;p&gt;The soft limit is the limit currently enforced for the process.&lt;/p&gt;

&lt;p&gt;Once the process reaches this limit, attempts to open additional file descriptors will fail.&lt;/p&gt;




&lt;h3&gt;
  
  
  Understand the Cause
&lt;/h3&gt;

&lt;p&gt;Inspect the reproduction script:&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;cat &lt;/span&gt;fd_exhaustion.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&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="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/dev/null&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;O_RDONLY&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;FILES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script continuously opens &lt;code&gt;/dev/null&lt;/code&gt; using &lt;code&gt;os.open()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every returned file descriptor is stored in the &lt;code&gt;FILES&lt;/code&gt; list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;FILES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The descriptors are intentionally not closed while the scenario is running. As a result, the number of open file descriptors continuously increases&lt;/p&gt;

&lt;p&gt;Eventually, &lt;code&gt;os.open()&lt;/code&gt; fails because the process has reached its file descriptor limit.&lt;/p&gt;

&lt;p&gt;The script then reports the error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to open new file descriptor
[Errno 24] Too many open files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the root cause of the file descriptor exhaustion in this scenario.&lt;/p&gt;

&lt;p&gt;File descriptor exhaustion can have broader consequences for real applications. Processes may fail to open files, accept new connections, create sockets, or perform other operations that require file descriptors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix and Verify
&lt;/h3&gt;

&lt;p&gt;Once the responsible process and its cause have been identified, terminate the process using its PID:&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;kill &lt;/span&gt;42297
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the process has stopped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-p&lt;/span&gt; 42297
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the process has stopped, the command should return no process entry.&lt;/p&gt;

&lt;p&gt;You can also verify that the process no longer exists:&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;test&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /proc/&amp;lt;PID&amp;gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Process still exists"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Process stopped"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file descriptors belonging to the process are automatically released by the kernel when the process terminates.&lt;/p&gt;

&lt;p&gt;Therefore, &lt;code&gt;/proc/&amp;lt;PID&amp;gt;/fd&lt;/code&gt; will no longer be available after the process has exited.&lt;/p&gt;

&lt;p&gt;Finally, verify the system-wide file descriptor state again:&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;cat&lt;/span&gt; /proc/sys/fs/file-nr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The allocated file handle count should return closer to its previous level after the exhausted process has been terminated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network Connection Exhaustion
&lt;/h2&gt;

&lt;p&gt;In this scenario, we intentionally create a process that continuously opens TCP connections to a local server and keeps those connections open.&lt;/p&gt;

&lt;p&gt;The reproduction script is available in the &lt;a href="https://github.com/muhammadyulasfipahrizal/linux-performance-lab.git" rel="noopener noreferrer"&gt;github repository&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;Run the reproduction script:&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;chmod&lt;/span&gt; +x ./network-connection/reproduce.sh
./network-connection/reproduce.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script will display a warning before starting the scenario.&lt;/p&gt;

&lt;p&gt;The reproduction uses &lt;code&gt;127.0.0.1:9000&lt;/code&gt; as the target, so all connections remain on the local machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify Abnormal Connections
&lt;/h3&gt;

&lt;p&gt;The first step is to confirm whether the system is experiencing an unusually large number of TCP connections.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;ss&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total: 1252
TCP:   2051 (estab 1023, closed 1021, orphaned 1, timewait 1020)

Transport Total     IP        IPv6
RAW       1         0         1
UDP       3         3         0
TCP       1030      1028      2
INET      1034      1031      3
FRAG      0         0         0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;estab&lt;/code&gt; — Established TCP connections&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;orphaned&lt;/code&gt; — Orphaned TCP connections&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;timewait&lt;/code&gt; — Connections waiting to be fully closed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;TCP&lt;/code&gt; — Total TCP sockets&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A significant increase in established connections can indicate abnormal connection usage.&lt;/p&gt;

&lt;p&gt;In this scenario, the large number of &lt;code&gt;estab&lt;/code&gt; connections is the main indicator that something is continuously creating and maintaining TCP connections.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identify the Connection and Process
&lt;/h3&gt;

&lt;p&gt;The next step is to identify which process owns the connections.&lt;/p&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-tanp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLOSE-WAIT  1  0  127.0.0.1:46374  127.0.0.1:9000  users:(("python3",pid=46938,fd=880))
CLOSE-WAIT  1  0  127.0.0.1:39120  127.0.0.1:9000  users:(("python3",pid=46938,fd=86))
CLOSE-WAIT  1  0  127.0.0.1:44192  127.0.0.1:9000  users:(("python3",pid=46938,fd=634))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important information is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;State&lt;/code&gt; — Current TCP connection state&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Local Address:Port&lt;/code&gt; — Local endpoint&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Peer Address:Port&lt;/code&gt; — Remote endpoint&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;users&lt;/code&gt; — Process owning the socket&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;pid&lt;/code&gt; — Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;fd&lt;/code&gt; — File descriptor associated with the socket&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here we can identify the process responsible for the connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users:(("python3",pid=46938,fd=880))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PID is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;46938
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same PID appearing across many connections is a strong indication that one process is responsible for creating the connection buildup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspect the Process
&lt;/h3&gt;

&lt;p&gt;Use the PID identified in the previous step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-fp&lt;/span&gt; 46938
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UID          PID    PPID  C STIME TTY          TIME CMD
root       46938   46922  0 03:42 pts/1    00:00:00 python3 ./connection_exhaustion.py 127.0.0.1 9000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides additional information about the process.&lt;/p&gt;

&lt;p&gt;The most important fields are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PID&lt;/code&gt; — Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;PPID&lt;/code&gt; — Parent Process ID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;STIME&lt;/code&gt; — Process start time&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;TIME&lt;/code&gt; — Total CPU time consumed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;CMD&lt;/code&gt; — Command used to start the process&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command shows that the process is running &lt;code&gt;connection_exhaustion.py&lt;/code&gt; and connecting to &lt;code&gt;127.0.0.1:9000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, check how many file descriptors the process currently has open:&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;ls&lt;/span&gt; /proc/46938/fd | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A large number of file descriptors is expected because every TCP socket consumes a file descriptor.&lt;/p&gt;

&lt;p&gt;You can inspect the descriptors directly:&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;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; /proc/46938/fd | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see entries pointing to sockets, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lrwx------ 1 root root 64 Aug 21 03:42 86 -&amp;gt; 'socket:[123456]'
lrwx------ 1 root root 64 Aug 21 03:42 87 -&amp;gt; 'socket:[123457]'
lrwx------ 1 root root 64 Aug 21 03:42 88 -&amp;gt; 'socket:[123458]'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms that the process is holding a large number of socket file descriptors.&lt;/p&gt;




&lt;h3&gt;
  
  
  Understand the Cause
&lt;/h3&gt;

&lt;p&gt;Inspect the reproduction script:&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;cat &lt;/span&gt;connection_exhaustion.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&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="n"&gt;sock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AF_INET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SOCK_STREAM&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;CONNECTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script continuously creates TCP sockets and connects them to the target server.&lt;/p&gt;

&lt;p&gt;Every successful connection is stored in the &lt;code&gt;CONNECTIONS&lt;/code&gt; list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CONNECTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sockets are intentionally not closed while the scenario is running. As a result, the number of active connections continuously increases:&lt;/p&gt;

&lt;p&gt;The TCP server is also intentionally designed to keep accepted connections open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the server does not immediately close the connections created by the client. Eventually, the client process can reach its file descriptor limit and fail to create additional sockets.&lt;/p&gt;

&lt;p&gt;This pattern can occur in real applications when connections are not properly closed, connection pools are misconfigured, or an application continuously creates new connections instead of reusing existing ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix and Verify
&lt;/h3&gt;

&lt;p&gt;Once the responsible process and its cause have been identified, terminate the process using its PID:&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;kill &lt;/span&gt;46938
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the network connection summary again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also check the connections to port &lt;code&gt;9000&lt;/code&gt; directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-tan&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;':9000'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The number of established TCP connections should return to its previous level.&lt;/p&gt;

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

&lt;p&gt;Linux performance troubleshooting is not about finding a single command that tells you why a server is slow.&lt;/p&gt;

&lt;p&gt;It is about building a structured investigation from the symptoms you observe.&lt;/p&gt;

&lt;p&gt;In the scenarios covered in this article, we investigated several different types of Linux performance problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;High CPU caused by an infinite loop&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;High disk I/O caused by continuous writes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Memory pressure caused by excessive memory allocation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;File descriptor exhaustion caused by descriptors that are never closed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Network connection exhaustion caused by continuously opened TCP connections&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although the symptoms are different, the troubleshooting approach remains consistent:&lt;/p&gt;

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

&lt;p&gt;The important lesson is to avoid making assumptions based on a single metric.&lt;/p&gt;

&lt;p&gt;High CPU does not immediately tell you which process is responsible. High memory usage does not necessarily mean the system is running out of usable memory. A large number of TCP connections does not automatically mean the network is slow.&lt;/p&gt;

&lt;p&gt;Each symptom needs to be investigated with the right evidence.&lt;/p&gt;

&lt;p&gt;When a production server becomes slow, the most valuable skill is not knowing how to restart it.&lt;/p&gt;

&lt;p&gt;It is being able to answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is happening, why is it happening, and how can I prove that my fix actually worked?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;The scenarios in this article are intentionally simplified, but the troubleshooting methodology can be applied to much more complex production incidents.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can reproduce all of these scenarios and investigate yourself by cloning my github repository bellow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/muhammadyulasfipahrizal/linux-performance-lab.git" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/linux-performance-lab.git&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
    </item>
  </channel>
</rss>
