<?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: Mo Rizal</title>
    <description>The latest articles on DEV Community by Mo Rizal (@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: Mo Rizal</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>From Static Passwords to Dynamic Database Credentials with HashiCorp Vault</title>
      <dc:creator>Mo Rizal</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>Mo Rizal</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>Mo Rizal</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>Mo Rizal</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;
              Mo Rizal
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mo Rizal
                
                
              
              &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;Mo Rizal&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>Mo Rizal</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>Mo Rizal</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>Mo Rizal</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>Mo Rizal</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>
    <item>
      <title>7 Bash Scripts Every Devops Engineer Should Have</title>
      <dc:creator>Mo Rizal</dc:creator>
      <pubDate>Tue, 18 Aug 2026 03:58:05 +0000</pubDate>
      <link>https://dev.to/morizal/7-bash-scripts-every-devops-engineer-should-have-3cgl</link>
      <guid>https://dev.to/morizal/7-bash-scripts-every-devops-engineer-should-have-3cgl</guid>
      <description>&lt;p&gt;A production server rarely fails in a convenient way.&lt;/p&gt;

&lt;p&gt;A disk suddenly fills up. A process consumes all available CPU. A network connection becomes unreliable, or an SSH account needs to be investigated after a suspicious login.&lt;/p&gt;

&lt;p&gt;When this happens, a DevOps engineer usually does not start by writing a new monitoring system. The first response is often much simpler: connect to the server and investigate what is happening.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The problem is that the investigation itself can become repetitive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Running &lt;code&gt;df&lt;/code&gt;, &lt;code&gt;du&lt;/code&gt;, &lt;code&gt;ps&lt;/code&gt;, &lt;code&gt;ss&lt;/code&gt;, &lt;code&gt;journalctl&lt;/code&gt;, checking authentication logs, and collecting system information manually may seem trivial on one server. But during an incident, repeatedly remembering which commands to run, in which order, and how to collect the results wastes time and makes investigations inconsistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is where Bash becomes useful.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of treating Bash as a language for small one line commands, we can use it to turn recurring operational procedures into reusable tools. The goal is not to replace observability platforms or configuration management systems. The goal is to create lightweight scripts that can be executed directly on a server when an engineer needs fast, consistent information.&lt;/p&gt;

&lt;p&gt;In this project, we will build a collection of 7 practical bash scripts for common DevOps operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Investigating disk usage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Collecting incident context&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inspecting log activity within a specific time window&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Investigating network paths&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Auditing running processes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Auditing SSH access&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cleaning up unnecessary system resources&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each script is designed around a real operational problem.&lt;/p&gt;

&lt;p&gt;By the end of this project, we will have a small Bash toolkit that can turn common server investigations from a collection of ad-hoc commands into repeatable operational workflows.&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 Bash Scripts 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/7-bash-scripts.git" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/terraform-setup&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Disk Investigator
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://disk-investigator.sh" rel="noopener noreferrer"&gt;&lt;code&gt;disk-investigator.sh&lt;/code&gt;&lt;/a&gt; is a filesystem investigation script designed to answer a common question during Linux server incidents:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is consuming the disk, and where should I investigate next?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of running several commands manually, the script combines filesystem, inode, directory, file, and modification-time information into a single investigation report.&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%2F6e4wze8xht9de5df8c7a.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%2F6e4wze8xht9de5df8c7a.png" alt=" " width="437" height="505"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script accepts three parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--path&lt;/code&gt; controls which directory or filesystem is investigated.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--top&lt;/code&gt; controls how many results are displayed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;--min-size&lt;/code&gt; defines what qualifies as a large file&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script starts with the filesystem itself. It reports the filesystem type and basic capacity information, then checks inode consumption. This distinction is important because a server can run out of inodes even when there is still available disk space.&lt;/p&gt;

&lt;p&gt;It then moves from a broad view to more specific information:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Filesystem capacity and inode usage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Largest directories&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Largest files&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Large files modified within the last 24 hours&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  2. Incident Context
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://incident-context.sh" rel="noopener noreferrer"&gt;&lt;code&gt;incident-context.sh&lt;/code&gt;&lt;/a&gt; collects the current state of a Linux server into a single incident report.&lt;/p&gt;

&lt;p&gt;During an incident, the first challenge is often not finding the exact root cause immediately. It is establishing what the server looked like when the problem occurred.&lt;/p&gt;

&lt;p&gt;Instead of manually running commands such as &lt;code&gt;uptime&lt;/code&gt;, &lt;code&gt;free&lt;/code&gt;, &lt;code&gt;df&lt;/code&gt;, &lt;code&gt;ps&lt;/code&gt;, &lt;code&gt;ip&lt;/code&gt;, &lt;code&gt;ss&lt;/code&gt;, &lt;code&gt;systemctl&lt;/code&gt;, and &lt;code&gt;journalctl&lt;/code&gt;, this script collects those signals together and presents them as one structured snapshot.&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%2F1f0ue09tox0h3coa7k8y.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%2F1f0ue09tox0h3coa7k8y.png" alt=" " width="638" height="849"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The report is organized around the main areas an engineer would typically investigate:&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%2F8qwb2e6b5hpu4f13ob28.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%2F8qwb2e6b5hpu4f13ob28.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script also introduces an important operational concept: time-bounded investigation.&lt;/p&gt;

&lt;p&gt;By default, it collects journal and kernel information from the last 30 minutes, but the window can be changed with &lt;code&gt;--since&lt;/code&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;sudo ./incident-context.sh --since "2 hours ago"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents the report from becoming an uncontrolled dump of historical logs. During an incident, the most useful information is usually the information surrounding the period in which the failure occurred.&lt;/p&gt;

&lt;p&gt;The script can also write the collected information to a report file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ./incident-context.sh --output incident-report.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a reusable incident snapshot: instead of starting an investigation with an empty terminal and a list of commands to remember, the engineer can generate a consistent baseline of the server's state and use it as the starting point for deeper investigation.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Log Window
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://log-window.sh" rel="noopener noreferrer"&gt;&lt;code&gt;log-window.sh&lt;/code&gt;&lt;/a&gt; is a focused log investigation tool for extracting only the log entries that are relevant to an incident.&lt;/p&gt;

&lt;p&gt;When an application produces thousands of log lines, searching the entire file manually can make an investigation unnecessarily difficult. Usually, an engineer already has some context about the problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;when the problem happened,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;what severity the event had, or&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a phrase associated with the failure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of reading the entire log file, this script allows those clues to become filters.&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%2Fhhoam0s6nndqnh1o6gew.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%2Fhhoam0s6nndqnh1o6gew.png" alt=" " width="433" height="87"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script supports four independent filters:&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%2F8th2mfxlmdc83ibal9ic.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%2F8th2mfxlmdc83ibal9ic.png" alt=" " width="799" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These filters can be combined like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./log-window.sh &lt;span class="nt"&gt;--file&lt;/span&gt; ../logs/dummy.log &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--level&lt;/span&gt; WARN &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--contains&lt;/span&gt; &lt;span class="s2"&gt;"Database response time increased"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--start&lt;/span&gt; &lt;span class="s2"&gt;"2026-08-18 10:07:00"&lt;/span&gt;  &lt;span class="nt"&gt;--end&lt;/span&gt; &lt;span class="s2"&gt;"2026-08-19 10:09:00"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Network Path
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://network-path.sh" rel="noopener noreferrer"&gt;&lt;code&gt;network-path.sh&lt;/code&gt;&lt;/a&gt; is a network troubleshooting script that checks connectivity to a destination layer by layer, instead of treating connectivity as a single yes-or-no question.&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%2F51q6i0ss1ronokv88e28.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%2F51q6i0ss1ronokv88e28.png" alt=" " width="631" height="713"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A connection failure can happen at different points. DNS might fail to resolve the hostname, the routing table might not provide a valid path, the TCP port might be unreachable, or an application-layer problem might occur after the connection succeeds.&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%2Fl5adbbciqug1kxmsolvp.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%2Fl5adbbciqug1kxmsolvp.png" alt=" " width="799" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If DNS fails, there is no reason to continue testing TCP. If DNS and routing succeed but TCP fails, the investigation can focus on connectivity, firewall rules, security groups, or the destination service rather than the application itself.&lt;/p&gt;

&lt;p&gt;The script therefore produces a more useful result than simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connection failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can identify a failure boundary 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;DNS      PASS
Route    PASS
TCP      FAIL
TLS      SKIP
HTTP     SKIP

Failure detected at: TCP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script requires a destination host and TCP port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./network-path.sh &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--host&lt;/span&gt; google.com &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--port&lt;/span&gt; 443 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--https&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Process Audit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://process-audit.sh" rel="noopener noreferrer"&gt;&lt;code&gt;process-audit.sh&lt;/code&gt;&lt;/a&gt; provides a deeper view of how running processes are consuming system resources.&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%2Fioo1ols50pyt48502m6f.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%2Fioo1ols50pyt48502m6f.png" alt=" " width="551" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Commands such as &lt;code&gt;top&lt;/code&gt; and &lt;code&gt;ps&lt;/code&gt; are excellent for quickly identifying CPU or memory usage, but during troubleshooting we often need more context. A process consuming resources may also have an unusually high number of file descriptors, threads, context switches, or a large resident memory footprint.&lt;/p&gt;

&lt;p&gt;This script collects those signals for every accessible process and turns them into a structured resource audit.&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%2Flnlnrvi1sz6w4ps4nyk7.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%2Flnlnrvi1sz6w4ps4nyk7.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script can rank processes by different resource dimensions:&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;sudo&lt;/span&gt; ./process-audit.sh &lt;span class="nt"&gt;--sort&lt;/span&gt; cpu
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./process-audit.sh &lt;span class="nt"&gt;--sort&lt;/span&gt; memory
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./process-audit.sh &lt;span class="nt"&gt;--sort&lt;/span&gt; fd
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./process-audit.sh &lt;span class="nt"&gt;--sort&lt;/span&gt; threads
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. SSH Access Audit
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://ssh-access-audit.sh" rel="noopener noreferrer"&gt;&lt;code&gt;ssh-access-audit.sh&lt;/code&gt;&lt;/a&gt; is a read-only security audit tool for examining how users can access a Linux server through SSH and what privileges they have after gaining access.&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%2Ffhl7suv8ie2nxnoynhfv.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%2Ffhl7suv8ie2nxnoynhfv.png" alt=" " width="435" height="721"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SSH is often the primary entry point for administrators and automation. Because of that, troubleshooting SSH access should not only ask:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Is SSH running?&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It should also answer:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Who can access the server, how can they authenticate, what keys are authorized, and what privileges do those users have?&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The script approaches SSH access from several 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%2Fhnw69ngyxwyg4d3clhjm.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%2Fhnw69ngyxwyg4d3clhjm.png" alt=" " width="800" height="1054"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script evaluates the effective SSH configuration using &lt;code&gt;sshd -T&lt;/code&gt;, covering authentication settings such as root login, password authentication, public-key authentication, and keyboard-interactive authentication.&lt;/p&gt;

&lt;p&gt;Finally, findings are classified as &lt;code&gt;HIGH&lt;/code&gt;, &lt;code&gt;MEDIUM&lt;/code&gt;, or &lt;code&gt;INFO&lt;/code&gt; so critical security issues can be prioritized.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. System Cleanup
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://system-cleanup.sh" rel="noopener noreferrer"&gt;&lt;code&gt;system-cleanup.sh&lt;/code&gt;&lt;/a&gt; is a safe filesystem cleanup tool that identifies files that may be consuming unnecessary disk space and removes them only after explicit confirmation.&lt;/p&gt;

&lt;p&gt;Disk cleanup is inherently risky because deleting the wrong file can cause service failures or data loss. Instead of immediately running destructive commands such as &lt;code&gt;rm -rf&lt;/code&gt;, the script separates cleanup into two phases:&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%2F4pqmcsqu7godeal4d35a.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%2F4pqmcsqu7godeal4d35a.png" alt=" " width="800" height="76"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It looks for cleanup candidates in common locations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/tmp&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/var/tmp&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;APT package cache&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Old compressed logs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Systemd journal usage&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this script, engineers don't need to manually search for and delete files, reducing the risk of accidentally removing important data. The script only deletes discovered candidates from predefined safe locations after explicit confirmation.&lt;/p&gt;

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

&lt;p&gt;These 7 bash scripts turn common Linux administration tasks into a practical operational toolkit.&lt;/p&gt;

&lt;p&gt;Each script provides a focused capability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Disk Investigator&lt;/strong&gt; helps identify what is consuming storage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Incident Context&lt;/strong&gt; provides a structured view of the server state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Log Window&lt;/strong&gt; makes relevant events easier to isolate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Network Path&lt;/strong&gt; helps pinpoint where connectivity breaks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Process Audit&lt;/strong&gt; reveals which processes are consuming system resources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SSH Access Audit&lt;/strong&gt; exposes potential access and privilege risks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;System Cleanup&lt;/strong&gt; makes disk cleanup safer and more controlled.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest benefit comes from using them together. Instead of relying on individual commands and ad-hoc procedures, engineers have a consistent set of tools for investigation, diagnosis, security auditing, and cleanup.&lt;/p&gt;

&lt;p&gt;More importantly, these scripts can become building blocks for larger operational practices. They can be extended with additional checks, integrated into automation, or adapted to the specific standards of an organization's infrastructure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A command that you run once is useful.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A well designed script that consistently performs the same set of commands is an operational asset.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can find the source code for this article in my github repository:&lt;br&gt;
&lt;a href="https://github.com/muhammadyulasfipahrizal/7-bash-scripts.git" rel="noopener noreferrer"&gt;Github Repository&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building AWS Infrastructure The Right Way With Terraform</title>
      <dc:creator>Mo Rizal</dc:creator>
      <pubDate>Sat, 15 Aug 2026 07:10:44 +0000</pubDate>
      <link>https://dev.to/morizal/building-aws-infrastructure-the-right-way-with-terraform-5a4j</link>
      <guid>https://dev.to/morizal/building-aws-infrastructure-the-right-way-with-terraform-5a4j</guid>
      <description>&lt;p&gt;Provisioning AWS infrastructure is easy when there are only a few resources.&lt;/p&gt;

&lt;p&gt;Open the AWS Console, create a VPC, add Subnets, configure Security Group, launch EC2 instance, etc.&lt;/p&gt;

&lt;p&gt;The problem starts when the infrastructure grows.&lt;/p&gt;

&lt;p&gt;A few manually created resources can quickly become dozens of resources with different configurations. One environment may have different network settings, another may contain a slightly different security rule.&lt;/p&gt;

&lt;p&gt;Over time, the infrastructure becomes difficult to understand and even harder to reproduce.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The problem is not only how to create AWS resources, but how to structure them so the infrastructure does not become a mess.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where Terraform becomes valuable.&lt;/p&gt;

&lt;p&gt;Instead of managing infrastructure through the AWS Console, we can define it as code, store it in Git, review changes, and reproduce the same architecture across environments.&lt;/p&gt;

&lt;p&gt;In this project, we will build a small production style AWS setup using Terraform.&lt;/p&gt;

&lt;p&gt;This project will include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;VPC &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Public and private subnets&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Internet gateway&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Route tables&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security group&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EC2 instance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;IAM Role&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;S3 bucket&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Terraform remote state&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;But the infrastructure itself is only part of the project.&lt;/p&gt;

&lt;p&gt;The main goal is to apply several Terraform design principles that become increasingly important as infrastructure grows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Modularization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Environment Separation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Variables Instead of Hardcoding&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Outputs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;State Management&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Instead of managing AWS infrastructure manually, define it as reusable Terraform code that can be reviewed, reproduced, and maintained.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Before writing Terraform code, let's define the architecture we want to build.&lt;/p&gt;

&lt;p&gt;The final environment contains a VPC with public and private subnets. The EC2 instance runs in the public subnet so we can connect to it for validation.&lt;/p&gt;

&lt;p&gt;The private subnet is included to demonstrate basic network separation and provide a foundation for expanding the architecture later if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final 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%2Fv85s0hnjpbekbn6til4y.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%2Fv85s0hnjpbekbn6til4y.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The EC2 instance will be placed in the public subnet while the private subnet is included to demonstrate a basic network separation pattern&lt;/p&gt;

&lt;p&gt;The network uses:&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;VPC              10.0.0.0/16&lt;/span&gt;
&lt;span class="s"&gt;Public Subnet    10.0.1.0/24&lt;/span&gt;
&lt;span class="s"&gt;Private Subnet   10.0.2.0/24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The EC2 instance is also associated with an IAM role so workloads can interact with AWS services without storing long lived AWS credentials on the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform 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%2Fqbvqk5lew56pc58vqtin.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%2Fqbvqk5lew56pc58vqtin.png" alt=" " width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Terraform configuration is divided into three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bootstrap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;bootstrap&lt;/code&gt; directory is responsible for creating the resources required before the main Terraform environments can use remote state.&lt;/p&gt;

&lt;p&gt;In this project, that means creating the S3 bucket used for Terraform state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;environments&lt;/code&gt; directory contains environment specific configurations.&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%2F7t5e8oway0dxk2aewmqj.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%2F7t5e8oway0dxk2aewmqj.png" alt=" " width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The environments are not responsible for implementing every AWS resource themselves. Instead, they will compose reusable modules.&lt;/p&gt;

&lt;p&gt;For example, the dev environment can use:&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%2F1heauxp5jp0a0trha8la.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%2F1heauxp5jp0a0trha8la.png" alt=" " width="798" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same modules can then be reused by the prod environment with different variables.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This allows the same infrastructure modules to be reused with different values for development and production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Modules&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;modules&lt;/code&gt; directory contains reusable infrastructure components.&lt;/p&gt;

&lt;p&gt;Each module has a specific responsibility.&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;modules/vpc/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;contains the logic for creating the VPC and its networking components.&lt;/p&gt;

&lt;p&gt;Meanwhile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules/ec2/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;contains the logic for creating the EC2 instance.&lt;/p&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%2F74w3yrpk7gews7hum8yn.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%2F74w3yrpk7gews7hum8yn.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The module files follow Terraform conventional structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://main.tf" rel="noopener noreferrer"&gt;&lt;code&gt;main.tf&lt;/code&gt;&lt;/a&gt; — resources&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://variables.tf" rel="noopener noreferrer"&gt;&lt;code&gt;variables.tf&lt;/code&gt;&lt;/a&gt; — inputs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://outputs.tf" rel="noopener noreferrer"&gt;&lt;code&gt;outputs.tf&lt;/code&gt;&lt;/a&gt; — values exposed for othe configurations&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The environment directories additionally contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="http://backend.tf" rel="noopener noreferrer"&gt;&lt;code&gt;backend.tf&lt;/code&gt;&lt;/a&gt; — remote state configuration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;terraform.tfvars&lt;/code&gt;— environment values&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest of the article will use the &lt;strong&gt;dev environment&lt;/strong&gt; to provision the infrastructure.&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 Terraform 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/terraform-setup" rel="noopener noreferrer"&gt;https://github.com/muhammadyulasfipahrizal/terraform-setup&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Configure Remote State
&lt;/h2&gt;

&lt;p&gt;Before provisioning the AWS infrastructure, we need to solve one problem&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Where should Terraform store its state?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Terraform uses a state file to keep track of the infrastructure it manages. By default, this state is stored locally as:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;For a small personal project, local state may be enough.&lt;/p&gt;

&lt;p&gt;However, once infrastructure is shared between environments, machines, and engineers, relying on a local state file becomes difficult to manage.&lt;/p&gt;

&lt;p&gt;A better approach is to store the state remotely in AWS S3.&lt;/p&gt;

&lt;p&gt;In this project, we use a separate bootstrap configuration to create the S3 bucket that will later be used as the Terraform backend.&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%2F11ecnw3junfbi6r820l4.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%2F11ecnw3junfbi6r820l4.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Bootstrap
&lt;/h3&gt;

&lt;p&gt;The purpose of this configuration is to create the S3 bucket required by the Terraform environments.&lt;/p&gt;

&lt;p&gt;The bootstrap configuration can be initialized and applied independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd bootstrap

terraform init

terraform plan

terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the apply completes, the S3 bucket required for remote state exists.&lt;/p&gt;

&lt;p&gt;We can now configure the dev environment to use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure the Dev Backend
&lt;/h3&gt;

&lt;p&gt;The dev environment contains a &lt;a href="http://backend.tf" rel="noopener noreferrer"&gt;&lt;code&gt;backend.tf&lt;/code&gt;&lt;/a&gt; file:&lt;/p&gt;

&lt;p&gt;The backend configuration tells Terraform where its state should be stored.&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;terraform {&lt;/span&gt;
  &lt;span class="s"&gt;backend "s3" {&lt;/span&gt;
    &lt;span class="s"&gt;bucket = "terraform-right-way"&lt;/span&gt;
    &lt;span class="s"&gt;key    = "dev/terraform.tfstate"&lt;/span&gt;
    &lt;span class="s"&gt;region = "ap-southeast-3"&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the &lt;code&gt;key&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;key = "dev/terraform.tfstate"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows different environments to maintain separate state locations within the same bucket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialize the Dev Environment
&lt;/h3&gt;

&lt;p&gt;Now move into the dev environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd environments/dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform reads &lt;a href="http://backend.tf" rel="noopener noreferrer"&gt;&lt;code&gt;backend.tf&lt;/code&gt;&lt;/a&gt; and configures the S3 backend.&lt;/p&gt;

&lt;p&gt;If Terraform was previously using local state, it may ask whether the existing state should be migrated to the new backend.&lt;/p&gt;

&lt;p&gt;For a new environment with no existing infrastructure, there is normally no local state to migrate.&lt;/p&gt;

&lt;p&gt;After initialization, Terraform is ready to use the S3 backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Bootstrap Is Separate
&lt;/h3&gt;

&lt;p&gt;There is a small dependency problem when using S3 as the Terraform backend:&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%2Fbgxinbposqhz0qpke151.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%2Fbgxinbposqhz0qpke151.png" alt=" " width="800" height="71"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We solve this by creating the state bucket through a separate bootstrap configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  VPC
&lt;/h2&gt;

&lt;p&gt;The first infrastructure component is the VPC, which provides the network boundary for our AWS resources.&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;VPC              10.0.0.0/16&lt;/span&gt;
&lt;span class="s"&gt;Public Subnet    10.0.1.0/24&lt;/span&gt;
&lt;span class="s"&gt;Private Subnet   10.0.2.0/24&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module creates the VPC, subnets, Internet Gateway, and route tables.&lt;/p&gt;

&lt;p&gt;The public subnet is associated with a route table that sends Internet bound traffic through the Internet Gateway. The private subnet does not have a direct Internet route.&lt;/p&gt;

&lt;p&gt;The module exposes the values required by other parts of the infrastructure:&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;output "vpc_id" {&lt;/span&gt;
  &lt;span class="s"&gt;value = aws_vpc.this.id&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;output "public_subnet_id" {&lt;/span&gt;
  &lt;span class="s"&gt;value = aws_subnet.public.id&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;output "private_subnet_id" {&lt;/span&gt;
  &lt;span class="s"&gt;value = aws_subnet.private.id&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dev environment then consumes the module:&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;module "vpc" {&lt;/span&gt;
  &lt;span class="s"&gt;source = "../../modules/vpc"&lt;/span&gt;

  &lt;span class="s"&gt;vpc_cidr            = var.vpc_cidr&lt;/span&gt;
  &lt;span class="s"&gt;public_subnet_cidr  = var.public_subnet_cidr&lt;/span&gt;
  &lt;span class="s"&gt;private_subnet_cidr = var.private_subnet_cidr&lt;/span&gt;
  &lt;span class="s"&gt;availability_zone   = var.availability_zone&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important design decision is that the CIDR ranges are variables, rather than being hardcoded directly into the environment's resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the Security Group
&lt;/h2&gt;

&lt;p&gt;The Security Group will be attached to the EC2 instance and will define the allowed inbound traffic.&lt;/p&gt;

&lt;p&gt;The main rule we need is SSH access to the EC2 instance.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding an IP address inside the module, the allowed SSH source is provided as a variable:&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;variable "allowed_ssh_cidr" {&lt;/span&gt;
  &lt;span class="s"&gt;description = "CIDR block allowed to access SSH"&lt;/span&gt;
  &lt;span class="s"&gt;type        = string&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Security Group can then use that value for its SSH rule:&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;ingress {&lt;/span&gt;
  &lt;span class="s"&gt;description = "Allow SSH"&lt;/span&gt;
  &lt;span class="s"&gt;protocol    = "tcp"&lt;/span&gt;
  &lt;span class="s"&gt;from_port   = &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;
  &lt;span class="s"&gt;to_port     = &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;
  &lt;span class="s"&gt;cidr_blocks = [var.allowed_ssh_cidr]&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an important design decision.&lt;/p&gt;

&lt;p&gt;We don't want the module to assume that SSH should always be accessible from a particular IP address. The module defines the rule, while the environment decides who should be allowed to use it.&lt;/p&gt;

&lt;p&gt;For example, the dev environment can provide:&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;allowed_ssh_cidr = "YOUR_IP/32"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;/32&lt;/code&gt; limits SSH access to a single public IP address.&lt;/p&gt;

&lt;p&gt;The Security Group also needs to allow outbound traffic so the EC2 instance can communicate with external services when required.&lt;/p&gt;

&lt;p&gt;Once created, the module exposes the Security Group ID:&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;output "security_group_id" {&lt;/span&gt;
  &lt;span class="s"&gt;value = aws_security_group.this.id&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dev environment can then pass the output to the EC2 module:&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;module "ec2" {&lt;/span&gt;
  &lt;span class="s"&gt;source = "../../modules/ec2"&lt;/span&gt;

  &lt;span class="s"&gt;subnet_id         = module.vpc.public_subnet_id&lt;/span&gt;
  &lt;span class="s"&gt;security_group_id = module.security_group.security_group_id&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates another simple dependency between our modules without coupling their implementations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy EC2
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;modules/ec2/&lt;/code&gt; configuration provides the values required by the module, such as the AMI, instance type, subnet, Security Group, and SSH key.&lt;/p&gt;

&lt;p&gt;The module then connects the EC2 instance to the infrastructure we created earlier:&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;module "ec2" {&lt;/span&gt;
  &lt;span class="s"&gt;source = "../../modules/ec2"&lt;/span&gt;

  &lt;span class="s"&gt;subnet_id         = module.vpc.public_subnet_id&lt;/span&gt;
  &lt;span class="s"&gt;security_group_id = module.security_group.security_group_id&lt;/span&gt;

  &lt;span class="s"&gt;ami_id            = var.ami_id&lt;/span&gt;
  &lt;span class="s"&gt;instance_type     = var.instance_type&lt;/span&gt;
  &lt;span class="s"&gt;key_name          = var.key_name&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part here is how the dependencies are connected.&lt;/p&gt;

&lt;p&gt;The EC2 instance does not need to know how the VPC or Security Group is implemented. It only consumes their outputs:&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;module.vpc.public_subnet_id&lt;/span&gt;
&lt;span class="s"&gt;module.security_group.security_group_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual EC2 resource remains inside the module:&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;resource "aws_instance" "this" {&lt;/span&gt;
  &lt;span class="s"&gt;ami                    = var.ami_id&lt;/span&gt;
  &lt;span class="s"&gt;instance_type          = var.instance_type&lt;/span&gt;
  &lt;span class="s"&gt;subnet_id              = var.subnet_id&lt;/span&gt;
  &lt;span class="s"&gt;vpc_security_group_ids = [var.security_group_id]&lt;/span&gt;
  &lt;span class="s"&gt;key_name               = var.key_name&lt;/span&gt;

  &lt;span class="s"&gt;associate_public_ip_address = &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="s"&gt;tags = {&lt;/span&gt;
    &lt;span class="s"&gt;Name = var.instance_name&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instance is placed in the public subnet and receives a public IP so that we can connect to it and validate the infrastructure after deployment.&lt;/p&gt;

&lt;p&gt;The values that can vary between environments are kept outside the module.&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 yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;instance_type = "t3.micro"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same EC2 module can later be used by production with a different instance type without changing the module itself.&lt;/p&gt;

&lt;p&gt;The module also exposes useful information through outputs:&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;output "instance_id" {&lt;/span&gt;
  &lt;span class="s"&gt;value = aws_instance.this.id&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;output "public_ip" {&lt;/span&gt;
  &lt;span class="s"&gt;value = aws_instance.this.public_ip&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows Terraform to display the instance information after deployment and gives other parts of the configuration a clean interface to consume it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the IAM Role
&lt;/h2&gt;

&lt;p&gt;The EC2 instance may need to interact with other AWS services. A common but unsafe approach would be to store AWS access keys directly on the server.&lt;/p&gt;

&lt;p&gt;Instead, AWS provides &lt;strong&gt;IAM roles for EC2&lt;/strong&gt;, allowing applications running on the instance to obtain temporary credentials automatically.&lt;/p&gt;

&lt;p&gt;The module creates an IAM role with an EC2 trust policy:&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;resource "aws_iam_role" "this" {&lt;/span&gt;
  &lt;span class="s"&gt;name = var.role_name&lt;/span&gt;

  &lt;span class="s"&gt;assume_role_policy = jsonencode({&lt;/span&gt;
    &lt;span class="s"&gt;Version = "2012-10-17"&lt;/span&gt;

    &lt;span class="s"&gt;Statement = [&lt;/span&gt;
      &lt;span class="s"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;Effect = "Allow"&lt;/span&gt;

        &lt;span class="s"&gt;Principal = {&lt;/span&gt;
          &lt;span class="s"&gt;Service = "ec2.amazonaws.com"&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;

        &lt;span class="s"&gt;Action = "sts:AssumeRole"&lt;/span&gt;
      &lt;span class="s"&gt;}&lt;/span&gt;
    &lt;span class="s"&gt;]&lt;/span&gt;
  &lt;span class="s"&gt;})&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the trust relationship:&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%2Fimy6eynvvf3rlg6xpl1w.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%2Fimy6eynvvf3rlg6xpl1w.png" alt=" " width="799" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This allows the EC2 service to assume the role on behalf of the instance.&lt;/p&gt;

&lt;p&gt;The role is then associated with an instance profile:&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;resource "aws_iam_instance_profile" "this" {&lt;/span&gt;
  &lt;span class="s"&gt;name = var.instance_profile_name&lt;/span&gt;
  &lt;span class="s"&gt;role = aws_iam_role.this.name&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instance profile is what allows the IAM role to be attached to the EC2 instance.&lt;/p&gt;

&lt;p&gt;The EC2 module can then receive the instance profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_instance" "this" {
  iam_instance_profile = var.iam_instance_profile
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps AWS credentials out of the server's configuration.&lt;/p&gt;

&lt;p&gt;For this project, the role does not need broad permissions simply because the EC2 instance exists. Permissions should be added according to the actual AWS operations the workload requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the S3 Bucket
&lt;/h2&gt;

&lt;p&gt;It is important to distinguish this bucket from the S3 bucket created during the bootstrap stage.&lt;/p&gt;

&lt;p&gt;The bootstrap bucket is used by Terraform to store remote state, while this S3 bucket is part of the infrastructure managed by the dev environment.&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;resource "aws_s3_bucket" "this" {&lt;/span&gt;
  &lt;span class="s"&gt;bucket = var.bucket_name&lt;/span&gt;

  &lt;span class="s"&gt;tags = {&lt;/span&gt;
    &lt;span class="s"&gt;Name = var.bucket_name&lt;/span&gt;
  &lt;span class="s"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bucket name is provided by the environment rather than being hardcoded inside the module.&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;bucket_name = "dev-server-bucket"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module exposes the bucket information through outputs:&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;output "bucket_id" {&lt;/span&gt;
  &lt;span class="s"&gt;value = aws_s3_bucket.this.id&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;

&lt;span class="s"&gt;output "bucket_arn" {&lt;/span&gt;
  &lt;span class="s"&gt;value = aws_s3_bucket.this.arn&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dev environment can then consume the module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module "s3" {
  source = "../../modules/s3"

  bucket_name = var.bucket_name
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This follows the same pattern used by the other infrastructure components: &lt;strong&gt;the module contains the implementation, while the environment provides the configuration.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate and Deploy
&lt;/h2&gt;

&lt;p&gt;We have now defined the infrastructure as Terraform code. Before creating anything in AWS, we should validate the configuration and review the changes Terraform intends to make.&lt;/p&gt;

&lt;p&gt;For this project, we will deploy the &lt;strong&gt;dev environment&lt;/strong&gt; from:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;environments/dev/&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Format the Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;cd environments/dev&lt;/span&gt;

&lt;span class="s"&gt;terraform fmt -recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the Terraform configuration consistently formatted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate the Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;terraform validate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform checks whether the configuration is syntactically valid and whether the configuration can be successfully loaded.&lt;/p&gt;

&lt;p&gt;A successful validation 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;Success! The configuration is valid.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Review the Execution Plan
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform compares the desired configuration with the current state and shows which resources it intends to create, change, or destroy.&lt;/p&gt;

&lt;p&gt;For a new development environment, we should expect Terraform to plan the creation of our infrastructure.&lt;/p&gt;

&lt;p&gt;This step is important because &lt;code&gt;terraform plan&lt;/code&gt; gives us an opportunity to review the changes before they are applied.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apply the Infrastructure
&lt;/h3&gt;

&lt;p&gt;Once the plan looks correct, apply it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform will display the execution plan again and ask for confirmation.&lt;/p&gt;

&lt;p&gt;Enter:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Terraform will then create the resources defined by the dev environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Structure Matters
&lt;/h2&gt;

&lt;p&gt;At first, this structure may look more complicated than putting everything into one &lt;a href="http://main.tf" rel="noopener noreferrer"&gt;&lt;code&gt;main.tf&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For a small project, that may be true.&lt;/p&gt;

&lt;p&gt;The benefit becomes clearer when the infrastructure grows.&lt;/p&gt;

&lt;p&gt;A new environment can reuse the existing modules:&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;dev  → modules&lt;/span&gt;
&lt;span class="s"&gt;prod → modules&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of duplicating the VPC, EC2, IAM, and S3 implementation, each environment provides its own configuration.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;If the network implementation changes, we know where to look.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If production needs a larger EC2 instance, we change the production configuration rather than duplicating the EC2 module.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If another environment is introduced, it can reuse the existing modules.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Terraform is not difficult because creating an AWS resource is complicated.&lt;/p&gt;

&lt;p&gt;The real challenge is managing infrastructure as it grows.&lt;/p&gt;

&lt;p&gt;A single &lt;a href="http://main.tf" rel="noopener noreferrer"&gt;&lt;code&gt;main.tf&lt;/code&gt;&lt;/a&gt; can work for a small experiment, but infrastructure becomes harder to maintain when environments, networking, compute, IAM, storage, and state management are all mixed together.&lt;/p&gt;

&lt;p&gt;In this project, we applied five core concepts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Modularization&lt;/strong&gt; — infrastructure was divided into reusable modules for networking, security, compute, IAM, and storage instead of keeping everything in a single configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Environment Separation&lt;/strong&gt; — development and production have their own configurations while sharing the same reusable modules.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Variables Instead of Hardcoding&lt;/strong&gt; — values such as CIDR ranges, instance types, AMIs, SSH access, and resource names are provided through variables.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Outputs&lt;/strong&gt; — modules expose the values that other parts of the infrastructure need, allowing resources to be connected without tightly coupling their implementations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;State Management&lt;/strong&gt; — Terraform state is stored remotely in S3, with separate state paths for each environment.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These concepts may seem unnecessary for a small infrastructure project. However, they become increasingly important as the number of resources and environments grows.&lt;/p&gt;

&lt;p&gt;You can find the source code for this article in my github repository:&lt;/p&gt;

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

</description>
    </item>
    <item>
      <title>I built a zero touch ubuntu server provisioning workflow with ansible</title>
      <dc:creator>Mo Rizal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 04:09:47 +0000</pubDate>
      <link>https://dev.to/morizal/-jp0</link>
      <guid>https://dev.to/morizal/-jp0</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/morizal/zero-touch-provisioning-with-ansible-from-0-to-secure-production-ready-server-4ie5" class="crayons-story__hidden-navigation-link"&gt;Zero-Touch Provisioning With Ansible: From 0 To Secure Production Ready Server&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;
              Mo Rizal
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mo Rizal
                
              
              &lt;div id="story-author-preview-content-4365384" 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;Mo Rizal&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/zero-touch-provisioning-with-ansible-from-0-to-secure-production-ready-server-4ie5" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 11&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/zero-touch-provisioning-with-ansible-from-0-to-secure-production-ready-server-4ie5" id="article-link-4365384"&gt;
          Zero-Touch Provisioning With Ansible: From 0 To Secure Production Ready Server
        &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/zero-touch-provisioning-with-ansible-from-0-to-secure-production-ready-server-4ie5" 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/zero-touch-provisioning-with-ansible-from-0-to-secure-production-ready-server-4ie5#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;
            16 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>Zero-Touch Provisioning With Ansible: From 0 To Secure Production Ready Server</title>
      <dc:creator>Mo Rizal</dc:creator>
      <pubDate>Tue, 11 Aug 2026 04:09:01 +0000</pubDate>
      <link>https://dev.to/morizal/zero-touch-provisioning-with-ansible-from-0-to-secure-production-ready-server-4ie5</link>
      <guid>https://dev.to/morizal/zero-touch-provisioning-with-ansible-from-0-to-secure-production-ready-server-4ie5</guid>
      <description>&lt;p&gt;Provisioning a new server is often more complicated than simply installing a few packages.&lt;/p&gt;

&lt;p&gt;A fresh ubuntu server may start with almost nothing configured. An engineer still needs to create users, configure SSH access, set the hostname configure the firewall, and make sure the required services are running correctly.&lt;/p&gt;

&lt;p&gt;When these steps are performed manually, the process can become time-consuming and inconsistent.&lt;/p&gt;

&lt;p&gt;One server might have slightly different configurations from another. A firewall rule might be forgotten, SSH password authentication might remain enabled, or a monitoring agent might not be installed at all.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The problem is not only how to configure a server, but how to configure every server consistently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where infrastructure automation become valuable.&lt;/p&gt;

&lt;p&gt;In this article, we will build a zero-touch server provisioning workflow using ansible, starting from a fresh Ubuntu server and automatically transforming it into a standardized and secured server environment.&lt;/p&gt;

&lt;p&gt;The provisioning process covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Bootstrapping and configuring the server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Applying system security hardening&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Installing and configuring docker&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Setting up system monitoring&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Restricting network access&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Start with a fresh ubuntu server, run the ansible playbook, and let ansible handle the rest&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By the end of the process, the server will be ready to run applications, protected with a basic security baseline, equipped with Docker, and connected to the monitoring infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provisioning Flow
&lt;/h2&gt;

&lt;p&gt;Before looking at the ansible configuration, it is important to understand what we are actually building.&lt;/p&gt;

&lt;p&gt;The goal of this project is not to create a collection of independent Ansible playbooks. The goal is to create a repeatable provisioning workflow that can take a fresh Ubuntu server through several configuration stages until it reaches standardized state.&lt;/p&gt;

&lt;p&gt;The provisioning flow 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%2F8z1wnvxhor8j5yb8ykya.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%2F8z1wnvxhor8j5yb8ykya.png" alt="provisioning flow" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Instead of putting the entire provisioning process into a single playbook, the project separates configuration into several files, each with a specific responsibility.&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%2Fegrb47rvdnkee2b70tff.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%2Fegrb47rvdnkee2b70tff.png" alt="Project Structure" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The easiest way to understand the architecture is to follow how ansible processes a command.&lt;/p&gt;

&lt;p&gt;When we run:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ansible-playbook site.yml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ansible first loads its configuration from &lt;code&gt;ansible.cfg&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="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="s"&gt;inventory = inventory/hosts.ini&lt;/span&gt;
&lt;span class="s"&gt;interpreter_python = auto_silent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This configuration tells Ansible where to find the inventory&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;inventory/hosts.ini&lt;/code&gt; tells ansible which servers it should manage and how to connect to them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="s"&gt;vm1 ansible_host=192.168.122.68&lt;/span&gt;

&lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="s"&gt;ansible_user=root&lt;/span&gt;
&lt;span class="c1"&gt;# ansible_user=devops&lt;/span&gt;
&lt;span class="s"&gt;ansible_ssh_private_key_file=~/.ssh/ansible&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The servers group is important because the playbooks target this group:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hosts: servers&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ansible can therefore apply the same configuration to every server belonging to that group.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;group_vars/servers.yml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This file contains variables that apply to the entire servers group:&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;server_timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Asia/Makassar&lt;/span&gt;
&lt;span class="na"&gt;admin_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;devops&lt;/span&gt;
&lt;span class="na"&gt;server_hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ansible-demo&lt;/span&gt;
&lt;span class="na"&gt;server_ssh_public_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YOUR_SSH_PUBLIC_KEY&lt;/span&gt;
&lt;span class="na"&gt;monitoring_server_ip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;192.168.122.1&lt;/span&gt;
&lt;span class="na"&gt;node_exporter_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.9.1&lt;/span&gt;
&lt;span class="na"&gt;node_exporter_arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux-amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the playbooks to focus on what should be configured, while server-specific values remain outside the playbooks.&lt;/p&gt;

&lt;p&gt;For example, instead of writing:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;name: devops&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;inside a playbook, we use:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;name: "{{ admin_user }}"&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Main Entry Point&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The entire provisioning process is orchestrated through &lt;code&gt;site.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="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;bootstrap server&lt;/span&gt;
  &lt;span class="na"&gt;import_playbook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playbooks/bootstrap.yml&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;configure base system&lt;/span&gt;
  &lt;span class="na"&gt;import_playbook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playbooks/base.yml&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;harden server&lt;/span&gt;
  &lt;span class="na"&gt;import_playbook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playbooks/security.yml&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;install docker&lt;/span&gt;
  &lt;span class="na"&gt;import_playbook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playbooks/docker.yml&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;configure monitoring&lt;/span&gt;
  &lt;span class="na"&gt;import_playbook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playbooks/monitoring.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file does not contain the actual server configuration tasks. Instead, it defines the order in which the provisioning stages are executed.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Bootstrap Playbook
&lt;/h2&gt;

&lt;p&gt;At the start, the server does not have the administrative user that we want to use for day to day management. The initial connection therefore uses the existing &lt;code&gt;root&lt;/code&gt; account.&lt;/p&gt;

&lt;p&gt;The purpose of the bootstrap stage is to establish the foundation required for the rest of the provisioning process.&lt;/p&gt;

&lt;p&gt;The bootstrap playbook performs four main tasks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Creates the administrative user&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Grants the user sudo privileges&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configures SSH key-based authentication&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sets the server hostname&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important part of this stage is that root access is only required for the initial bootstrap.&lt;/p&gt;

&lt;p&gt;Once the &lt;code&gt;devops&lt;/code&gt; user has been created and configured, subsequent playbooks connect to the server using that dedicated administrative account.&lt;/p&gt;

&lt;p&gt;The playbook looks like this:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bootstrap fresh ubuntu server&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;servers&lt;/span&gt;

  &lt;span class="na"&gt;tasks&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;create admin user&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
        &lt;span class="na"&gt;create_home&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;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sudo&lt;/span&gt;
        &lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;configure passwordless sudo&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ALL=(ALL)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;NOPASSWD:ALL&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/sudoers.d/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0440'&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;configure ssh access for admin user&lt;/span&gt;
      &lt;span class="na"&gt;ansible.posix.authorized_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;server_ssh_public_key&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;set server hostname&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;server_hostname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating the Administrative User
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create admin user&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
        &lt;span class="na"&gt;create_home&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;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sudo&lt;/span&gt;
        &lt;span class="na"&gt;append&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 username comes from the variable defined in &lt;code&gt;group_vars/servers.yml&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;admin_user: devops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the playbook does not depend on a specific username.&lt;/p&gt;

&lt;p&gt;Ansible creates the user's home directory and adds the user to the &lt;code&gt;sudo&lt;/code&gt; group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;groups: sudo
append: true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;append: true&lt;/code&gt; option is important because it adds the user to the specified group without removing the user from other existing groups.&lt;/p&gt;

&lt;p&gt;At the end of this task, the server has a dedicated administrative identity that can be used for subsequent configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring Passwordless Sudo
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;configure passwordless sudo&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ALL=(ALL)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;NOPASSWD:ALL&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/sudoers.d/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0440'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of modifying the main &lt;code&gt;/etc/sudoers&lt;/code&gt; file, the configuration is placed inside:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/etc/sudoers.d/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This keeps the custom sudo configuration separated from the system's main sudo configuration.&lt;/p&gt;

&lt;p&gt;The resulting file is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/etc/sudoers.d/devops&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;0440&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The user can now execute administrative commands through sudo without being prompted for a password.&lt;/p&gt;

&lt;p&gt;This is useful for automated provisioning because subsequent ansible tasks need to perform privileged operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring SSH Key Authentication
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;configure ssh access for admin user&lt;/span&gt;
      &lt;span class="s"&gt;ansible.posix.authorized_key&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;server_ssh_public_key&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The public key is provided through:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;server_ssh_public_key: YOUR_SSH_PUBLIC_KEY&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ansible manages the user's authorized_keys file instead of requiring us to manually create it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting the Hostname
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;set server hostname&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;server_hostname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A predictable hostname is useful for identifying the server through monitoring systems, logs, and other infrastructure tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Base Playbook
&lt;/h2&gt;

&lt;p&gt;After the bootstrap stage, the server now has a dedicated administrative user and the basic access configuration required for ansible to continue provisioning it.&lt;/p&gt;

&lt;p&gt;The next step is to establish a consistent baseline for the operating system.&lt;/p&gt;

&lt;p&gt;A fresh Ubuntu installation may contain different package versions, system settings, or filesystem directories depending on how the server was created. Before installing infrastructure components, it is useful to bring the system into a predictable state.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;base.yml&lt;/code&gt; playbook is responsible for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Updating the APT package cache&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Upgrading installed packages&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configuring the server timezone&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creating required application directories&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The playbook looks like this:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;configure base ubuntu server&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;servers&lt;/span&gt;
  &lt;span class="na"&gt;become&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;tasks&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;update apt package cache&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;update_cache&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;cache_valid_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&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;upgrade installed packages&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;upgrade&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&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;configure server timezone&lt;/span&gt;
      &lt;span class="na"&gt;community.general.timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;server_timezone&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create standard application directories&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;directory&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0755'&lt;/span&gt;
      &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apps&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating the APT Package Cache
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;update apt package cache&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;update_cache&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;cache_valid_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;update_cache&lt;/code&gt; option performs the equivalent of:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;cache_valid_time&lt;/code&gt; prevents ansible from unnecessarily updating the package cache on every execution when the cache is still considered valid.&lt;/p&gt;

&lt;p&gt;This is useful when the playbook contains multiple package related tasks because it reduces unnecessary repository requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upgrading the System
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upgrade installed packages&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;upgrade&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is equivalent to performing a distribution-level package upgrade through APT.&lt;/p&gt;

&lt;p&gt;The purpose is to ensure that the server starts the rest of the provisioning process with its existing packages brought up to date.&lt;/p&gt;

&lt;p&gt;This also establishes a more predictable baseline before we install additional components such as docker and monitoring agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring the Timezone
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;configure server timezone&lt;/span&gt;
      &lt;span class="s"&gt;community.general.timezone&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;server_timezone&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consistent timezone configuration is particularly useful for infrastructure because timestamps appear throughout:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;System logs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Application logs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monitoring data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scheduled jobs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Troubleshooting sessions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A consistent timezone makes these timestamps easier to correlate when investigating an issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the Application Directory
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create standard application directories&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;directory&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0755'&lt;/span&gt;
  &lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apps&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rather than allowing application files to be placed arbitrarily throughout the filesystem, the provisioning process establishes a predictable location:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/devops/apps
/home/devops/monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The directory is owned by the administrative user defined in our variables:&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;admin_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;devops&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us a standardized location that can later be used when deploying applications or additional infrastructure components.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Security Playbook
&lt;/h2&gt;

&lt;p&gt;The server now has a predictable operating system baseline.&lt;/p&gt;

&lt;p&gt;However, a freshly provisioned server should not be considered secure simply because its packages are up to date. We still need to reduce unnecessary access, protect the SSH service, control network traffic, and add a mechanism to respond to repeated authentication attempts.&lt;/p&gt;

&lt;p&gt;This is the responsibility of the &lt;code&gt;security.yml&lt;/code&gt; playbook.&lt;/p&gt;

&lt;p&gt;The security playbook focuses on four areas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Hardening SSH access&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configuring UFW as the host firewall&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Allowing only the required network traffic&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Installing and enabling Fail2ban&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The playbook looks like this:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;harden ubuntu server&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;servers&lt;/span&gt;
  &lt;span class="na"&gt;become&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;tasks&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;disable root SSH login&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.lineinfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/ssh/sshd_config&lt;/span&gt;
        &lt;span class="na"&gt;regexp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^#?PermitRootLogin'&lt;/span&gt;
        &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PermitRootLogin&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no'&lt;/span&gt;
        &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/usr/sbin/sshd&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-t&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%s'&lt;/span&gt;
      &lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restart ssh&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;disable SSH password authentication&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.lineinfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/ssh/sshd_config&lt;/span&gt;
        &lt;span class="na"&gt;regexp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^#?PasswordAuthentication'&lt;/span&gt;
        &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PasswordAuthentication&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no'&lt;/span&gt;
        &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/usr/sbin/sshd&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-t&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%s'&lt;/span&gt;
      &lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restart ssh&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow SSH through firewall&lt;/span&gt;
      &lt;span class="na"&gt;community.general.ufw&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;22'&lt;/span&gt;
        &lt;span class="na"&gt;proto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tcp&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enable UFW&lt;/span&gt;
      &lt;span class="na"&gt;community.general.ufw&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enabled&lt;/span&gt;
        &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deny&lt;/span&gt;
        &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;incoming&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;install Fail2ban&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt&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;fail2ban&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&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;enable Fail2ban&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fail2ban&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;

  &lt;span class="na"&gt;handlers&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;restart ssh&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restarted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hardening SSH
&lt;/h3&gt;

&lt;p&gt;SSH is one of the primary entry points into a server, so securing it is an important part of the baseline.&lt;/p&gt;

&lt;p&gt;The first task disables direct root login:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disable root SSH login&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.lineinfile&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/ssh/sshd_config&lt;/span&gt;
        &lt;span class="na"&gt;regexp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^#?PermitRootLogin'&lt;/span&gt;
        &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PermitRootLogin&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no'&lt;/span&gt;
        &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/usr/sbin/sshd&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-t&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%s'&lt;/span&gt;
  &lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restart ssh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting configuration is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PermitRootLogin no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means an external SSH connection can no longer authenticate directly as &lt;code&gt;root&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is particularly important because the bootstrap stage has already created the &lt;code&gt;devops&lt;/code&gt; administrative account.&lt;/p&gt;

&lt;p&gt;The intended access model is therefore:&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%2Fh8f99ldt01pmsv4qn3ny.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%2Fh8f99ldt01pmsv4qn3ny.png" alt=" " width="798" height="94"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;rather than:&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%2Favaqxcabucyri1nz4zgg.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%2Favaqxcabucyri1nz4zgg.png" alt=" " width="799" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Disable SSH Password Authentication
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disable SSH password authentication&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.lineinfile&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/ssh/sshd_config&lt;/span&gt;
        &lt;span class="na"&gt;regexp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^#?PasswordAuthentication'&lt;/span&gt;
        &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PasswordAuthentication&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;no'&lt;/span&gt;
        &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/usr/sbin/sshd&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-t&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%s'&lt;/span&gt;
  &lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restart ssh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this configuration, SSH authentication relies on ssh key configured during the bootstrap stage.&lt;/p&gt;

&lt;p&gt;This removes password based SSH authentication from the server's remote access path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validating SSH Configuration
&lt;/h3&gt;

&lt;p&gt;Notice that both SSH configuration tasks include:&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;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/usr/sbin/sshd&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-t'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an important detail.&lt;/p&gt;

&lt;p&gt;Ansible does not immediately apply an invalid SSH configuration. The &lt;code&gt;sshd -t&lt;/code&gt; command validates the configuration before the file is accepted.&lt;/p&gt;

&lt;p&gt;This reduces the risk of provisioning an invalid SSH configuration that could prevent future connections.&lt;/p&gt;

&lt;p&gt;The tasks also use a handler:&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;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restart ssh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SSH service is therefore restarted only when the configuration actually changes.&lt;/p&gt;

&lt;p&gt;This is one of the advantages of using Ansible modules instead of simply executing shell commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing Security Packages
&lt;/h3&gt;

&lt;p&gt;The playbook installs UFW and Fail2ban:&lt;/p&gt;

&lt;p&gt;UFW provides a simple interface for managing the linux host firewall, while Fail2ban can monitor authentication related logs and temporarily block clients that repeatedly fail authentication.&lt;/p&gt;

&lt;p&gt;These tools address different parts of the security baseline:&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%2F1qbivobpxmoflks56efa.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%2F1qbivobpxmoflks56efa.png" alt=" " width="799" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring UFW
&lt;/h3&gt;

&lt;p&gt;The first firewall rule allows SSH:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow ssh&lt;/span&gt;
  &lt;span class="na"&gt;community.general.ufw&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&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;OpenSSH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rule is intentionally configured before enabling UFW.&lt;/p&gt;

&lt;p&gt;The firewall is then enabled with a default-deny policy for incoming traffic:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enable ufw&lt;/span&gt;
  &lt;span class="na"&gt;community.general.ufw&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enabled&lt;/span&gt;
    &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deny&lt;/span&gt;
    &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;incoming&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting security model is essentially:&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%2F8tpywrw9qlrtr8g0hef2.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%2F8tpywrw9qlrtr8g0hef2.png" alt=" " width="800" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This follows the principle of default deny: services should not become reachable simply because they happen to be listening on a network port.&lt;/p&gt;

&lt;p&gt;Additional ports can then be explicitly allowed when they are required by later components of the infrastructure.&lt;/p&gt;

&lt;p&gt;For example, the monitoring stage will later add a specific rule for Node Exporter rather than exposing all ports on the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enabling Fail2ban
&lt;/h3&gt;

&lt;p&gt;The final security task ensures that Fail2ban is enabled and running:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enable fail2ban&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fail2ban&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two important states being configured here:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;ensures that Fail2ban starts automatically when the server boots.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;ensures that the service is running immediately after provisioning.&lt;/p&gt;

&lt;p&gt;This means the server does not need to be manually configured after deployment to activate the security service.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Docker Playbook
&lt;/h2&gt;

&lt;p&gt;With the operating system configured and the initial security baseline in place, the next step is to prepare the server to run containerized workloads.&lt;/p&gt;

&lt;p&gt;For this project, docker is installed through the official docker APT repository rather than relying on the docker package provided by the default ubuntu repositories.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;docker.yml&lt;/code&gt; playbook is responsible for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Installing the required repository prerequisites&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configuring Docker's official APT repository&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Installing Docker Engine and related components&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enabling and starting the Docker service&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Allowing the administrative user to manage Docker&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The playbook looks like this:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install docker on ubuntu server&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;servers&lt;/span&gt;
  &lt;span class="na"&gt;become&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;tasks&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;install docker repository prerequisites&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt&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="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ca-certificates&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
        &lt;span class="na"&gt;update_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;create docker keyring directory&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/apt/keyrings&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;directory&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0755'&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;download docker GPG key&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.get_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://download.docker.com/linux/ubuntu/gpg&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/apt/keyrings/docker.asc&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0644'&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;add docker APT repository&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.deb822_repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
        &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deb&lt;/span&gt;
        &lt;span class="na"&gt;uris&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://download.docker.com/linux/ubuntu&lt;/span&gt;
        &lt;span class="na"&gt;suites&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;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_facts['distribution_release']&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;stable&lt;/span&gt;
        &lt;span class="na"&gt;architectures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
        &lt;span class="na"&gt;signed_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/apt/keyrings/docker.asc&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&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;install docker engine and compose plugin&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt&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="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-ce&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-ce-cli&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;containerd.io&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-buildx-plugin&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-compose-plugin&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
        &lt;span class="na"&gt;update_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;ensure docker service is enabled and running&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&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;add admin user to docker group&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
        &lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installing Repository Prerequisites
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install docker repository prerequisites&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ca-certificates&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
        &lt;span class="na"&gt;update_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ca-certificates&lt;/code&gt; allows the system to properly validate HTTPS certificates, while &lt;code&gt;curl&lt;/code&gt; is used to retrieve the Docker repository signing key.&lt;/p&gt;

&lt;p&gt;These packages provide the basic requirements for securely adding the external repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the APT Keyring Directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/apt/keyrings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;using:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create docker keyring directory&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/apt/keyrings&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;directory&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0755'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;APT repository signing keys are stored separately from the repository configuration.&lt;/p&gt;

&lt;p&gt;This provides a clear location for repository specific signing keys instead of placing them in a global trusted key configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Docker's Repository Signing Key
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;download docker GPG key&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.get_url&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://download.docker.com/linux/ubuntu/gpg&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/apt/keyrings/docker.asc&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0644'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signing key is then referenced by the docker repository configuration.&lt;/p&gt;

&lt;p&gt;This allows APT to verify that packages retrieved from the docker repository are signed by the expected key.&lt;/p&gt;

&lt;p&gt;The important part here is that the playbook does not simply add an arbitrary repository and trust it globally. The repository is explicitly associated with its signing key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring the Docker APT Repository
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add docker APT repository&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.deb822_repository&lt;/span&gt;&lt;span class="err"&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;docker&lt;/span&gt;
        &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deb&lt;/span&gt;
        &lt;span class="na"&gt;uris&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https://download.docker.com/linux/ubuntu&lt;/span&gt;
        &lt;span class="na"&gt;suites&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;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_facts['distribution_release']&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;stable&lt;/span&gt;
        &lt;span class="na"&gt;architectures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
        &lt;span class="na"&gt;signed_by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/apt/keyrings/docker.asc&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One useful detail here 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;suites&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;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_facts['distribution_release']&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of hardcoding an ubuntu release name, ansible obtains the distribution release from the target system.&lt;/p&gt;

&lt;p&gt;This makes the playbook less dependent on a specific ubuntu release.&lt;/p&gt;

&lt;p&gt;The repository is also restricted to the &lt;code&gt;stable&lt;/code&gt; component and &lt;code&gt;amd64&lt;/code&gt; architecture:&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;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;stable&lt;/span&gt;

&lt;span class="na"&gt;architectures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Installing Docker
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install docker engine and compose plugin&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.apt&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="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-ce&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-ce-cli&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;containerd.io&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-buildx-plugin&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-compose-plugin&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
    &lt;span class="na"&gt;update_cache&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;Rather than installing only the docker engine package, the playbook installs the components needed for a practical container environment.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docker-ce&lt;/code&gt; — docker engine&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docker-ce-cli&lt;/code&gt; — docker command line interface&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;containerd.io&lt;/code&gt; — container runtime&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docker-buildx-plugin&lt;/code&gt; — docker image build functionality&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docker-compose-plugin&lt;/code&gt; — docker compose&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives the server the tooling required to build, run, and manage containerized applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ensuring Docker Is Running
&lt;/h3&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ensure docker service is enabled and running&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two desired states here.&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;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;ensures docker starts automatically when the server boots.&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;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ensures docker is running immediately after the provisioning stage.&lt;/p&gt;

&lt;p&gt;This means that once provisioning finishes, the server is already capable of running containers without requiring manual intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Allowing the Administrative User to Manage Docker
&lt;/h3&gt;

&lt;p&gt;The final task adds the administrative user to the docker group:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add admin user to docker group&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;admin_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
        &lt;span class="na"&gt;append&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;This allows the administrative user to interact with the docker daemon without prefixing every docker command with &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, the user can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&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;sudo &lt;/span&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Monitoring Playbook
&lt;/h2&gt;

&lt;p&gt;The server is now configured, hardened, and ready to run containerized workloads.&lt;/p&gt;

&lt;p&gt;There is one final piece missing: &lt;strong&gt;observability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A server can be perfectly configured and still become difficult to operate if we cannot see its resource usage and system health.&lt;/p&gt;

&lt;p&gt;For this project, Prometheus Node Exporter is used to expose hardware and operating system metrics that can be collected by Prometheus.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;monitoring.yml&lt;/code&gt; playbook is responsible for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Creating a dedicated system user for Node Exporter&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Downloading and installing Node Exporter&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Creating a systemd service&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensuring Node Exporter starts automatically&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Restricting access to the metrics endpoint&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The playbook looks like this:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;configure monitoring agent&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;servers&lt;/span&gt;
  &lt;span class="na"&gt;become&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;tasks&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;create node exporter user&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.user&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;node_exporter&lt;/span&gt;
        &lt;span class="na"&gt;system&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;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/sbin/nologin&lt;/span&gt;
        &lt;span class="na"&gt;create_home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;download node exporter&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.get_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/prometheus/node_exporter/releases/download/v{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/node_exporter-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_arch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.tar.gz"&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/node_exporter-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.tar.gz"&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0644'&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;extract node exporter&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.unarchive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/node_exporter-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.tar.gz"&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;
        &lt;span class="na"&gt;remote_src&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;creates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/node_exporter-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_arch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install node exporter binary&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/node_exporter-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_arch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/node_exporter"&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/bin/node_exporter&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0755'&lt;/span&gt;
        &lt;span class="na"&gt;remote_src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;create node exporter systemd service&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/systemd/system/node_exporter.service&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0644'&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;[Unit]&lt;/span&gt;
          &lt;span class="s"&gt;Description=Prometheus Node Exporter&lt;/span&gt;
          &lt;span class="s"&gt;Wants=network-online.target&lt;/span&gt;
          &lt;span class="s"&gt;After=network-online.target&lt;/span&gt;

          &lt;span class="s"&gt;[Service]&lt;/span&gt;
          &lt;span class="s"&gt;User=node_exporter&lt;/span&gt;
          &lt;span class="s"&gt;Group=node_exporter&lt;/span&gt;
          &lt;span class="s"&gt;Type=simple&lt;/span&gt;
          &lt;span class="s"&gt;ExecStart=/usr/local/bin/node_exporter&lt;/span&gt;

          &lt;span class="s"&gt;[Install]&lt;/span&gt;
          &lt;span class="s"&gt;WantedBy=multi-user.target&lt;/span&gt;
      &lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restart node exporter&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;enable and start node exporter&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node_exporter&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
        &lt;span class="na"&gt;daemon_reload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow node exporter from monitoring server&lt;/span&gt;
      &lt;span class="na"&gt;community.general.ufw&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
        &lt;span class="na"&gt;from_ip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;monitoring_server_ip&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;to_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;9100'&lt;/span&gt;
        &lt;span class="na"&gt;proto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tcp&lt;/span&gt;

  &lt;span class="na"&gt;handlers&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;restart node exporter&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node_exporter&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restarted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating a Dedicated System User
&lt;/h3&gt;

&lt;p&gt;The first step is to create a dedicated system user:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create node exporter user&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="err"&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;node_exporter&lt;/span&gt;
        &lt;span class="na"&gt;system&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;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/sbin/nologin&lt;/span&gt;
        &lt;span class="na"&gt;create_home&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node Exporter does not need an interactive shell or a home directory.&lt;/p&gt;

&lt;p&gt;Therefore, instead of running the service as &lt;code&gt;root&lt;/code&gt;, we create a dedicated user with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shell: /usr/sbin/nologin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This follows the principle of least privilege.&lt;/p&gt;

&lt;p&gt;The service only needs to perform its intended function, so there is no reason for it to have an interactive login account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Downloading Node Exporter
&lt;/h3&gt;

&lt;p&gt;The Node Exporter version is controlled through &lt;code&gt;group_vars/servers.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;node_exporter_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.9.1&lt;/span&gt;
&lt;span class="na"&gt;node_exporter_arch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux-amd64&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The playbook uses these variables to construct the download url:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;download node exporter&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.get_url&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://github.com/prometheus/node_exporter/releases/download/v{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/node_exporter-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_arch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.tar.gz"&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/node_exporter-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.tar.gz"&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0644'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we need to upgrade Node Exporter later, the version can be changed in the variables file instead of modifying the playbook itself.&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 yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;node_exporter_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.10.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Extracting the Binary
&lt;/h3&gt;

&lt;p&gt;The downloaded archive is extracted using Ansible's &lt;code&gt;unarchive&lt;/code&gt; module:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;extract node exporter&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.unarchive&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/node_exporter-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.tar.gz"&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;
        &lt;span class="na"&gt;remote_src&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;creates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/node_exporter-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_arch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part here 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;remote_src&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 archive already exists on the target server, so ansible does not need to transfer it from the machine running Ansible.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;creates&lt;/code&gt; parameter also provides an idempotency check.&lt;/p&gt;

&lt;p&gt;If the extracted directory already exists, Ansible does not need to extract the archive again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installing the Node Exporter Binary
&lt;/h3&gt;

&lt;p&gt;The Node Exporter binary is then copied into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/local/bin/node_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;using:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install node exporter binary&lt;/span&gt;
      &lt;span class="s"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tmp/node_exporter-{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;node_exporter_arch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/node_exporter"&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/local/bin/node_exporter&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0755'&lt;/span&gt;
        &lt;span class="na"&gt;remote_src&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 binary is owned by &lt;code&gt;root&lt;/code&gt;, while the service itself will run as the dedicated &lt;code&gt;node_exporter&lt;/code&gt; user.&lt;/p&gt;

&lt;p&gt;This creates a separation between the executable and the account that executes it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the systemd Service
&lt;/h3&gt;

&lt;p&gt;Installing the binary alone does not make Node Exporter a managed system service.&lt;/p&gt;

&lt;p&gt;The playbook therefore creates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/systemd/system/node_exporter.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Prometheus Node Exporter&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;node_exporter&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;node_exporter&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;simple&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/node_exporter&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enabling and Starting Node Exporter
&lt;/h3&gt;

&lt;p&gt;The service is then enabled and started:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enable and start node exporter&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node_exporter&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
    &lt;span class="na"&gt;daemon_reload&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 &lt;code&gt;daemon_reload&lt;/code&gt; option tells systemd to reload its service definitions after Ansible creates the new unit file.&lt;/p&gt;

&lt;p&gt;The two important states are:&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;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first ensures Node Exporter starts automatically after a reboot.&lt;/p&gt;

&lt;p&gt;The second ensures it is already running when provisioning finishes.&lt;/p&gt;

&lt;p&gt;At this point, Node Exporter exposes its metrics endpoint on the standard port:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Restricting Monitoring Access
&lt;/h3&gt;

&lt;p&gt;The metrics endpoint contains information about the server's operating system and resource usage, so there is no reason to expose it to every host that can reach the server.&lt;/p&gt;

&lt;p&gt;The playbook therefore adds a specific UFW rule:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow node exporter from monitoring server&lt;/span&gt;
      &lt;span class="s"&gt;community.general.ufw&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
        &lt;span class="na"&gt;from_ip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;monitoring_server_ip&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;to_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;9100'&lt;/span&gt;
        &lt;span class="na"&gt;proto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tcp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The monitoring server address is defined in:&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;monitoring_server_ip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;192.168.122.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This results in a much narrower network 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%2Fnqjo86puj0f8a3kzztjy.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%2Fnqjo86puj0f8a3kzztjy.png" alt=" " width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is an important continuation of the security model introduced in the Security Playbook.&lt;/p&gt;

&lt;p&gt;We do not simply open port &lt;code&gt;9100&lt;/code&gt; to the entire network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9100 → 0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, access is explicitly limited to the monitoring server.&lt;/p&gt;

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

&lt;p&gt;Provisioning a server manually may seem simple when there is only one server to configure. But as the number of servers grows, manual configuration quickly becomes difficult to maintain and can lead to configuration drift.&lt;/p&gt;

&lt;p&gt;In this project, we have built an ansible workflow that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Bootstrapping and configuring the server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Applying system security hardening&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Installing and configuring Docker&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Setting up system monitoring&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Restricting network access&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of relying on a sequence of manual commands, the desired server state is now defined as code that can be reviewed, version controlled, reproduced, and applied consistently.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Define the desired state. Automate it. Repeat it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can find the source code for this article in my github repository: &lt;a href="https://github.com/muhammadyulasfipahrizal/zero-touch-ansible" rel="noopener noreferrer"&gt;&lt;strong&gt;https://github.com/muhammadyulasfipahrizal/zero-touch-ansible.git&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
