<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://marufmoinuddin.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://marufmoinuddin.github.io/" rel="alternate" type="text/html" /><updated>2026-07-18T13:06:07+00:00</updated><id>https://marufmoinuddin.github.io/feed.xml</id><title type="html">Moin Uddin Ahmed</title><subtitle>Operations engineer writing about infrastructure, reliability, and systems work.</subtitle><author><name>Moin Uddin Ahmed</name></author><entry><title type="html">A Secure Kubernetes Multi-Master Cluster Setup Guide</title><link href="https://marufmoinuddin.github.io/blog/2026/07/kubernetes-multi-master-cluster-setup-guide/" rel="alternate" type="text/html" title="A Secure Kubernetes Multi-Master Cluster Setup Guide" /><published>2026-07-18T00:00:00+00:00</published><updated>2026-07-18T00:00:00+00:00</updated><id>https://marufmoinuddin.github.io/blog/2026/07/kubernetes-multi-master-cluster-setup-guide</id><content type="html" xml:base="https://marufmoinuddin.github.io/blog/2026/07/kubernetes-multi-master-cluster-setup-guide/"><![CDATA[<h1 id="kubernetes-multi-master-cluster-setup-guide">Kubernetes Multi-Master Cluster Setup Guide</h1>

<blockquote>
  <p><strong>Purpose:</strong> This guide provides <strong>manual, step-by-step instructions</strong> for setting up a production-grade, multi-master Kubernetes cluster. It is designed for operators who want to understand <strong>what</strong> each step does and <strong>why</strong> it is necessary — without relying on Ansible or any automation tool.</p>

  <p><strong>OS Support:</strong> Ubuntu/Debian (apt) and CentOS/RHEL (yum/dnf)</p>
</blockquote>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ol>
  <li><a href="#1-architecture-overview">Architecture Overview</a></li>
  <li><a href="#2-node-planning--prerequisites">Node Planning &amp; Prerequisites</a></li>
  <li><a href="#3-phase-0--cluster-cleanup-nuke">Phase 0 — Cluster Cleanup (Nuke)</a></li>
  <li><a href="#4-phase-1--hostname--hosts-file">Phase 1 — Hostname &amp; Hosts File</a></li>
  <li><a href="#5-phase-2--os-prerequisites">Phase 2 — OS Prerequisites</a></li>
  <li><a href="#6-phase-3--os-hardening-cis-compliance">Phase 3 — OS Hardening (CIS Compliance)</a></li>
  <li><a href="#7-phase-4--initialize-the-first-master-node">Phase 4 — Initialize the First Master Node</a></li>
  <li><a href="#8-phase-5--join-additional-master-nodes">Phase 5 — Join Additional Master Nodes</a></li>
  <li><a href="#9-phase-6--join-worker-nodes">Phase 6 — Join Worker Nodes</a></li>
  <li><a href="#10-phase-7--high-availability-with-keepalived--haproxy">Phase 7 — High Availability with Keepalived + HAProxy</a></li>
  <li><a href="#11-phase-8--certificate-management">Phase 8 — Certificate Management</a></li>
  <li><a href="#12-phase-9--reset-worker-nodes-for-re-joining">Phase 9 — Reset Worker Nodes (for Re-joining)</a></li>
  <li><a href="#13-phase-10--reboot-all-nodes">Phase 10 — Reboot All Nodes</a></li>
  <li><a href="#14-appendix--verification--troubleshooting">Appendix — Verification &amp; Troubleshooting</a></li>
</ol>

<hr />

<h2 id="1-architecture-overview">1. Architecture Overview</h2>

<h3 id="what-we-are-building">What we are building</h3>

<p>A <strong>multi-master Kubernetes cluster</strong> with:</p>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>3 Master Nodes</strong> (control plane)</td>
      <td>Run <code class="language-plaintext highlighter-rouge">kube-apiserver</code>, <code class="language-plaintext highlighter-rouge">kube-controller-manager</code>, <code class="language-plaintext highlighter-rouge">kube-scheduler</code>, <code class="language-plaintext highlighter-rouge">etcd</code></td>
    </tr>
    <tr>
      <td><strong>N Worker Nodes</strong></td>
      <td>Run your application workloads</td>
    </tr>
    <tr>
      <td><strong>Virtual IP (VIP)</strong></td>
      <td>A floating IP that HAProxy + Keepalived manage for high availability</td>
    </tr>
    <tr>
      <td><strong>HAProxy</strong></td>
      <td>Load-balances the Kubernetes API server across all master nodes</td>
    </tr>
    <tr>
      <td><strong>Keepalived</strong></td>
      <td>Provides the Virtual IP (VIP) that floats between master nodes</td>
    </tr>
    <tr>
      <td><strong>Cilium CNI</strong></td>
      <td>Container Network Interface for pod networking</td>
    </tr>
  </tbody>
</table>

<h3 id="traffic-flow">Traffic flow</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre>         ┌──────────────────────────────────────────┐
         │          Virtual IP (VIP)                 │
         │         e.g., 192.168.1.100               │
         └──────────────┬───────────────────────────┘
                        │
          ┌─────────────┼─────────────┐
          ▼             ▼             ▼
     ┌─────────┐  ┌─────────┐  ┌─────────┐
     │ HAProxy │  │ HAProxy │  │ HAProxy │
     │ Master1 │  │ Master2 │  │ Master3 │
     └────┬────┘  └────┬────┘  └────┬────┘
          │            │            │
          └────────────┼────────────┘
                       ▼
              ┌─────────────────┐
              │  kube-apiserver │  (on each master)
              │  :6443          │
              └─────────────────┘
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="network-assumptions">Network assumptions</h3>

<table>
  <thead>
    <tr>
      <th>Setting</th>
      <th>Example Value</th>
      <th>Notes</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Pod CIDR</td>
      <td><code class="language-plaintext highlighter-rouge">10.244.0.0/16</code></td>
      <td>Used by Cilium — must not overlap with host networks</td>
    </tr>
    <tr>
      <td>Service CIDR</td>
      <td><code class="language-plaintext highlighter-rouge">10.96.0.0/12</code></td>
      <td>Default Kubernetes service range</td>
    </tr>
    <tr>
      <td>Docker bridge</td>
      <td><code class="language-plaintext highlighter-rouge">172.30.0.1/24</code></td>
      <td>Isolated bridge for Docker</td>
    </tr>
    <tr>
      <td>Cluster domain</td>
      <td><code class="language-plaintext highlighter-rouge">cluster.local</code></td>
      <td>Default Kubernetes internal DNS domain</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="2-node-planning--prerequisites">2. Node Planning &amp; Prerequisites</h2>

<h3 id="21-node-requirements">2.1 Node requirements</h3>

<p>Before you begin, decide on your node layout. Here is a minimum recommended setup:</p>

<table>
  <thead>
    <tr>
      <th>Node Role</th>
      <th>Hostname</th>
      <th>IP Address</th>
      <th>RAM</th>
      <th>CPU</th>
      <th>Disk</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Master 1</td>
      <td><code class="language-plaintext highlighter-rouge">k8s-master-01</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.1.10</code></td>
      <td>4 GB</td>
      <td>2 vCPU</td>
      <td>40 GB</td>
    </tr>
    <tr>
      <td>Master 2</td>
      <td><code class="language-plaintext highlighter-rouge">k8s-master-02</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.1.11</code></td>
      <td>4 GB</td>
      <td>2 vCPU</td>
      <td>40 GB</td>
    </tr>
    <tr>
      <td>Master 3</td>
      <td><code class="language-plaintext highlighter-rouge">k8s-master-03</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.1.12</code></td>
      <td>4 GB</td>
      <td>2 vCPU</td>
      <td>40 GB</td>
    </tr>
    <tr>
      <td>Worker 1</td>
      <td><code class="language-plaintext highlighter-rouge">k8s-worker-01</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.1.20</code></td>
      <td>8 GB</td>
      <td>4 vCPU</td>
      <td>80 GB</td>
    </tr>
    <tr>
      <td>Worker 2</td>
      <td><code class="language-plaintext highlighter-rouge">k8s-worker-02</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.1.21</code></td>
      <td>8 GB</td>
      <td>4 vCPU</td>
      <td>80 GB</td>
    </tr>
    <tr>
      <td>Worker N</td>
      <td><code class="language-plaintext highlighter-rouge">k8s-worker-NN</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.1.2X</code></td>
      <td>8 GB</td>
      <td>4 vCPU</td>
      <td>80 GB</td>
    </tr>
    <tr>
      <td><strong>VIP</strong></td>
      <td><code class="language-plaintext highlighter-rouge">k8s-api.example.com</code></td>
      <td><code class="language-plaintext highlighter-rouge">192.168.1.100</code></td>
      <td>—</td>
      <td>—</td>
      <td>—</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>Key decisions to make before starting:</strong></p>

  <ol>
    <li><strong>Virtual IP (VIP):</strong> Choose a free IP on your subnet that will float between masters. This is the address <code class="language-plaintext highlighter-rouge">kubectl</code> and workers will use to reach the API server.</li>
    <li><strong>Hostnames:</strong> Decide on a consistent naming scheme. All nodes must be able to resolve each other by hostname.</li>
    <li><strong>Kubernetes version:</strong> Pick a stable version (e.g., <code class="language-plaintext highlighter-rouge">1.29</code>, <code class="language-plaintext highlighter-rouge">1.30</code>, <code class="language-plaintext highlighter-rouge">1.31</code>). All nodes must run the same version.</li>
    <li><strong>Network interface:</strong> Know the primary network interface name (e.g., <code class="language-plaintext highlighter-rouge">eth0</code>, <code class="language-plaintext highlighter-rouge">enp1s0</code>, <code class="language-plaintext highlighter-rouge">ens192</code>). You will need this for Keepalived.</li>
  </ol>
</blockquote>

<h3 id="22-what-to-have-ready">2.2 What to have ready</h3>

<ul>
  <li><strong>SSH access</strong> to all nodes as a user with <code class="language-plaintext highlighter-rouge">sudo</code> privileges</li>
  <li><strong>All nodes can ping each other</strong> (network connectivity)</li>
  <li><strong>Internet access</strong> on all nodes (to download packages)</li>
  <li>A <strong>DNS server</strong> or entries in <code class="language-plaintext highlighter-rouge">/etc/hosts</code> for name resolution (we will set this up)</li>
</ul>

<hr />

<h2 id="3-phase-0--cluster-cleanup-nuke">3. Phase 0 — Cluster Cleanup (Nuke)</h2>

<blockquote>
  <p>⚠️ <strong>Only run this if you are tearing down an existing cluster and starting fresh.</strong>
This will <strong>completely remove</strong> Kubernetes, Docker, containerd, and all their data.</p>
</blockquote>

<h3 id="31-reset-kubernetes">3.1 Reset Kubernetes</h3>

<p>Run this on <strong>every node</strong> (all masters and workers):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="c"># Force reset Kubernetes — removes all pods, configs, and local data</span>
<span class="nb">sudo </span>kubeadm reset <span class="nt">-f</span>

<span class="c"># Remove CNI configuration</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /etc/cni/net.d

<span class="c"># Remove kubeconfig from root and your user</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> ~/.kube
<span class="nb">rm</span> <span class="nt">-rf</span> <span class="nv">$HOME</span>/.kube

<span class="c"># Remove Kubernetes package sources</span>
<span class="nb">sudo rm</span> <span class="nt">-f</span> /etc/apt/sources.list.d/kubernetes.list    <span class="c"># Debian/Ubuntu</span>
<span class="nb">sudo rm</span> <span class="nt">-f</span> /etc/yum.repos.d/kubernetes.repo            <span class="c"># CentOS/RHEL</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="32-unhold-and-remove-kubernetes-packages-debianubuntu">3.2 Unhold and remove Kubernetes packages (Debian/Ubuntu)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c"># Unhold packages so they can be removed</span>
<span class="nb">sudo </span>apt-mark unhold kubeadm kubelet kubectl

<span class="c"># Remove Kubernetes binaries</span>
<span class="nb">sudo </span>apt remove <span class="nt">--purge</span> <span class="nt">-y</span> kubeadm kubelet kubectl
<span class="nb">sudo </span>apt autoremove <span class="nt">-y</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="33-remove-docker-and-containerd">3.3 Remove Docker and containerd</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c"># Remove Docker packages</span>
<span class="nb">sudo </span>apt remove <span class="nt">--purge</span> <span class="nt">-y</span> docker-ce docker-ce-cli containerd.io       <span class="c"># Debian/Ubuntu</span>
<span class="nb">sudo </span>yum remove <span class="nt">-y</span> docker-ce docker-ce-cli containerd.io               <span class="c"># CentOS/RHEL</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /var/lib/docker /var/lib/containerd

<span class="c"># Remove Docker configuration</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /etc/docker
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /etc/containerd
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="34-remove-kubernetes-directories">3.4 Remove Kubernetes directories</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="nb">sudo rm</span> <span class="nt">-rf</span> /etc/kubernetes
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /var/lib/kubelet
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /var/lib/etcd
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /etc/cni
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="35-remove-kubernetes-binaries-from-usrlocalbin">3.5 Remove Kubernetes binaries from /usr/local/bin</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo rm</span> <span class="nt">-f</span> /usr/local/bin/kubeadm /usr/local/bin/kubelet /usr/local/bin/kubectl
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="36-clean-iptables">3.6 Clean iptables</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>iptables <span class="nt">-F</span>
<span class="nb">sudo </span>iptables <span class="nt">-X</span>
<span class="nb">sudo </span>iptables <span class="nt">-t</span> nat <span class="nt">-F</span>
<span class="nb">sudo </span>iptables <span class="nt">-t</span> mangle <span class="nt">-F</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="37-remove-kubernetes-related-sysctl-settings">3.7 Remove Kubernetes-related sysctl settings</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c"># Remove the kubernetes sysctl config file</span>
<span class="nb">sudo rm</span> <span class="nt">-f</span> /etc/sysctl.d/kubernetes.conf

<span class="c"># Apply the change</span>
<span class="nb">sudo </span>sysctl <span class="nt">--system</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="38-optional-reboot">3.8 (Optional) Reboot</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>reboot
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>Why clean up so thoroughly?</strong> A fresh install over a previous cluster can fail with cryptic errors. Certificates, configuration, and CNI state can conflict. It is safer to start clean.</p>
</blockquote>

<hr />

<h2 id="4-phase-1--hostname--hosts-file">4. Phase 1 — Hostname &amp; Hosts File</h2>

<blockquote>
  <p><strong>Why:</strong> Kubernetes uses hostnames to identify nodes. Each node must have a unique, resolvable hostname. The <code class="language-plaintext highlighter-rouge">/etc/hosts</code> file ensures all nodes can find each other even without DNS.</p>
</blockquote>

<h3 id="41-set-hostname">4.1 Set hostname</h3>

<p>Run on <strong>each node</strong> with its unique hostname:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c"># Replace with the actual hostname for this node</span>
<span class="nb">sudo </span>hostnamectl set-hostname k8s-master-01   <span class="c"># On master 1</span>
<span class="nb">sudo </span>hostnamectl set-hostname k8s-master-02   <span class="c"># On master 2</span>
<span class="nb">sudo </span>hostnamectl set-hostname k8s-worker-01   <span class="c"># On worker 1, etc.</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Verify it changed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>hostnamectl status
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="42-configure-etchosts">4.2 Configure /etc/hosts</h3>

<p>Edit <code class="language-plaintext highlighter-rouge">/etc/hosts</code> on <strong>every node</strong> to include ALL nodes and the VIP. This way every node can resolve every other node without a DNS server.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>vi /etc/hosts
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Add entries like this (adapt IPs and hostnames to your environment):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre># Kubernetes cluster nodes
192.168.1.10  k8s-master-01
192.168.1.11  k8s-master-02
192.168.1.12  k8s-master-03
192.168.1.20  k8s-worker-01
192.168.1.21  k8s-worker-02

# Kubernetes VIP (used as API endpoint)
192.168.1.100 k8s-api.example.com

# Local hostname mapping
127.0.1.1     k8s-master-01   # On each node, this should be its own hostname
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Also ensure <code class="language-plaintext highlighter-rouge">127.0.1.1</code> points to the node’s own hostname (this is needed for correct name resolution):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Ensure 127.0.1.1 points to the current node's hostname (NOT localhost)</span>
<span class="nb">echo</span> <span class="s2">"127.0.1.1 </span><span class="si">$(</span><span class="nb">hostname</span><span class="si">)</span><span class="s2">"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/hosts
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>Note:</strong> If you have a working DNS server, you can skip the <code class="language-plaintext highlighter-rouge">/etc/hosts</code> entries and use DNS A records instead. But <code class="language-plaintext highlighter-rouge">/etc/hosts</code> is simpler and more reliable for small clusters.</p>
</blockquote>

<hr />

<h2 id="5-phase-2--os-prerequisites">5. Phase 2 — OS Prerequisites</h2>

<blockquote>
  <p><strong>Why:</strong> Kubernetes and Docker require specific kernel parameters, modules, and settings to function correctly. This phase prepares the operating system on ALL nodes (masters and workers).</p>
</blockquote>

<h3 id="51-update-system-packages-debianubuntu">5.1 Update system packages (Debian/Ubuntu)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt upgrade <span class="nt">-y</span>
<span class="nb">sudo </span>apt autoremove <span class="nt">-y</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="52-update-system-packages-centosrhel">5.2 Update system packages (CentOS/RHEL)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>yum update <span class="nt">-y</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="53-set-selinux-to-permissive-centosrhel-only">5.3 Set SELinux to permissive (CentOS/RHEL only)</h3>

<p>Kubernetes does not fully support SELinux enforcing mode. Set it to permissive:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c"># Temporarily set</span>
<span class="nb">sudo </span>setenforce 0

<span class="c"># Permanently set in config</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/^SELINUX=enforcing/SELINUX=permissive/'</span> /etc/selinux/config
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/^SELINUX=enforcing/SELINUX=permissive/'</span> /etc/sysconfig/selinux

<span class="c"># Verify</span>
getenforce   <span class="c"># Should show "Permissive"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="54-disable-swap">5.4 Disable swap</h3>

<blockquote>
  <p><strong>Why:</strong> The Kubernetes kubelet requires swap to be disabled. With swap enabled, the kubelet will fail to start. Swap can cause unpredictable performance for pods.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c"># Disable swap immediately</span>
<span class="nb">sudo </span>swapoff <span class="nt">-a</span>

<span class="c"># Remove or comment out swap entries in /etc/fstab so it stays off after reboot</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'/ swap / s/^\(.*\)$/#\1/'</span> /etc/fstab

<span class="c"># Verify</span>
free <span class="nt">-m</span>        <span class="c"># Swap should show 0</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="55-load-required-kernel-modules">5.5 Load required kernel modules</h3>

<blockquote>
  <p><strong>Why:</strong> Kubernetes networking relies on <code class="language-plaintext highlighter-rouge">br_netfilter</code> for bridge-netfilter communication, and <code class="language-plaintext highlighter-rouge">overlay</code> for container filesystems. If you use Ceph/Rook storage, you also need <code class="language-plaintext highlighter-rouge">rbd</code> and <code class="language-plaintext highlighter-rouge">ceph</code> modules.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="c"># Load modules immediately</span>
<span class="nb">sudo </span>modprobe overlay
<span class="nb">sudo </span>modprobe br_netfilter
<span class="nb">sudo </span>modprobe bridge

<span class="c"># Create a config file so they load on boot</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
bridge
</span><span class="no">EOF

</span><span class="c"># If using Ceph/Rook, also load these:</span>
<span class="nb">sudo </span>modprobe rbd
<span class="nb">sudo </span>modprobe ceph

<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/modules-load.d/rbd.conf
rbd
</span><span class="no">EOF

</span><span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/modules-load.d/ceph.conf
ceph
</span><span class="no">EOF

</span><span class="c"># Verify modules are loaded</span>
lsmod | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s2">"br_netfilter|overlay|bridge"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="56-configure-sysctl-for-kubernetes-networking">5.6 Configure sysctl for Kubernetes networking</h3>

<blockquote>
  <p><strong>Why:</strong> These sysctl settings enable IP forwarding (required for pod-to-pod communication across nodes) and bridge-netfilter (required for iptables rules to apply to bridged traffic, which is how Kubernetes Services work).</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/sysctl.d/kubernetes.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
</span><span class="no">EOF

</span><span class="c"># Also set in /etc/sysctl.conf for persistence</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'/^net.bridge.bridge-nf-call-iptables=/d'</span> /etc/sysctl.conf
<span class="nb">echo</span> <span class="s2">"net.bridge.bridge-nf-call-iptables=1"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/sysctl.conf
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'/^net.bridge.bridge-nf-call-ip6tables=/d'</span> /etc/sysctl.conf
<span class="nb">echo</span> <span class="s2">"net.bridge.bridge-nf-call-ip6tables=1"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/sysctl.conf

<span class="c"># Apply sysctl settings</span>
<span class="nb">sudo </span>sysctl <span class="nt">--system</span>

<span class="c"># Verify</span>
sysctl net.bridge.bridge-nf-call-iptables net.bridge.bridge-nf-call-ip6tables net.ipv4.ip_forward
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="57-configure-inotify-limits-for-large-clusters">5.7 Configure inotify limits (for large clusters)</h3>

<blockquote>
  <p><strong>Why:</strong> Kubernetes and many applications (e.g., file watchers) use inotify. The default limits are too low for production clusters.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/sysctl.d/99-inotify-limits.conf
fs.inotify.max_user_watches = 524288
fs.inotify.max_user_instances = 1024
fs.inotify.max_queued_events = 16384
</span><span class="no">EOF

</span><span class="nb">sudo </span>sysctl <span class="nt">--system</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="58-configure-file-descriptor-limits">5.8 Configure file descriptor limits</h3>

<blockquote>
  <p><strong>Why:</strong> Kubernetes and Docker handle many concurrent connections. High file descriptor limits prevent “too many open files” errors.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="rouge-code"><pre><span class="c"># Increase system-wide file descriptors</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'/^fs.file-max=/d'</span> /etc/sysctl.conf
<span class="nb">echo</span> <span class="s2">"fs.file-max=999999"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/sysctl.conf
<span class="nb">sudo </span>sysctl <span class="nt">--system</span>

<span class="c"># Add security limits</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee -a /etc/security/limits.conf
*    hard    nofile    999999
*    soft    nofile    256000
</span><span class="no">EOF

</span><span class="c"># Enable PAM limits (Debian/Ubuntu)</span>
<span class="nb">grep</span> <span class="nt">-q</span> <span class="s2">"pam_limits.so"</span> /etc/pam.d/common-session <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"session required pam_limits.so"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/pam.d/common-session
<span class="nb">grep</span> <span class="nt">-q</span> <span class="s2">"pam_limits.so"</span> /etc/pam.d/common-session-noninteractive <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"session required pam_limits.so"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/pam.d/common-session-noninteractive

<span class="c"># Enable PAM limits (CentOS/RHEL)</span>
<span class="nb">grep</span> <span class="nt">-q</span> <span class="s2">"pam_limits.so"</span> /etc/pam.d/system-auth <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"session required pam_limits.so"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/pam.d/system-auth
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="59-configure-systemd-service-limits">5.9 Configure systemd service limits</h3>

<blockquote>
  <p><strong>Why:</strong> Individual systemd services (docker, kubelet, containerd) need their own file descriptor limits.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="c"># Global systemd limits</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/^#DefaultLimitNOFILE=/DefaultLimitNOFILE=999999/'</span> /etc/systemd/system.conf
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/^#DefaultLimitNOFILE=/DefaultLimitNOFILE=999999/'</span> /etc/systemd/user.conf

<span class="c"># Docker service override</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/systemd/system/docker.service.d
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/systemd/system/docker.service.d/override.conf
[Service]
LimitNOFILE=999999
</span><span class="no">EOF

</span><span class="c"># Kubelet service override</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/systemd/system/kubelet.service.d
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/systemd/system/kubelet.service.d/override.conf
[Service]
LimitNOFILE=999999
</span><span class="no">EOF

</span><span class="c"># Containerd service override</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/systemd/system/containerd.service.d
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/systemd/system/containerd.service.d/override.conf
[Service]
LimitNOFILE=999999
</span><span class="no">EOF

</span><span class="c"># Reload systemd</span>
<span class="nb">sudo </span>systemctl daemon-reexec
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="510-disable-firewalls">5.10 Disable firewalls</h3>

<blockquote>
  <p><strong>Why:</strong> Kubernetes manages its own networking rules via iptables/nftables. Having a separate firewall (firewalld, ufw) can conflict and cause connectivity issues.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c"># CentOS/RHEL — disable firewalld</span>
<span class="nb">sudo </span>systemctl stop firewalld
<span class="nb">sudo </span>systemctl disable firewalld

<span class="c"># Debian/Ubuntu — disable ufw</span>
<span class="nb">sudo </span>systemctl stop ufw
<span class="nb">sudo </span>systemctl disable ufw
<span class="nb">sudo </span>ufw disable

<span class="c"># Flush all iptables rules</span>
<span class="nb">sudo </span>iptables <span class="nt">-F</span>
<span class="nb">sudo </span>iptables <span class="nt">-t</span> nat <span class="nt">-F</span>
<span class="nb">sudo </span>iptables <span class="nt">-t</span> mangle <span class="nt">-F</span>
<span class="nb">sudo </span>ip6tables <span class="nt">-F</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p>⚠️ <strong>Security note:</strong> In a production environment, you should configure firewall rules that align with Kubernetes requirements instead of fully disabling the firewall. The required ports are: TCP 6443 (API server), 2379-2380 (etcd), 10250 (kubelet), 30000-32767 (NodePort services). However, for initial setup, disabling the firewall avoids complexity.</p>
</blockquote>

<h3 id="511-install-docker">5.11 Install Docker</h3>

<h4 id="debianubuntu">Debian/Ubuntu</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="c"># Install prerequisites</span>
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> curl ca-certificates

<span class="c"># Create keyrings directory</span>
<span class="nb">sudo install</span> <span class="nt">-m</span> 0755 <span class="nt">-d</span> /etc/apt/keyrings

<span class="c"># Add Docker's GPG key</span>
curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg | <span class="nb">sudo </span>gpg <span class="nt">--dearmor</span> <span class="nt">-o</span> /etc/apt/keyrings/docker.gpg
<span class="nb">sudo chmod </span>a+r /etc/apt/keyrings/docker.gpg

<span class="c"># Add Docker repository</span>
<span class="nb">echo</span> <span class="s2">"deb [arch=</span><span class="si">$(</span>dpkg <span class="nt">--print-architecture</span><span class="si">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu </span><span class="si">$(</span><span class="nb">.</span> /etc/os-release <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="se">\"</span><span class="nv">$VERSION_CODENAME</span><span class="se">\"</span><span class="si">)</span><span class="s2"> stable"</span> | <span class="nb">sudo tee</span> /etc/apt/sources.list.d/docker.list <span class="o">&gt;</span> /dev/null

<span class="c"># Update and install</span>
<span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="centosrhel">CentOS/RHEL</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c"># Install dependencies</span>
<span class="nb">sudo </span>yum <span class="nb">install</span> <span class="nt">-y</span> dnf-plugins-core

<span class="c"># Add Docker repository</span>
<span class="nb">sudo </span>dnf config-manager <span class="nt">--add-repo</span> https://download.docker.com/linux/centos/docker-ce.repo

<span class="c"># Install Docker</span>
<span class="nb">sudo </span>yum <span class="nb">install</span> <span class="nt">-y</span> docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="512-configure-docker-daemon">5.12 Configure Docker daemon</h3>

<blockquote>
  <p><strong>Why:</strong> The Docker daemon configuration isolates the Docker bridge network, disables iptables management (Kubernetes will handle that), and sets log rotation to prevent disks from filling up.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/docker
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/docker/daemon.json
{
  "bip": "172.30.0.1/24",
  "iptables": false,
  "ip-masq": false,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  }
}
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="513-start-and-enable-docker-and-containerd">5.13 Start and enable Docker and containerd</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl <span class="nb">enable </span>containerd
<span class="nb">sudo </span>systemctl start containerd
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>docker
<span class="nb">sudo </span>systemctl start docker
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="514-configure-containerd-to-use-systemd-cgroup-driver">5.14 Configure containerd to use systemd cgroup driver</h3>

<blockquote>
  <p><strong>Why:</strong> Kubernetes recommends the <code class="language-plaintext highlighter-rouge">systemd</code> cgroup driver. By default, containerd uses <code class="language-plaintext highlighter-rouge">cgroupfs</code>. They must match.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c"># Generate default containerd config</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/containerd
containerd config default | <span class="nb">sudo tee</span> /etc/containerd/config.toml

<span class="c"># Enable SystemdCgroup</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/SystemdCgroup = false/SystemdCgroup = true/'</span> /etc/containerd/config.toml

<span class="c"># Restart containerd</span>
<span class="nb">sudo </span>systemctl restart containerd
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="515-centosrhel-only-configure-systemd-resolver">5.15 (CentOS/RHEL only) Configure systemd resolver</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /run/systemd/resolve/
<span class="nb">sudo ln</span> <span class="nt">-sf</span> /run/NetworkManager/resolv.conf /run/systemd/resolve/resolv.conf
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="516-install-kubernetes-components-kubeadm-kubelet-kubectl">5.16 Install Kubernetes components (kubeadm, kubelet, kubectl)</h3>

<blockquote>
  <p><strong>Why:</strong> These three tools form the foundation of a Kubernetes cluster:</p>
  <ul>
    <li><code class="language-plaintext highlighter-rouge">kubeadm</code> — bootstraps the cluster</li>
    <li><code class="language-plaintext highlighter-rouge">kubelet</code> — the node agent that runs on every node</li>
    <li><code class="language-plaintext highlighter-rouge">kubectl</code> — the command-line tool to interact with the cluster</li>
  </ul>
</blockquote>

<h4 id="debianubuntu-1">Debian/Ubuntu</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="c"># Choose your Kubernetes version</span>
<span class="nv">K8S_VERSION</span><span class="o">=</span><span class="s2">"1.31"</span>

<span class="c"># Create keyrings directory</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/apt/keyrings

<span class="c"># Add Kubernetes GPG key</span>
curl <span class="nt">-fsSL</span> https://pkgs.k8s.io/core:/stable:/v<span class="k">${</span><span class="nv">K8S_VERSION</span><span class="k">}</span>/deb/Release.key | <span class="nb">sudo </span>gpg <span class="nt">--dearmor</span> <span class="nt">-o</span> /etc/apt/keyrings/kubernetes-apt-keyring.gpg
<span class="nb">sudo chmod </span>a+r /etc/apt/keyrings/kubernetes-apt-keyring.gpg

<span class="c"># Add Kubernetes repository</span>
<span class="nb">echo</span> <span class="s2">"deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v</span><span class="k">${</span><span class="nv">K8S_VERSION</span><span class="k">}</span><span class="s2">/deb/ /"</span> | <span class="nb">sudo tee</span> /etc/apt/sources.list.d/kubernetes.list

<span class="c"># Update and install</span>
<span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> kubelet kubeadm kubectl

<span class="c"># Hold packages to prevent accidental upgrades</span>
<span class="nb">sudo </span>apt-mark hold kubelet kubeadm kubectl
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="centosrhel-1">CentOS/RHEL</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="nv">K8S_VERSION</span><span class="o">=</span><span class="s2">"1.31"</span>

<span class="c"># Add Kubernetes repository</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v</span><span class="k">${</span><span class="nv">K8S_VERSION</span><span class="k">}</span><span class="sh">/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v</span><span class="k">${</span><span class="nv">K8S_VERSION</span><span class="k">}</span><span class="sh">/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
</span><span class="no">EOF

</span><span class="c"># Install</span>
<span class="nb">sudo </span>yum <span class="nb">install</span> <span class="nt">-y</span> kubelet kubeadm kubectl <span class="nt">--disableexcludes</span><span class="o">=</span>kubernetes
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="517-enable-kubelet-service">5.17 Enable kubelet service</h3>

<blockquote>
  <p><strong>Note:</strong> The kubelet will restart repeatedly until the cluster is initialized. This is normal.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl <span class="nb">enable </span>kubelet
<span class="nb">sudo </span>systemctl start kubelet
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="518-verify-the-installation">5.18 Verify the installation</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c"># Check Docker</span>
docker <span class="nt">--version</span>

<span class="c"># Check Kubernetes components</span>
kubelet <span class="nt">--version</span>
kubeadm version
kubectl version <span class="nt">--client</span>

<span class="c"># Check file descriptor limits</span>
<span class="nb">ulimit</span> <span class="nt">-n</span>
<span class="nb">grep</span> <span class="s2">"nofile"</span> /etc/security/limits.conf

<span class="c"># Check modules</span>
lsmod | <span class="nb">grep</span> <span class="nt">-E</span> <span class="s2">"br_netfilter|overlay"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="6-phase-3--os-hardening-cis-compliance">6. Phase 3 — OS Hardening (CIS Compliance)</h2>

<blockquote>
  <p><strong>Why:</strong> This phase applies security hardening based on CIS (Center for Internet Security) benchmarks. It should be done on ALL master and worker nodes.</p>
</blockquote>

<h3 id="61-install-and-configure-apparmor-cis-131">6.1 Install and configure AppArmor (CIS 1.3.1)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> apparmor apparmor-utils

<span class="c"># Enable AppArmor in GRUB</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/GRUB_CMDLINE_LINUX="\(.*\)"/GRUB_CMDLINE_LINUX="\1 apparmor=1 security=apparmor"/'</span> /etc/default/grub
<span class="nb">sudo </span>update-grub
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="62-restrict-core-dumps-cis-153">6.2 Restrict core dumps (CIS 1.5.3)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c"># Prevent core dumps via limits.conf</span>
<span class="nb">echo</span> <span class="s2">"* hard core 0"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/security/limits.conf

<span class="c"># Disable suid core dumps</span>
<span class="nb">echo</span> <span class="s2">"fs.suid_dumpable = 0"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/sysctl.d/99-cis.conf
<span class="nb">sudo </span>sysctl <span class="nt">-w</span> fs.suid_dumpable<span class="o">=</span>0
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="63-configure-ssh-access-restrictions-cis-514">6.3 Configure SSH access restrictions (CIS 5.1.4)</h3>

<blockquote>
  <p><strong>Why:</strong> Restrict SSH access to specific groups so that only authorized users can SSH into the cluster nodes.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="c"># Create admin groups</span>
<span class="nb">sudo </span>groupadd <span class="nt">--force</span> <span class="nb">sudo
sudo </span>groupadd <span class="nt">--force</span> kubernetes-admin

<span class="c"># Restrict SSH to these groups</span>
<span class="nb">echo</span> <span class="s2">"AllowGroups sudo kubernetes-admin"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/ssh/sshd_config

<span class="c"># Configure strong SSH MAC algorithms</span>
<span class="nb">echo</span> <span class="s2">"MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/ssh/sshd_config

<span class="c"># Disable root login</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/^#PermitRootLogin.*/PermitRootLogin no/'</span> /etc/ssh/sshd_config
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/^PermitRootLogin.*/PermitRootLogin no/'</span> /etc/ssh/sshd_config

<span class="c"># Also check and update included configs</span>
<span class="k">for </span>f <span class="k">in</span> /etc/ssh/sshd_config.d/<span class="k">*</span>.conf<span class="p">;</span> <span class="k">do</span>
  <span class="o">[</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$f</span><span class="s2">"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/^PermitRootLogin.*/PermitRootLogin no/'</span> <span class="s2">"</span><span class="nv">$f</span><span class="s2">"</span>
<span class="k">done</span>

<span class="c"># Restart SSH</span>
<span class="nb">sudo </span>systemctl restart sshd
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="64-configure-login-warning-banner-cis-163">6.4 Configure login warning banner (CIS 1.6.3)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">echo</span> <span class="s2">"Authorized users only. All activity may be monitored and reported."</span> | <span class="nb">sudo tee</span> /etc/issue.net
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="65-configure-sudo-logging-cis-523">6.5 Configure sudo logging (CIS 5.2.3)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">echo</span> <span class="s2">"Defaults logfile=/var/log/sudo.log"</span> | <span class="nb">sudo </span><span class="nv">EDITOR</span><span class="o">=</span><span class="s1">'tee -a'</span> visudo
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="66-configure-password-quality-cis-5332">6.6 Configure password quality (CIS 5.3.3.2)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
</pre></td><td class="rouge-code"><pre><span class="c"># Install pwquality</span>
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> libpam-pwquality

<span class="c"># Configure password quality rules</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee -a /etc/security/pwquality.conf
difok = 3
minlen = 14
dcredit = -1
ucredit = -1
lcredit = -1
ocredit = -1
maxrepeat = 3
maxsequence = 3
dictcheck = 1
</span><span class="no">EOF

</span><span class="c"># Ensure PAM enforces password quality</span>
<span class="nb">grep</span> <span class="nt">-q</span> <span class="s2">"pam_pwquality.so"</span> /etc/pam.d/common-password <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"password requisite pam_pwquality.so retry=3 enforce_for_root"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/pam.d/common-password
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="67-remove-nullok-from-pam-cis-53341">6.7 Remove nullok from PAM (CIS 5.3.3.4.1)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/\(pam_unix\.so.*\)nullok\(.*\)/\1\2/'</span> /etc/pam.d/common-password
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="68-configure-secure-root-path-cis-5425">6.8 Configure secure root PATH (CIS 5.4.2.5)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">echo</span> <span class="s1">'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'</span> | <span class="nb">sudo tee</span> /etc/environment
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="69-configure-shell-session-timeout-cis-5432">6.9 Configure shell session timeout (CIS 5.4.3.2)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee -a /etc/profile

# Set session timeout (15 minutes)
readonly TMOUT=900
export TMOUT
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="610-install-and-configure-aide-cis-611">6.10 Install and configure AIDE (CIS 6.1.1)</h3>

<blockquote>
  <p><strong>Why:</strong> AIDE (Advanced Intrusion Detection Environment) monitors file integrity. It detects unauthorized changes to critical system files.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> aide aide-common

<span class="c"># Initialize AIDE database (this may take a while)</span>
<span class="nb">sudo </span>aideinit

<span class="c"># Move the new database into place</span>
<span class="nb">sudo mv</span> /var/lib/aide/aide.db.new /var/lib/aide/aide.db

<span class="c"># Configure daily AIDE checks</span>
<span class="nb">echo</span> <span class="s2">"0 5 * * * /usr/bin/aide.wrapper --check"</span> | <span class="nb">sudo </span>crontab -
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="611-configure-remote-logging-cis-6212">6.11 Configure remote logging (CIS 6.2.1.2)</h3>

<blockquote>
  <p><strong>Why:</strong> Forward system logs to a central log server for audit and compliance.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> systemd-journal-remote

<span class="c"># Configure journal upload</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/systemd/journal-upload.conf
[Upload]
URL=http://&lt;YOUR-LOG-SERVER-IP&gt;:19532
ServerKeyFile=/etc/ssl/private/journal-upload.pem
ServerCertificateFile=/etc/ssl/certs/journal-upload.pem
TrustedCertificateFile=/etc/ssl/ca/trusted.pem
</span><span class="no">EOF

</span><span class="c"># Enable and start the upload service</span>
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>systemd-journal-upload.service
<span class="nb">sudo </span>systemctl start systemd-journal-upload.service
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>Note:</strong> Replace <code class="language-plaintext highlighter-rouge">&lt;YOUR-LOG-SERVER-IP&gt;</code> with the actual IP of your central log server. The certificate files should be obtained from your CA.</p>
</blockquote>

<h3 id="612-set-permissions-on-kubelet-service-files-after-cluster-init">6.12 Set permissions on kubelet service files (after cluster init)</h3>

<p>Once the cluster is initialized (Phase 4+), come back and run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="c"># Restrict permissions on kubelet service files</span>
<span class="nb">sudo chmod </span>0600 /etc/systemd/system/kubelet.service
<span class="nb">sudo chmod </span>0600 /etc/systemd/system/kubelet.service.d/10-kubeadm.conf

<span class="c"># Restrict kubelet config</span>
<span class="nb">sudo chmod </span>0600 /var/lib/kubelet/config.yaml

<span class="c"># Restrict proxy kubeconfig</span>
<span class="nb">sudo chmod </span>0600 /etc/kubernetes/proxy.conf

<span class="c"># Restrict CA certificate</span>
<span class="nb">sudo chmod </span>0600 /etc/kubernetes/pki/ca.crt

<span class="c"># Restrict kubelet TLS certs</span>
<span class="nb">sudo chmod </span>0600 /var/lib/kubelet/pki/kubelet.crt
<span class="nb">sudo chmod </span>0600 /var/lib/kubelet/pki/kubelet.key
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="7-phase-4--initialize-the-first-master-node">7. Phase 4 — Initialize the First Master Node</h2>

<blockquote>
  <p><strong>Why:</strong> This is the core step — it creates the first control plane node. The <code class="language-plaintext highlighter-rouge">kubeadm init</code> command bootstraps the cluster: it generates certificates, starts the control plane components (<code class="language-plaintext highlighter-rouge">api-server</code>, <code class="language-plaintext highlighter-rouge">controller-manager</code>, <code class="language-plaintext highlighter-rouge">scheduler</code>), and configures <code class="language-plaintext highlighter-rouge">etcd</code>.</p>
</blockquote>

<h3 id="71-prepare-containerd-on-all-nodes">7.1 Prepare containerd (on all nodes)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="c"># Remove any existing containerd config and regenerate</span>
<span class="nb">sudo rm</span> <span class="nt">-f</span> /etc/containerd/config.toml
<span class="nb">sudo </span>containerd config default | <span class="nb">sudo tee</span> /etc/containerd/config.toml

<span class="c"># Enable SystemdCgroup</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s/SystemdCgroup = false/SystemdCgroup = true/'</span> /etc/containerd/config.toml

<span class="c"># Restart containerd and kubelet</span>
<span class="nb">sudo </span>systemctl restart containerd
<span class="nb">sudo </span>systemctl restart kubelet
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="72-create-kubeadm-configuration">7.2 Create kubeadm configuration</h3>

<p>On the <strong>first master node</strong> only, create a kubeadm config file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c"># Create config directory</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/kubernetes

<span class="c"># Create kubeadm config</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/kubernetes/kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: InitConfiguration
---
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
networking:
  podSubnet: 10.244.0.0/16
controlPlaneEndpoint: "k8s-api.example.com:6443"
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>What is <code class="language-plaintext highlighter-rouge">controlPlaneEndpoint</code>?</strong> This is the address (hostname or IP + port) that all nodes will use to reach the API server. In a multi-master cluster, this should be the <strong>VIP</strong> or the first master’s IP. Using a hostname here is better because it allows the VIP to change in the future.</p>
</blockquote>

<h3 id="73-initialize-the-cluster">7.3 Initialize the cluster</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="c"># Initialize certificates CA phase first</span>
<span class="nb">sudo </span>kubeadm init phase certs ca

<span class="c"># Then initialize the cluster</span>
<span class="nb">sudo </span>kubeadm init <span class="se">\</span>
  <span class="nt">--config</span><span class="o">=</span>/etc/kubernetes/kubeadm-config.yaml <span class="se">\</span>
  <span class="nt">--upload-certs</span> 2&gt;&amp;1 | <span class="nb">tee </span>cluster_initialized.log
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>What does <code class="language-plaintext highlighter-rouge">--upload-certs</code> do?</strong> It uploads the control plane certificates to the cluster so that additional master nodes can automatically download them when they join.</p>
</blockquote>

<h3 id="74-set-up-kubeconfig-for-your-user">7.4 Set up kubeconfig for your user</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c"># Create kube directory</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nv">$HOME</span>/.kube

<span class="c"># Copy admin config</span>
<span class="nb">sudo cp</span> /etc/kubernetes/admin.conf <span class="nv">$HOME</span>/.kube/config
<span class="nb">sudo chown</span> <span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span>:<span class="si">$(</span><span class="nb">id</span> <span class="nt">-g</span><span class="si">)</span> <span class="nv">$HOME</span>/.kube/config

<span class="c"># Verify access</span>
kubectl get nodes
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>Why:</strong> By default, <code class="language-plaintext highlighter-rouge">kubectl</code> uses <code class="language-plaintext highlighter-rouge">~/.kube/config</code>. The admin.conf file contains the cluster CA certificate and admin credentials.</p>
</blockquote>

<h3 id="75-install-cilium-cni">7.5 Install Cilium CNI</h3>

<blockquote>
  <p><strong>Why:</strong> Cilium is the Container Network Interface (CNI) plugin that provides pod networking, network policies, and load balancing. We use Cilium version 1.16.4.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="c"># Set architecture</span>
<span class="nv">ARCH</span><span class="o">=</span><span class="si">$(</span><span class="nb">uname</span> <span class="nt">-m</span> | <span class="nb">sed</span> <span class="s1">'s/x86_64/amd64/; s/aarch64/arm64/'</span><span class="si">)</span>
<span class="nv">CILIUM_VERSION</span><span class="o">=</span><span class="s2">"v0.18.2"</span>

<span class="c"># Download Cilium CLI</span>
curl <span class="nt">-L</span> <span class="s2">"https://github.com/cilium/cilium-cli/releases/download/</span><span class="k">${</span><span class="nv">CILIUM_VERSION</span><span class="k">}</span><span class="s2">/cilium-linux-</span><span class="k">${</span><span class="nv">ARCH</span><span class="k">}</span><span class="s2">.tar.gz"</span> <span class="nt">-o</span> /tmp/cilium.tar.gz
curl <span class="nt">-L</span> <span class="s2">"https://github.com/cilium/cilium-cli/releases/download/</span><span class="k">${</span><span class="nv">CILIUM_VERSION</span><span class="k">}</span><span class="s2">/cilium-linux-</span><span class="k">${</span><span class="nv">ARCH</span><span class="k">}</span><span class="s2">.tar.gz.sha256sum"</span> <span class="nt">-o</span> /tmp/cilium.sha256sum

<span class="c"># Verify checksum</span>
<span class="nb">cd</span> /tmp <span class="o">&amp;&amp;</span> <span class="nb">sha256sum</span> <span class="nt">-c</span> cilium.sha256sum

<span class="c"># Extract</span>
<span class="nb">sudo tar </span>xzvf /tmp/cilium.tar.gz <span class="nt">-C</span> /usr/local/bin

<span class="c"># Clean up</span>
<span class="nb">rm</span> <span class="nt">-f</span> /tmp/cilium.tar.gz /tmp/cilium.sha256sum

<span class="c"># Install Cilium in the cluster (may take 2-3 minutes)</span>
cilium <span class="nb">install</span> <span class="nt">--version</span> 1.16.4

<span class="c"># Verify Cilium is running</span>
cilium status <span class="nt">--wait</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="76-wait-for-cluster-readiness">7.6 Wait for cluster readiness</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c"># Wait a moment for everything to settle</span>
<span class="nb">sleep </span>30

<span class="c"># Check cluster status</span>
kubectl get nodes
kubectl get pods <span class="nt">-A</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="77-generate-join-commands-for-additional-masters-and-workers">7.7 Generate join commands for additional masters and workers</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="c"># Generate worker join command (valid for 2 hours)</span>
<span class="nb">echo</span> <span class="s2">"--- Worker Join Command ---"</span>
<span class="nb">sudo </span>kubeadm token create <span class="nt">--print-join-command</span>

<span class="c"># Generate master join command (includes certificate key)</span>
<span class="nb">echo</span> <span class="s2">"--- Master Join Command ---"</span>
<span class="nv">CERT_KEY</span><span class="o">=</span><span class="si">$(</span><span class="nb">sudo </span>kubeadm init phase upload-certs <span class="nt">--upload-certs</span> | <span class="nb">grep</span> <span class="nt">-A</span> 1 <span class="s1">'certificate key'</span> | <span class="nb">tail</span> <span class="nt">-n</span> 1<span class="si">)</span>
<span class="nb">sudo </span>kubeadm token create <span class="nt">--print-join-command</span> <span class="nt">--certificate-key</span> <span class="nv">$CERT_KEY</span>
<span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"Add --control-plane flag to the master join command above"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>Save these commands!</strong> You will use them in Phases 5 and 6. The token expires after 2 hours — if it expires, generate a new one with the same commands.</p>
</blockquote>

<hr />

<h2 id="8-phase-5--join-additional-master-nodes">8. Phase 5 — Join Additional Master Nodes</h2>

<blockquote>
  <p><strong>Why:</strong> For high availability, you need at least 3 master nodes. This ensures that if one master fails, the cluster can still operate (etcd requires a majority: 2 out of 3).</p>
</blockquote>

<h3 id="81-on-each-additional-master-node">8.1 On each additional master node</h3>

<p>On <strong>each additional master</strong> (master-02, master-03, …), run the <strong>master join command</strong> you saved from Phase 7.7.</p>

<p>It will look something like:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>kubeadm <span class="nb">join </span>k8s-api.example.com:6443 <span class="se">\</span>
  <span class="nt">--token</span> &lt;your-token&gt; <span class="se">\</span>
  <span class="nt">--discovery-token-ca-cert-hash</span> sha256:&lt;<span class="nb">hash</span><span class="o">&gt;</span> <span class="se">\</span>
  <span class="nt">--control-plane</span> <span class="se">\</span>
  <span class="nt">--certificate-key</span> &lt;cert-key&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>What does <code class="language-plaintext highlighter-rouge">--control-plane</code> do?</strong> It tells kubeadm to install control plane components (api-server, controller-manager, scheduler, etcd) on this node, making it a master.</p>
</blockquote>

<h3 id="82-set-up-kubeconfig-on-the-new-master">8.2 Set up kubeconfig on the new master</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nv">$HOME</span>/.kube
<span class="nb">sudo cp</span> /etc/kubernetes/admin.conf <span class="nv">$HOME</span>/.kube/config
<span class="nb">sudo chown</span> <span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span>:<span class="si">$(</span><span class="nb">id</span> <span class="nt">-g</span><span class="si">)</span> <span class="nv">$HOME</span>/.kube/config
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="83-verify-from-the-first-master">8.3 Verify from the first master</h3>

<p>On the <strong>first master</strong>, verify the new master has joined:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get nodes
</pre></td></tr></tbody></table></code></pre></div></div>

<p>You should see all master nodes listed.</p>

<hr />

<h2 id="9-phase-6--join-worker-nodes">9. Phase 6 — Join Worker Nodes</h2>

<h3 id="91-prepare-worker-node-skip-if-already-done-in-phase-2">9.1 Prepare worker node (skip if already done in Phase 2)</h3>

<p>If the worker node has not gone through the OS prerequisites, run through Phase 2 first (Docker, containerd, kubelet, sysctl, etc.).</p>

<h3 id="92-on-each-worker-node">9.2 On each worker node</h3>

<p>Run the <strong>worker join command</strong> you saved from Phase 7.7:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>kubeadm <span class="nb">join </span>k8s-api.example.com:6443 <span class="se">\</span>
  <span class="nt">--token</span> &lt;your-token&gt; <span class="se">\</span>
  <span class="nt">--discovery-token-ca-cert-hash</span> sha256:&lt;<span class="nb">hash</span><span class="o">&gt;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>Tip:</strong> If you get errors, try adding <code class="language-plaintext highlighter-rouge">--ignore-preflight-errors=all</code> to bypass non-critical warnings.</p>
</blockquote>

<h3 id="93-verify-from-a-master">9.3 Verify from a master</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get nodes <span class="nt">-o</span> wide
</pre></td></tr></tbody></table></code></pre></div></div>

<p>You should see all nodes listed with status <code class="language-plaintext highlighter-rouge">Ready</code>.</p>

<hr />

<h2 id="10-phase-7--high-availability-with-keepalived--haproxy">10. Phase 7 — High Availability with Keepalived + HAProxy</h2>

<blockquote>
  <p><strong>Why:</strong> Without HA, if the first master fails, the API server becomes unreachable. HAProxy load-balances traffic across all healthy masters, and Keepalived provides a floating Virtual IP (VIP) that automatically moves to a surviving master.</p>
</blockquote>

<h3 id="101-architecture">10.1 Architecture</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre>                     ┌──────────────────┐
                     │   Virtual IP     │
                     │  192.168.1.100   │
                     └────────┬─────────┘
                              │
          ┌───────────────────┼───────────────────┐
          ▼                   ▼                   ▼
   ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
   │  Keepalived  │   │  Keepalived  │   │  Keepalived  │
   │  + HAProxy   │   │  + HAProxy   │   │  + HAProxy   │
   │  Master 1    │   │  Master 2    │   │  Master 3    │
   │  Priority:100│   │  Priority:90 │   │  Priority:80 │
   └──────┬───────┘   └──────┬───────┘   └──────┬───────┘
          │                  │                  │
          └──────────────────┼──────────────────┘
                             ▼
                    ┌─────────────────┐
                    │ kube-apiserver  │
                    │ :6443           │
                    └─────────────────┘
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Keepalived uses VRRP to elect a <strong>MASTER</strong> — the node with the highest priority owns the VIP. If it fails, the next highest priority takes over.</p>

<h3 id="102-install-keepalived-and-haproxy-on-all-masters">10.2 Install Keepalived and HAProxy on all masters</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c"># Debian/Ubuntu</span>
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> keepalived haproxy

<span class="c"># CentOS/RHEL</span>
<span class="nb">sudo </span>yum <span class="nb">install</span> <span class="nt">-y</span> keepalived haproxy
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="103-configure-keepalived">10.3 Configure Keepalived</h3>

<p>Create the configuration on <strong>each master node</strong>. The priority determines which node becomes the initial MASTER.</p>

<h4 id="on-master-1-highest-priority--initial-master">On Master 1 (highest priority — initial MASTER)</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/keepalived/keepalived.conf
vrrp_instance VI_1 {
    state MASTER
    interface enp1s0                  # Replace with your network interface
    virtual_router_id 51
    priority 100                      # Highest priority
    advert_int 1
    virtual_ipaddress {
        192.168.1.100                 # Replace with your chosen VIP
    }
}
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="on-master-2">On Master 2</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/keepalived/keepalived.conf
vrrp_instance VI_1 {
    state BACKUP
    interface enp1s0                  # Replace with your network interface
    virtual_router_id 51
    priority 90                       # Lower than master 1
    advert_int 1
    virtual_ipaddress {
        192.168.1.100                 # Replace with your chosen VIP
    }
}
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="on-master-3">On Master 3</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/keepalived/keepalived.conf
vrrp_instance VI_1 {
    state BACKUP
    interface enp1s0                  # Replace with your network interface
    virtual_router_id 51
    priority 80                       # Lower than master 2
    advert_int 1
    virtual_ipaddress {
        192.168.1.100                 # Replace with your chosen VIP
    }
}
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>What do these settings mean?</strong></p>
  <ul>
    <li><code class="language-plaintext highlighter-rouge">interface</code>: The network interface that will hold the VIP. Find yours with <code class="language-plaintext highlighter-rouge">ip link show</code> (common names: <code class="language-plaintext highlighter-rouge">eth0</code>, <code class="language-plaintext highlighter-rouge">enp1s0</code>, <code class="language-plaintext highlighter-rouge">ens192</code>).</li>
    <li><code class="language-plaintext highlighter-rouge">virtual_router_id</code>: Must be the same across all nodes (51 is a safe default).</li>
    <li><code class="language-plaintext highlighter-rouge">priority</code>: Higher number = more likely to be master. Subtract 10 for each additional master.</li>
    <li><code class="language-plaintext highlighter-rouge">advert_int</code>: How often (in seconds) the master advertises its health.</li>
    <li><code class="language-plaintext highlighter-rouge">virtual_ipaddress</code>: The VIP that will float between masters.</li>
  </ul>
</blockquote>

<h3 id="104-configure-haproxy-on-all-masters">10.4 Configure HAProxy on all masters</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="c"># Get the list of master IPs</span>
<span class="c"># In this example: 192.168.1.10, 192.168.1.11, 192.168.1.12</span>

<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/haproxy/haproxy.cfg
global
    log /dev/log local0
    maxconn 2000
    user haproxy
    group haproxy

defaults
    log     global
    mode    tcp
    timeout connect 10s
    timeout client  1m
    timeout server  1m

frontend kube-apiserver
    bind 192.168.1.100:6443           # VIP:port
    default_backend kube-apiserver-backend

backend kube-apiserver-backend
    mode tcp
    server master-1 192.168.1.10:6443 check
    server master-2 192.168.1.11:6443 check
    server master-3 192.168.1.12:6443 check
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>How HAProxy works:</strong> It listens on the VIP:6443 and forwards traffic to the real API servers on each master. The <code class="language-plaintext highlighter-rouge">check</code> option means HAProxy will automatically remove a master from the pool if it fails.</p>
</blockquote>

<h3 id="105-start-and-enable-keepalived-and-haproxy">10.5 Start and enable Keepalived and HAProxy</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl <span class="nb">enable </span>keepalived
<span class="nb">sudo </span>systemctl start keepalived
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>haproxy
<span class="nb">sudo </span>systemctl start haproxy

<span class="c"># Verify they are running</span>
<span class="nb">sudo </span>systemctl status keepalived
<span class="nb">sudo </span>systemctl status haproxy
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="106-verify-the-vip-is-assigned">10.6 Verify the VIP is assigned</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c"># Check which master currently owns the VIP</span>
ip addr show | <span class="nb">grep </span>192.168.1.100

<span class="c"># You should see the VIP on the MASTER node</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="107-update-the-kubeadm-configmap-with-the-vip">10.7 Update the kubeadm ConfigMap with the VIP</h3>

<p>On the <strong>first master</strong>, update the cluster configuration to use the VIP/hostname:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c"># Get the current ClusterConfiguration</span>
kubectl get cm kubeadm-config <span class="nt">-n</span> kube-system <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.data.ClusterConfiguration}'</span> <span class="o">&gt;</span> /tmp/current-config.yaml

<span class="c"># Update controlPlaneEndpoint to use the VIP hostname</span>
<span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/controlPlaneEndpoint: .*/controlPlaneEndpoint: k8s-api.example.com:6443/"</span> /tmp/current-config.yaml

<span class="c"># Patch the ConfigMap</span>
kubectl patch configmap kubeadm-config <span class="nt">-n</span> kube-system <span class="nt">--patch</span> <span class="s2">"{</span><span class="se">\"</span><span class="s2">data</span><span class="se">\"</span><span class="s2">:{</span><span class="se">\"</span><span class="s2">ClusterConfiguration</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="si">$(</span><span class="nb">cat</span> /tmp/current-config.yaml | <span class="nb">sed</span> <span class="s1">':a;N;$!ba;s/\n/\\n/g'</span><span class="si">)</span><span class="se">\"</span><span class="s2">}}"</span>

<span class="c"># Clean up</span>
<span class="nb">rm</span> /tmp/current-config.yaml
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="108-update-kubeconfig-files-on-all-nodes">10.8 Update kubeconfig files on ALL nodes</h3>

<h4 id="on-master-nodes">On master nodes</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c"># Update all kubeconfig files to use the VIP hostname</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s|server: https://.*:6443|server: https://k8s-api.example.com:6443|'</span> /etc/kubernetes/admin.conf
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s|server: https://.*:6443|server: https://k8s-api.example.com:6443|'</span> /etc/kubernetes/controller-manager.conf
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s|server: https://.*:6443|server: https://k8s-api.example.com:6443|'</span> /etc/kubernetes/scheduler.conf
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s|server: https://.*:6443|server: https://k8s-api.example.com:6443|'</span> <span class="nv">$HOME</span>/.kube/config
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="on-worker-nodes">On worker nodes</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c"># Update kubelet configs to use the VIP hostname</span>
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s|server: https://.*:6443|server: https://k8s-api.example.com:6443|'</span> /etc/kubernetes/kubelet.conf
<span class="nb">sudo sed</span> <span class="nt">-i</span> <span class="s1">'s|server: https://.*:6443|server: https://k8s-api.example.com:6443|'</span> /var/lib/kubelet/kubeconfig
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="109-restart-kubelet-on-all-nodes">10.9 Restart kubelet on ALL nodes</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl restart kubelet
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="1010-regenerate-api-server-certificates-with-sans">10.10 Regenerate API server certificates with SANs</h3>

<p>On the <strong>first master</strong>, regenerate certificates to include the VIP and all master IPs as Subject Alternative Names (SANs):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="rouge-code"><pre><span class="c"># Backup existing certificates</span>
<span class="nb">sudo cp</span> /etc/kubernetes/pki/apiserver.crt /etc/kubernetes/pki/apiserver.crt.bak-<span class="si">$(</span><span class="nb">date</span> +%Y%m%d%H%M%S<span class="si">)</span>
<span class="nb">sudo cp</span> /etc/kubernetes/pki/apiserver.key /etc/kubernetes/pki/apiserver.key.bak-<span class="si">$(</span><span class="nb">date</span> +%Y%m%d%H%M%S<span class="si">)</span>

<span class="c"># Remove old certificates</span>
<span class="nb">sudo rm</span> <span class="nt">-f</span> /etc/kubernetes/pki/apiserver.crt /etc/kubernetes/pki/apiserver.key

<span class="c"># Create certificate config with SANs</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> | sudo tee /root/kubeadm-cert-config.yaml
apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: stable
controlPlaneEndpoint: "k8s-api.example.com:6443"
apiServer:
  certSANs:
    - "192.168.1.100"            # VIP
    - "k8s-api.example.com"       # VIP hostname
    - "10.96.0.1"                 # Kubernetes service IP
    - "kubernetes"
    - "kubernetes.default"
    - "kubernetes.default.svc"
    - "kubernetes.default.svc.cluster.local"
    - "localhost"
    - "127.0.0.1"
    - "192.168.1.10"              # Master 1 IP
    - "192.168.1.11"              # Master 2 IP
    - "192.168.1.12"              # Master 3 IP
</span><span class="no">EOF

</span><span class="c"># Regenerate certificates</span>
<span class="nb">sudo </span>kubeadm init phase certs apiserver <span class="nt">--config</span> /root/kubeadm-cert-config.yaml

<span class="c"># Verify the new certificate includes the VIP</span>
openssl x509 <span class="nt">-in</span> /etc/kubernetes/pki/apiserver.crt <span class="nt">-text</span> | <span class="nb">grep</span> <span class="nt">-A1</span> <span class="s2">"Subject Alternative Name"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>Why do we need certSANs?</strong> The API server certificate must list all valid names/IPs that clients use to connect. Without the VIP in the SANs, TLS verification will fail when connecting through the VIP.</p>
</blockquote>

<h3 id="1011-restart-kubelet-and-verify-from-a-master">10.11 Restart kubelet and verify from a master</h3>

<p>On the <strong>first master</strong>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c"># Restart kubelet</span>
<span class="nb">sudo </span>systemctl restart kubelet

<span class="c"># Wait for API server to be ready</span>
<span class="nb">sleep </span>10

<span class="c"># Test connectivity via the VIP</span>
kubectl get nodes <span class="nt">--server</span><span class="o">=</span>https://k8s-api.example.com:6443

<span class="c"># If that works, test VIP failover by temporarily stopping keepalived on the master</span>
<span class="c"># sudo systemctl stop keepalived</span>
<span class="c"># Then check that the VIP moves to another master</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="11-phase-8--certificate-management">11. Phase 8 — Certificate Management</h2>

<blockquote>
  <p><strong>Why:</strong> Kubernetes certificates expire (typically after 1 year). You may also need to add new IPs or hostnames to the certificate SANs as your cluster grows.</p>
</blockquote>

<h3 id="111-check-certificate-expiry">11.1 Check certificate expiry</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Check expiry of all certificates</span>
<span class="nb">sudo </span>kubeadm certs check-expiration
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="112-renew-all-certificates">11.2 Renew all certificates</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c"># Renew all certificates</span>
<span class="nb">sudo </span>kubeadm certs renew all

<span class="c"># Restart control plane components</span>
<span class="nb">sudo </span>systemctl restart kubelet
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="113-update-certificates-with-new-sans-for-ha">11.3 Update certificates with new SANs (for HA)</h3>

<p>If you need to add new IPs/hostnames to the API server certificate (e.g., after adding a new master), follow the steps in <a href="#1010-regenerate-api-server-certificates-with-sans">Section 10.10</a>.</p>

<h3 id="114-full-certificate-regeneration-with-kubeadm">11.4 Full certificate regeneration with kubeadm</h3>

<p>If the certificates are problematic, you can fully regenerate them:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="c"># On all masters:</span>
<span class="c"># 1. Backup old certificates</span>
<span class="nb">sudo cp</span> <span class="nt">-r</span> /etc/kubernetes/pki /etc/kubernetes/pki.bak-<span class="si">$(</span><span class="nb">date</span> +%Y%m%d%H%M%S<span class="si">)</span>

<span class="c"># 2. Remove the apiserver cert and key</span>
<span class="nb">sudo rm</span> <span class="nt">-f</span> /etc/kubernetes/pki/apiserver.crt /etc/kubernetes/pki/apiserver.key

<span class="c"># 3. Regenerate with your config</span>
<span class="nb">sudo </span>kubeadm init phase certs apiserver <span class="nt">--config</span> /root/kubeadm-cert-config.yaml

<span class="c"># 4. Restart kubelet</span>
<span class="nb">sudo </span>systemctl restart kubelet

<span class="c"># 5. On the first master only — update the admin kubeconfig</span>
<span class="nb">sudo </span>kubeadm init phase kubeconfig all <span class="nt">--control-plane-endpoint</span> k8s-api.example.com:6443
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="12-phase-9--reset-worker-nodes-for-re-joining">12. Phase 9 — Reset Worker Nodes (for Re-joining)</h2>

<blockquote>
  <p><strong>Why:</strong> If a worker node becomes corrupted, or you need to reinstall it, you must completely clean it before re-joining.</p>
</blockquote>

<h3 id="121-on-the-worker-node">12.1 On the worker node</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="c"># Stop kubelet</span>
<span class="nb">sudo </span>systemctl stop kubelet

<span class="c"># Kill any processes on port 10250 (kubelet port)</span>
<span class="nb">sudo </span>lsof <span class="nt">-t</span> <span class="nt">-i</span>:10250 | xargs <span class="nt">-r</span> <span class="nb">sudo kill</span> <span class="nt">-9</span>

<span class="c"># Force reset</span>
<span class="nb">sudo </span>kubeadm reset <span class="nt">--force</span>

<span class="c"># Remove CNI and Kubernetes configs</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /etc/cni/net.d
<span class="nb">sudo rm</span> <span class="nt">-f</span> /etc/kubernetes/kubelet.conf
<span class="nb">sudo rm</span> <span class="nt">-f</span> /etc/kubernetes/pki/ca.crt
<span class="nb">sudo rm</span> <span class="nt">-f</span> /etc/kubernetes/bootstrap-kubelet.conf
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /etc/kubernetes/pki
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /var/lib/kubelet/pki
<span class="nb">sudo rm</span> <span class="nt">-f</span> /var/lib/kubelet/config.yaml

<span class="c"># Reset iptables</span>
<span class="nb">sudo </span>iptables <span class="nt">-F</span>
<span class="nb">sudo </span>iptables <span class="nt">-t</span> nat <span class="nt">-F</span>
<span class="nb">sudo </span>iptables <span class="nt">-t</span> mangle <span class="nt">-F</span>
<span class="nb">sudo </span>iptables <span class="nt">-X</span>

<span class="c"># Restart services</span>
<span class="nb">sudo </span>systemctl restart containerd
<span class="nb">sudo </span>systemctl restart kubelet
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="122-on-the-first-master--delete-the-old-worker-node">12.2 On the first master — delete the old worker node</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c"># Get the name of the worker node</span>
kubectl get nodes

<span class="c"># Delete the worker node (replace with actual name)</span>
kubectl delete node k8s-worker-01
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="123-re-join-the-worker">12.3 Re-join the worker</h3>

<p>Now generate a new join token and re-join following <a href="#9-phase-6--join-worker-nodes">Phase 6</a>.</p>

<hr />

<h2 id="13-phase-10--reboot-all-nodes">13. Phase 10 — Reboot All Nodes</h2>

<p>If you need to reboot all nodes after the setup (e.g., after kernel updates):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Reboot all nodes (masters and workers)</span>
<span class="nb">sudo </span>reboot
</pre></td></tr></tbody></table></code></pre></div></div>

<p>After the reboot, verify the cluster recovers:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c"># On any master:</span>
kubectl get nodes
kubectl get pods <span class="nt">-A</span>
cilium status
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Kubernetes is designed to automatically recover after reboots. All nodes should rejoin and pods should be rescheduled automatically.</p>

<hr />

<h2 id="14-appendix--verification--troubleshooting">14. Appendix — Verification &amp; Troubleshooting</h2>

<h3 id="141-useful-verification-commands">14.1 Useful verification commands</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="c"># Cluster status</span>
kubectl cluster-info
kubectl get nodes <span class="nt">-o</span> wide
kubectl get pods <span class="nt">-A</span>
kubectl get svc <span class="nt">-A</span>

<span class="c"># Cilium status</span>
cilium status
cilium connectivity <span class="nb">test</span>

<span class="c"># Certificate expiry</span>
<span class="nb">sudo </span>kubeadm certs check-expiration

<span class="c"># HAProxy status</span>
<span class="nb">sudo </span>systemctl status haproxy
<span class="nb">sudo </span>haproxy <span class="nt">-f</span> /etc/haproxy/haproxy.cfg <span class="nt">-c</span>   <span class="c"># Check config</span>

<span class="c"># Keepalived status</span>
<span class="nb">sudo </span>systemctl status keepalived
ip addr show | <span class="nb">grep</span> &lt;VIP&gt;    <span class="c"># Check which node has the VIP</span>

<span class="c"># System checks</span>
free <span class="nt">-m</span>                       <span class="c"># Memory</span>
<span class="nb">df</span> <span class="nt">-h</span>                         <span class="c"># Disk</span>
<span class="nb">uptime</span>                        <span class="c"># How long running</span>
<span class="nb">sudo </span>sysctl net.ipv4.ip_forward  <span class="c"># Verify IP forwarding</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="142-common-issues-and-solutions">14.2 Common issues and solutions</h3>

<table>
  <thead>
    <tr>
      <th>Symptom</th>
      <th>Likely Cause</th>
      <th>Solution</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">kubeadm init</code> fails with “port 6443 already in use”</td>
      <td>Previous cluster not cleaned</td>
      <td>Run <code class="language-plaintext highlighter-rouge">sudo kubeadm reset -f</code> and retry</td>
    </tr>
    <tr>
      <td>Node shows <code class="language-plaintext highlighter-rouge">NotReady</code></td>
      <td>CNI not installed or containerd issue</td>
      <td>Check <code class="language-plaintext highlighter-rouge">kubectl get pods -n kube-system</code>, verify containerd is running</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">kubectl</code> connection refused</td>
      <td>API server down or wrong kubeconfig</td>
      <td>Check kube-apiserver pod, verify <code class="language-plaintext highlighter-rouge">controlPlaneEndpoint</code></td>
    </tr>
    <tr>
      <td>TLS handshake error with VIP</td>
      <td>VIP not in certificate SANs</td>
      <td>Regenerate certificates with the VIP in certSANs</td>
    </tr>
    <tr>
      <td>Keepalived VIP not appearing</td>
      <td>Interface name wrong or VRRP blocked</td>
      <td>Check <code class="language-plaintext highlighter-rouge">ip link show</code> for correct interface, check firewall</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">kubeadm join</code> token expired</td>
      <td>Token lifetime exceeded</td>
      <td>Generate a new token with <code class="language-plaintext highlighter-rouge">kubeadm token create --print-join-command</code></td>
    </tr>
    <tr>
      <td>Pods stuck in <code class="language-plaintext highlighter-rouge">ContainerCreating</code></td>
      <td>CNI not ready or network issues</td>
      <td>Check Cilium pods, verify <code class="language-plaintext highlighter-rouge">br_netfilter</code> module</td>
    </tr>
    <tr>
      <td>kubelet constantly restarting</td>
      <td>Not yet joined to cluster</td>
      <td>This is normal until <code class="language-plaintext highlighter-rouge">kubeadm join</code> or <code class="language-plaintext highlighter-rouge">kubeadm init</code> is run</td>
    </tr>
  </tbody>
</table>

<h3 id="143-node-port-range">14.3 Node port range</h3>

<p>Kubernetes uses ports 30000–32767 for NodePort services. Ensure these are open if you need external access to services.</p>

<h3 id="144-required-ports-reference">14.4 Required ports reference</h3>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Component</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>6443</td>
      <td>kube-apiserver</td>
      <td>Kubernetes API (HTTPS)</td>
    </tr>
    <tr>
      <td>2379-2380</td>
      <td>etcd</td>
      <td>etcd server/client</td>
    </tr>
    <tr>
      <td>10250</td>
      <td>kubelet</td>
      <td>Kubelet API</td>
    </tr>
    <tr>
      <td>10259</td>
      <td>kube-scheduler</td>
      <td>Scheduler health</td>
    </tr>
    <tr>
      <td>10257</td>
      <td>kube-controller-manager</td>
      <td>Controller manager health</td>
    </tr>
    <tr>
      <td>30000-32767</td>
      <td>NodePorts</td>
      <td>Service NodePort range</td>
    </tr>
    <tr>
      <td>8472</td>
      <td>Cilium</td>
      <td>VXLAN overlay (if used)</td>
    </tr>
    <tr>
      <td>4244</td>
      <td>Cilium</td>
      <td>Hubble relay</td>
    </tr>
  </tbody>
</table>

<hr />

<blockquote>
  <p><strong>Document Version:</strong> 1.0
<strong>Last Updated:</strong> 2026-07-18</p>

  <p>This guide replaces automation (Ansible) with detailed manual steps so operators understand what each command does and why it is needed. Adapt IP addresses, hostnames, interface names, and versions to match your environment.</p>
</blockquote>]]></content><author><name>Moin Uddin Ahmed</name></author><category term="Kubernetes" /><category term="cilium" /><category term="haproxy" /><category term="high-availability" /><category term="keepalived" /><category term="kubeadm" /><category term="kubernetes" /><category term="linux" /><summary type="html"><![CDATA[A comprehensive, secure, step-by-step manual guide for setting up a production-grade multi-master Kubernetes cluster. Covers architecture planning, OS prerequisites, cluster initialization with kubeadm, high availability with Keepalived and HAProxy, Cilium CNI, certificate management, and troubleshooting.]]></summary></entry><entry><title type="html">Setting Up Highly Available PostgreSQL Cluster with Patroni, etcd, HAProxy, and Keepalived</title><link href="https://marufmoinuddin.github.io/blog/2026/07/setting-up-highly-available-postgresql-cluster-with-patroni-etcd-haproxy-and-kee/" rel="alternate" type="text/html" title="Setting Up Highly Available PostgreSQL Cluster with Patroni, etcd, HAProxy, and Keepalived" /><published>2026-07-17T00:00:00+00:00</published><updated>2026-07-17T00:00:00+00:00</updated><id>https://marufmoinuddin.github.io/blog/2026/07/setting-up-highly-available-postgresql-cluster-with-patroni-etcd-haproxy-and-kee</id><content type="html" xml:base="https://marufmoinuddin.github.io/blog/2026/07/setting-up-highly-available-postgresql-cluster-with-patroni-etcd-haproxy-and-kee/"><![CDATA[<h1 id="setting-up-highly-available-postgresql-cluster-with-patroni-etcd-haproxy-and-keepalived">Setting Up Highly Available PostgreSQL Cluster with Patroni, etcd, HAProxy, and Keepalived</h1>

<h2 id="table-of-contents">Table of Contents</h2>
<ol>
  <li><a href="#overview">Overview</a></li>
  <li><a href="#architecture">Architecture</a></li>
  <li><a href="#prerequisites">Prerequisites</a></li>
  <li><a href="#initial-setup">Initial Setup</a>
    <ul>
      <li><a href="#hostname-configuration">Hostname Configuration</a></li>
      <li><a href="#package-installation">Package Installation</a></li>
    </ul>
  </li>
  <li><a href="#etcd-cluster-setup">etcd Cluster Setup</a>
    <ul>
      <li><a href="#configuration-steps">Configuration Steps</a></li>
    </ul>
  </li>
  <li><a href="#keepalived-setup">Keepalived Setup</a>
    <ul>
      <li><a href="#configuration-steps">Configuration Steps</a></li>
    </ul>
  </li>
  <li><a href="#patroni-configuration">Patroni Configuration</a>
    <ul>
      <li><a href="#environment-setup">Environment Setup</a></li>
      <li><a href="#create-patroni-configuration">Create Patroni Configuration</a></li>
      <li><a href="#create-patroni-service">Create Patroni Service</a></li>
      <li><a href="#start-patroni-service">Start Patroni Service</a></li>
    </ul>
  </li>
  <li><a href="#haproxy-configuration">HAProxy Configuration</a>
    <ul>
      <li><a href="#installation">Installation</a></li>
      <li><a href="#configuration">Configuration</a></li>
      <li><a href="#start-haproxy">Start HAProxy</a></li>
    </ul>
  </li>
  <li><a href="#connection-testing">Connection Testing</a>
    <ul>
      <li><a href="#test-primary-connection">Test Primary Connection</a></li>
    </ul>
  </li>
  <li><a href="#verification-and-testing">Verification and Testing</a>
    <ul>
      <li><a href="#check-cluster-status">Check Cluster Status</a></li>
    </ul>
  </li>
  <li><a href="#restoring-old-data-to-new-cluster">Restoring Old Data to New Cluster</a>
    <ul>
      <li><a href="#backup-the-data-from-the-previous-cluster">Backup the Data from the Previous Cluster</a></li>
      <li><a href="#create-users-and-restore-the-data-in-the-new-cluster">Create users and restore the data in the new cluster</a></li>
      <li><a href="#test-and-verify-the-data-in-the-new-cluster">Test and verify the data in the new cluster</a></li>
    </ul>
  </li>
  <li><a href="#pgbackrest-setup">pgBackRest Setup</a>
    <ul>
      <li><a href="#host-configuration">Host Configuration</a></li>
      <li><a href="#postgresql-node-configuration-for-pgbackrest">PostgreSQL Node Configuration for pgBackRest</a></li>
    </ul>
  </li>
  <li><a href="#monitoring-postgresql-with-percona-monitoring-and-management-&lt;REDACTED_USERNAME&gt;">Monitoring PostgreSQL with Percona Monitoring and Management (PMM)</a>
    <ul>
      <li><a href="#&lt;REDACTED_USERNAME&gt;-server-installation">PMM Server Installation</a></li>
      <li><a href="#&lt;REDACTED_USERNAME&gt;-client-installation">PMM Client Installation</a></li>
      <li><a href="#&lt;REDACTED_USERNAME&gt;-configuration">PMM Configuration</a></li>
    </ul>
  </li>
  <li><a href="#conclusion">Conclusion</a></li>
  <li><a href="#resources">Resources</a></li>
</ol>

<h2 id="overview">Overview</h2>

<p>In this guide, we are setting up a highly available PostgreSQL cluster using several key components: Patroni, etcd, HAProxy, and Keepalived. Patroni is a template for PostgreSQL high availability, allowing us to manage PostgreSQL clusters with automatic failover. etcd is a distributed key-value store that provides a reliable way to store data across a cluster of machines, ensuring that Patroni can maintain consensus on the state of the cluster. HAProxy is a high-performance TCP/HTTP load balancer that will distribute database connections across the PostgreSQL nodes, ensuring that read and write operations are directed appropriately. Finally, Keepalived is used to manage a virtual IP (VIP) that provides a single, consistent endpoint for database connections, even in the event of node failures. By following this guide, you will learn how to configure and integrate these components to create a robust and resilient PostgreSQL cluster suitable for production environments.</p>

<h2 id="architecture">Architecture</h2>

<p>The cluster consists of:</p>
<ul>
  <li>3 PostgreSQL nodes with Patroni</li>
  <li>Distributed etcd cluster</li>
  <li>HAProxy on each node</li>
  <li>Keepalived for VIP management</li>
  <li>Virtual IP (VIP) for high availability</li>
</ul>

<table>
  <thead>
    <tr>
      <th>Node Name</th>
      <th>Role</th>
      <th>IP Address</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><REDACTED_HOSTNAME></REDACTED_HOSTNAME></td>
      <td>PostgreSQL + Patroni + etcd</td>
      <td><REDACTED_IP></REDACTED_IP></td>
    </tr>
    <tr>
      <td><REDACTED_HOSTNAME></REDACTED_HOSTNAME></td>
      <td>PostgreSQL + Patroni + etcd</td>
      <td><REDACTED_IP></REDACTED_IP></td>
    </tr>
    <tr>
      <td><REDACTED_HOSTNAME></REDACTED_HOSTNAME></td>
      <td>PostgreSQL + Patroni + etcd</td>
      <td><REDACTED_IP></REDACTED_IP></td>
    </tr>
    <tr>
      <td><REDACTED_HOSTNAME></REDACTED_HOSTNAME></td>
      <td>pgBackRest + PMM Server</td>
      <td><REDACTED_IP></REDACTED_IP></td>
    </tr>
    <tr>
      <td>Virtual IP</td>
      <td>High Availability Endpoint</td>
      <td><REDACTED_IP></REDACTED_IP></td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Note: The VIP is used as the primary connection endpoint for applications.</p>
</blockquote>

<h2 id="architecture-diagram">Architecture Diagram</h2>

<p>The following diagram illustrates the complete node architecture of our PostgreSQL high availability setup:</p>

<p><img src="pg-docs/2025-03-09_02-44.png" alt="PostgreSQL High Availability Architecture" /></p>

<p><em>Figure 1: PostgreSQL HA Cluster Node Architecture showing all components distributed across nodes</em></p>

<p>The diagram shows how each component is distributed across the different nodes:</p>

<ul>
  <li>**<REDACTED_HOSTNAME>, <REDACTED_HOSTNAME>, <REDACTED_HOSTNAME>**: Each database node contains PostgreSQL, Patroni, etcd, HAProxy, Keepalived, and PMM Client</REDACTED_HOSTNAME></REDACTED_HOSTNAME></REDACTED_HOSTNAME></li>
  <li>**<REDACTED_HOSTNAME>**: Dedicated backup node running pgBackRest and PMM Server</REDACTED_HOSTNAME></li>
  <li>**Virtual IP (<REDACTED_IP>)**: Floating IP that provides a stable connection endpoint for applications</REDACTED_IP></li>
</ul>

<p>This architecture ensures high availability with automatic failover capabilities, comprehensive backup solutions, and performance monitoring.</p>

<blockquote>
  <p><strong>Note:</strong> To regenerate this diagram if needed, you can use the <a href="https://mermaid.live/">Mermaid Live Editor</a> with the diagram code available in the documentation source.</p>
</blockquote>

<h2 id="prerequisites">Prerequisites</h2>

<p>Before starting the setup, ensure you have the following:</p>

<ul>
  <li>Ubuntu 22.04 or later</li>
  <li>Sudo privileges on all nodes</li>
  <li>Network connectivity between nodes</li>
  <li>Required ports:
    <ul>
      <li>PostgreSQL: 5432</li>
      <li>Patroni: 8008</li>
      <li>etcd: 2379, 2380</li>
      <li>HAProxy: 5433</li>
      <li>Keepalived: VRRP (112)</li>
    </ul>
  </li>
</ul>

<h2 id="initial-setup">Initial Setup</h2>

<p>In this part, we will configure the basic environment settings, including hostname configuration and package installation. These are needed to prepare the nodes for the subsequent steps.</p>

<h3 id="hostname-configuration">Hostname Configuration</h3>
<ol>
  <li>Set the hostname on each node:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c"># On respective nodes</span>
 <span class="nb">sudo </span>hostnamectl set-hostname &lt;REDACTED_HOSTNAME&gt;  <span class="c"># For node 1</span>

 <span class="nb">sudo </span>hostnamectl set-hostname &lt;REDACTED_HOSTNAME&gt;  <span class="c"># For node 2</span>
    
 <span class="nb">sudo </span>hostnamectl set-hostname &lt;REDACTED_HOSTNAME&gt;  <span class="c"># For node 3</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <blockquote>
      <p>Note: Replace <code class="language-plaintext highlighter-rouge">&lt;REDACTED_HOSTNAME&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;REDACTED_HOSTNAME&gt;</code>, and <code class="language-plaintext highlighter-rouge">&lt;REDACTED_HOSTNAME&gt;</code> with the actual hostnames.</p>
    </blockquote>
  </li>
  <li>Update <code class="language-plaintext highlighter-rouge">/etc/hosts</code> on all nodes:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre> # Add to /etc/hosts
 &lt;REDACTED_IP&gt; &lt;REDACTED_HOSTNAME&gt;
 &lt;REDACTED_IP&gt; &lt;REDACTED_HOSTNAME&gt;
 &lt;REDACTED_IP&gt; &lt;REDACTED_HOSTNAME&gt;
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <blockquote>
      <p>Note: Replace IP addresses with the actual IP addresses of the nodes.</p>
    </blockquote>
  </li>
</ol>

<h3 id="package-installation">Package Installation</h3>
<p>Install required packages on all nodes:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre>    <span class="c"># Add Percona repository</span>
    <span class="nb">sudo </span>percona-release setup ppg12

    <span class="c"># Install required packages</span>
    <span class="nb">sudo </span>apt update
    <span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\</span>
        percona-postgresql-12 <span class="se">\</span>
        percona-patroni <span class="se">\</span>
        etcd <span class="se">\</span>
        percona-haproxy <span class="se">\</span>
        keepalived <span class="se">\</span>
        python3-pip <span class="se">\</span>
        python3-dev
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="etcd-cluster-setup">etcd Cluster Setup</h2>

<p>The etcd cluster is a critical component in the Patroni architecture, serving as the distributed key-value store that enables high availability and automatic failover capabilities for PostgreSQL.</p>

<h4 id="key-functions">Key Functions</h4>
<ul>
  <li>Maintains cluster state information</li>
  <li>Handles leader election processes</li>
  <li>Stores configuration data</li>
  <li>Enables distributed consensus among nodes</li>
</ul>

<h4 id="configuration-requirements">Configuration Requirements</h4>
<ol>
  <li>Each node in the cluster requires:
    <ul>
      <li>Unique etcd instance</li>
      <li>Individual configuration file</li>
      <li>Distinct network endpoints</li>
      <li>Specific cluster membership settings</li>
    </ul>
  </li>
</ol>

<h4 id="important-considerations">Important Considerations</h4>
<ul>
  <li>Minimum of 3 nodes recommended for fault tolerance</li>
  <li>Network connectivity between all nodes is essential</li>
  <li>Proper security configuration (TLS certificates if needed)</li>
  <li>Adequate storage for etcd data</li>
</ul>

<h4 id="best-practices">Best Practices</h4>
<ul>
  <li>Use dedicated hosts for etcd when possible</li>
  <li>Implement proper backup strategies</li>
  <li>Monitor etcd cluster health</li>
  <li>Configure appropriate timeouts and heartbeat intervals
The etcd cluster provides the distributed consensus mechanism required for Patroni. Each node needs its own etcd configuration.</li>
</ul>

<h3 id="configuration-steps">Configuration Steps</h3>
<ol>
  <li>Create etcd service file on each node:</li>
</ol>

<p>For <REDACTED_HOSTNAME> (`/etc/systemd/system/etcd.service`):</REDACTED_HOSTNAME></p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
</pre></td><td class="rouge-code"><pre>    <span class="nn">[Unit]</span>
    <span class="py">Description</span><span class="p">=</span><span class="s">etcd key-value store</span>
    <span class="py">Documentation</span><span class="p">=</span><span class="s">https://etcd.io/docs/</span>
    <span class="py">After</span><span class="p">=</span><span class="s">network.target</span>

    <span class="nn">[Service]</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"TOKEN=demo-cluster-token"</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"CLUSTER_STATE=new"</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"NAME_1=&lt;REDACTED_HOSTNAME&gt;"</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"NAME_2=&lt;REDACTED_HOSTNAME&gt;"</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"NAME_3=&lt;REDACTED_HOSTNAME&gt;"</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"HOST_1=&lt;REDACTED_IP&gt;"</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"HOST_2=&lt;REDACTED_IP&gt;"</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"HOST_3=&lt;REDACTED_IP&gt;"</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"CLUSTER=${NAME_1}=http://${HOST_1}:2380,${NAME_2}=http://${HOST_2}:2380,${NAME_3}=http://${HOST_3}:2380"</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"THIS_NAME=&lt;REDACTED_HOSTNAME&gt;"</span>
    <span class="py">Environment</span><span class="p">=</span><span class="s">"THIS_IP=&lt;REDACTED_IP&gt;"</span>

    <span class="py">ExecStart</span><span class="p">=</span><span class="s">/usr/bin/etcd </span><span class="se">\
</span>    <span class="s">--data-dir=/var/lib/etcd </span><span class="se">\
</span>    <span class="s">--name ${THIS_NAME} </span><span class="se">\
</span>    <span class="s">--initial-advertise-peer-urls http://${THIS_IP}:2380 </span><span class="se">\
</span>    <span class="s">--listen-peer-urls http://${THIS_IP}:2380 </span><span class="se">\
</span>    <span class="s">--advertise-client-urls http://${THIS_IP}:2379 </span><span class="se">\
</span>    <span class="s">--listen-client-urls http://${THIS_IP}:2379 </span><span class="se">\
</span>    <span class="s">--initial-cluster ${CLUSTER} </span><span class="se">\
</span>    <span class="s">--initial-cluster-state ${CLUSTER_STATE} </span><span class="se">\
</span>    <span class="s">--initial-cluster-token ${TOKEN}</span>

    <span class="py">Restart</span><span class="p">=</span><span class="s">always</span>
    <span class="py">RestartSec</span><span class="p">=</span><span class="s">5</span>

    <span class="nn">[Install]</span>
    <span class="py">WantedBy</span><span class="p">=</span><span class="s">multi-user.target</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<blockquote>
  <p>Note: Replace <code class="language-plaintext highlighter-rouge">&lt;REDACTED_HOSTNAME&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;REDACTED_IP&gt;</code>, and other values with the actual values for the node.</p>
</blockquote>

<p>Repeat for other nodes with appropriate THIS_NAME and THIS_IP values.</p>

<ol>
  <li>Start etcd service:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre> <span class="nb">sudo </span>systemctl daemon-reload
 <span class="nb">sudo </span>systemctl <span class="nb">enable </span>etcd
 <span class="nb">sudo </span>systemctl start etcd
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<h2 id="keepalived-setup">Keepalived Setup</h2>

<p>Keepalived is a Linux-based routing software that provides:</p>
<ul>
  <li>High availability (HA)</li>
  <li>Load balancing</li>
  <li>Automatic failover capabilities</li>
</ul>

<p>The Virtual IP (VIP) works like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>Client → Virtual IP (192.168.1.100)
                ↙               ↘
    Database Server 1    Database Server 2
    (192.168.1.101)     (192.168.1.102)
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="key-points">Key Points:</h4>
<ul>
  <li>The VIP acts as a floating IP address that can move between servers</li>
  <li>If the primary database server fails, Keepalived automatically moves the VIP to the backup server</li>
  <li>Applications connect to the VIP instead of physical server IPs</li>
  <li>This provides seamless failover without requiring application reconfiguration</li>
</ul>

<h4 id="benefits">Benefits:</h4>
<ul>
  <li><strong>High Availability</strong>: No single point of failure</li>
  <li><strong>Transparency</strong>: Client applications don’t need to know about the underlying server changes</li>
  <li><strong>Zero-downtime maintenance</strong>: Servers can be maintained without service interruption</li>
</ul>

<p>Think of it like having a single phone number (VIP) that can ring different phones (database servers) based on availability.</p>

<h3 id="configuration-steps-1">Configuration Steps</h3>
<ol>
  <li>Create Keepalived configuration (<code class="language-plaintext highlighter-rouge">/etc/keepalived/keepalived.conf</code>):
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre> vrrp_script check_haproxy {
     script "pgrep haproxy"
     interval 2
     weight 2
 }

 vrrp_instance VI_1 {
     state MASTER
     interface enp1s0
     virtual_router_id 51
     priority 101
     advert_int 1
     authentication {
         auth_type PASS
         auth_pass &lt;REDACTED_PASSWORD&gt;
     }
     virtual_ipaddress {
         &lt;REDACTED_IP&gt;
     }
     track_script {
         check_haproxy
     }
 }
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<blockquote>
  <p>Note: Replace <code class="language-plaintext highlighter-rouge">enp1s0</code>, <code class="language-plaintext highlighter-rouge">&lt;REDACTED_PASSWORD&gt;</code>, and <code class="language-plaintext highlighter-rouge">&lt;REDACTED_IP&gt;</code> with the actual values.</p>
</blockquote>

<ol>
  <li>Start Keepalived:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre> <span class="nb">sudo </span>systemctl <span class="nb">enable </span>keepalived
 <span class="nb">sudo </span>systemctl restart keepalived
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<p>[Previous sections remain the same up to Patroni Configuration]</p>

<h2 id="patroni-configuration">Patroni Configuration</h2>

<p>Patroni, is a robust PostgreSQL high-availability solution.</p>

<h4 id="technical-details">Technical Details</h4>
<p>Patroni provides:</p>
<ol>
  <li>Real-time replication monitoring</li>
  <li>Automatic primary-replica synchronization</li>
  <li>Health check mechanisms</li>
  <li>Dynamic configuration management</li>
</ol>

<h4 id="implementation-significance">Implementation Significance</h4>
<p>The system ensures continuous database availability through:</p>
<ul>
  <li>Seamless failover processes</li>
  <li>Consistent data replication</li>
  <li>Reliable cluster state management</li>
</ul>

<p>This configuration is essential for maintaining robust database operations in production environments.</p>

<h3 id="environment-setup">Environment Setup</h3>
<p>First, set up environment variables on each node:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>    <span class="c"># Set node name and IP</span>
    <span class="nb">export </span><span class="nv">NODE_NAME</span><span class="o">=</span><span class="si">$(</span><span class="nb">hostname</span> <span class="nt">-f</span><span class="si">)</span>
    <span class="nb">export </span><span class="nv">NODE_IP</span><span class="o">=</span><span class="si">$(</span><span class="nb">hostname</span> <span class="nt">-i</span> | <span class="nb">awk</span> <span class="s1">'{print $1}'</span><span class="si">)</span>

    <span class="c"># Set PostgreSQL paths</span>
    <span class="nb">export </span><span class="nv">DATA_DIR</span><span class="o">=</span><span class="s2">"/var/lib/postgresql/12/main"</span>
    <span class="nb">export </span><span class="nv">PG_BIN_DIR</span><span class="o">=</span><span class="s2">"/usr/lib/postgresql/12/bin"</span>

    <span class="c"># Set cluster information</span>
    <span class="nb">export </span><span class="nv">NAMESPACE</span><span class="o">=</span><span class="s2">"kyc"</span>
    <span class="nb">export </span><span class="nv">SCOPE</span><span class="o">=</span><span class="s2">"kyc"</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<blockquote>
  <p>Note: Replace the values with your own.</p>
</blockquote>

<h3 id="create-patroni-configuration">Create Patroni Configuration</h3>
<p>Create the Patroni configuration file (<code class="language-plaintext highlighter-rouge">/etc/patroni/patroni.yml</code>):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
</pre></td><td class="rouge-code"><pre>    <span class="nb">echo</span> <span class="s2">"
    namespace: </span><span class="k">${</span><span class="nv">NAMESPACE</span><span class="k">}</span><span class="s2">
    scope: </span><span class="k">${</span><span class="nv">SCOPE</span><span class="k">}</span><span class="s2">
    name: </span><span class="k">${</span><span class="nv">NODE_NAME</span><span class="k">}</span><span class="s2">

    restapi:
        listen: 0.0.0.0:8008
        connect_address: </span><span class="k">${</span><span class="nv">NODE_IP</span><span class="k">}</span><span class="s2">:8008

    etcd3:
        host: </span><span class="k">${</span><span class="nv">NODE_IP</span><span class="k">}</span><span class="s2">:2379

    bootstrap:
    # this section will be written into Etcd:/&lt;namespace&gt;/&lt;scope&gt;/config after initializing new cluster
    dcs:
        ttl: 30
        loop_wait: 10
        retry_timeout: 10
        maximum_lag_on_failover: 1048576

        postgresql:
            use_pg_rewind: true
            use_slots: true
            parameters:
                wal_level: replica
                hot_standby: "</span>on<span class="s2">"
                wal_keep_segments: 10
                max_wal_senders: 5
                max_replication_slots: 10
                wal_log_hints: "</span>on<span class="s2">"
                logging_collector: 'on'
                max_wal_size: '10GB'
                archive_mode: "</span>on<span class="s2">"
                archive_timeout: 600s
                archive_command: "</span><span class="nb">cp</span> <span class="nt">-f</span> %p /home/postgres/archived/%f<span class="s2">"

    # some desired options for 'initdb'
    initdb: # Note: It needs to be a list (some options need values, others are switches)
        - encoding: UTF8
        - data-checksums

    pg_hba: # Add following lines to pg_hba.conf after running 'initdb'
        - host replication replicator 127.0.0.1/32 trust
        - host replication replicator 0.0.0.0/0 md5
        - host all all 0.0.0.0/0 md5
        - host all all ::0/0 md5

    # Some additional users which needs to be created after initializing new cluster
    users:
        admin:
            password: &lt;REDACTED_PASSWORD&gt;
            options:
                - createrole
                - createdb
        percona:
            password: &lt;REDACTED_PASSWORD&gt;
            options:
                - createrole
                - createdb 

    postgresql:
        cluster_name: kyc
        listen: 0.0.0.0:5432
        connect_address: </span><span class="k">${</span><span class="nv">NODE_IP</span><span class="k">}</span><span class="s2">:5432
        data_dir: </span><span class="k">${</span><span class="nv">DATA_DIR</span><span class="k">}</span><span class="s2">
        bin_dir: </span><span class="k">${</span><span class="nv">PG_BIN_DIR</span><span class="k">}</span><span class="s2">
        pgpass: /tmp/pgpass0
        authentication:
            replication:
                username: replicator
                password: &lt;REDACTED_PASSWORD&gt;
            superuser:
                username: postgres
                password: &lt;REDACTED_PASSWORD&gt;
        parameters:
            unix_socket_directories: "</span>/var/run/postgresql/<span class="s2">"
        create_replica_methods:
            - basebackup
        basebackup:
            checkpoint: 'fast'

    tags:
        nofailover: false
        noloadbalance: false
        clonefrom: false
        nosync: false
    "</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/patroni/patroni.yml
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p>Note: Replace the passwords and other values with your own. Also, ensure that the <code class="language-plaintext highlighter-rouge">archive_command</code> path exists on the system.</p>
</blockquote>

<h3 id="create-patroni-service">Create Patroni Service</h3>
<p>Create the systemd service file (<code class="language-plaintext highlighter-rouge">/etc/systemd/system/patroni.service</code>):</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre>    <span class="nn">[Unit]</span>
    <span class="py">Description</span><span class="p">=</span><span class="s">Runners to orchestrate a high-availability PostgreSQL</span>
    <span class="py">After</span><span class="p">=</span><span class="s">syslog.target network.target</span>

    <span class="nn">[Service]</span>
    <span class="py">Type</span><span class="p">=</span><span class="s">simple</span>

    <span class="py">User</span><span class="p">=</span><span class="s">postgres</span>
    <span class="py">Group</span><span class="p">=</span><span class="s">postgres</span>

    <span class="py">ExecStart</span><span class="p">=</span><span class="s">/bin/patroni /etc/patroni/patroni.yml</span>
    <span class="py">ExecReload</span><span class="p">=</span><span class="s">/bin/kill -s HUP $MAINPID</span>

    <span class="py">KillMode</span><span class="p">=</span><span class="s">process</span>
    <span class="py">TimeoutSec</span><span class="p">=</span><span class="s">30</span>
    <span class="py">Restart</span><span class="p">=</span><span class="s">no</span>

    <span class="nn">[Install]</span>
    <span class="py">WantedBy</span><span class="p">=</span><span class="s">multi-user.target</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="start-patroni-service">Start Patroni Service</h3>
<p>Start the Patroni service in sequence, beginning with the first node:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>    <span class="nb">sudo </span>systemctl daemon-reload
    <span class="nb">sudo </span>systemctl <span class="nb">enable </span>patroni
    <span class="nb">sudo </span>systemctl start patroni
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Monitor the service:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>    <span class="nb">sudo </span>journalctl <span class="nt">-fu</span> patroni
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="haproxy-configuration">HAProxy Configuration</h2>

<p>HAProxy provides load balancing between PostgreSQL nodes, directing write operations to the primary and distributing read operations among replicas.</p>

<h4 id="key-functions-1">Key Functions</h4>

<ul>
  <li>Routes all write operations (INSERT, UPDATE, DELETE) to the primary node</li>
  <li>Maintains data consistency through single-point write operations</li>
  <li>Distributes SELECT queries across multiple replica nodes</li>
  <li>Implements load balancing algorithms (round-robin, least-connections)</li>
  <li>Prevents individual node overload</li>
</ul>

<p>Ideal for applications with read-heavy workloads and moderate write operations, such as typical web applications.</p>

<h3 id="installation">Installation</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>    <span class="nb">sudo </span>apt <span class="nb">install </span>percona-haproxy
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="configuration">Configuration</h3>
<p>Create the HAProxy configuration (<code class="language-plaintext highlighter-rouge">/etc/haproxy/haproxy.cfg</code>):</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
</pre></td><td class="rouge-code"><pre>    <span class="err">global</span>
        <span class="err">log</span> <span class="err">127.0.0.1</span> <span class="err">local0</span>
        <span class="err">maxconn</span> <span class="err">4096</span>
        <span class="err">user</span> <span class="err">haproxy</span>
        <span class="err">group</span> <span class="err">haproxy</span>
        <span class="err">daemon</span>

    <span class="err">defaults</span>
        <span class="err">log</span>     <span class="err">global</span>
        <span class="err">option</span>  <span class="err">tcplog</span>
        <span class="err">timeout</span> <span class="err">connect</span> <span class="err">10s</span>
        <span class="err">timeout</span> <span class="err">client</span>  <span class="err">30s</span>
        <span class="err">timeout</span> <span class="err">server</span>  <span class="err">30s</span>

    <span class="err">frontend</span> <span class="err">postgres</span>
        <span class="err">bind</span> <span class="err">*:5433</span>
        <span class="err">default_backend</span> <span class="err">patroni_cluster</span>

    <span class="err">backend</span> <span class="err">patroni_cluster</span>
        <span class="err">option</span> <span class="err">httpchk</span> <span class="err">OPTIONS</span> <span class="err">/master</span>
        <span class="err">http-check</span> <span class="err">expect</span> <span class="err">status</span> <span class="err">200</span>
        <span class="err">default-server</span> <span class="err">inter</span> <span class="err">3s</span> <span class="err">fall</span> <span class="err">3</span> <span class="err">rise</span> <span class="err">2</span> <span class="err">on-marked-down</span> <span class="err">shutdown-sessions</span>
        <span class="err">server</span> <span class="err">patroni1</span> <span class="err">&lt;REDACTED_IP&gt;:5432</span> <span class="err">check</span> <span class="err">port</span> <span class="err">8008</span>
        <span class="err">server</span> <span class="err">patroni2</span> <span class="err">&lt;REDACTED_IP&gt;:5432</span> <span class="err">check</span> <span class="err">port</span> <span class="err">8008</span>
        <span class="err">server</span> <span class="err">patroni3</span> <span class="err">&lt;REDACTED_IP&gt;:5432</span> <span class="err">check</span> <span class="err">port</span> <span class="err">8008</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p>Note: Replace IP addresses with the actual IP addresses of the nodes.</p>
</blockquote>

<h3 id="start-haproxy">Start HAProxy</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>    <span class="nb">sudo </span>systemctl restart haproxy
    <span class="nb">sudo </span>systemctl <span class="nb">enable </span>haproxy
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Monitor HAProxy:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>    <span class="nb">sudo </span>journalctl <span class="nt">-u</span> haproxy.service <span class="nt">-n</span> 100 <span class="nt">-f</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="connection-testing">Connection Testing</h2>

<p>Now that the cluster is set up, we can test the connection to the primary node and verify the cluster status.</p>

<h3 id="test-primary-connection">Test Primary Connection</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>    psql <span class="nt">-h</span> &lt;REDACTED_IP&gt; <span class="nt">-p</span> 5433 <span class="nt">-U</span> postgres <span class="nt">-d</span> postgres
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="verification-and-testing">Verification and Testing</h2>

<h3 id="check-cluster-status">Check Cluster Status</h3>
<ol>
  <li>Verify Patroni cluster:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> patronictl <span class="nt">-c</span> /etc/patroni/patroni.yml list
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<p>Expected output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>    + Cluster: kyc (7454297404487036569)+-----------+----+-----------+
    | Member  | Host          | Role    | State     | TL | Lag in MB |
    +---------+---------------+---------+-----------+----+-----------+
    | &lt;REDACTED_HOSTNAME&gt; | &lt;REDACTED_IP&gt; | Replica | streaming |  1 |         0 |
    | &lt;REDACTED_HOSTNAME&gt; | &lt;REDACTED_IP&gt; | Leader  | running   |  1 |           |
    | &lt;REDACTED_HOSTNAME&gt; | &lt;REDACTED_IP&gt; | Replica | streaming |  1 |         0 |
    +---------+---------------+---------+-----------+----+-----------+
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Verify etcd cluster:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre> <span class="nb">export </span><span class="nv">ETCDCTL_API</span><span class="o">=</span>3
 etcdctl <span class="nt">--endpoints</span><span class="o">=</span>http://&lt;REDACTED_IP&gt;:2379,http://&lt;REDACTED_IP&gt;:2379,http://&lt;REDACTED_IP&gt;:2379 member list
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Test VIP accessibility:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre> ping &lt;REDACTED_IP&gt;
 psql <span class="nt">-h</span> &lt;REDACTED_IP&gt; <span class="nt">-p</span> 5433 <span class="nt">-U</span> postgres
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<h2 id="restoring-old-data-to-new-cluster">Restoring Old Data to New Cluster</h2>

<p>You need to restore the some data from a backup of the previous PostgreSQL cluster to the new cluster. Here are the steps to do that.</p>

<h3 id="backup-the-data-from-the-previous-cluster">Backup the Data from the Previous Cluster</h3>

<ol>
  <li>Login as postgres user in bash and backup the <REDACTED_USERNAME> schema data and schema only data of kyc, <REDACTED_USERNAME> and <REDACTED_USERNAME>.</REDACTED_USERNAME></REDACTED_USERNAME></REDACTED_USERNAME></li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre>    <span class="c"># Log into the previous PostgreSQL cluster and switch to postgres bash user.</span>
    <span class="nb">sudo </span>su - postgres

    <span class="c"># Backup the &lt;REDACTED_USERNAME&gt; schema data</span>
    pg_dump <span class="nt">-h</span> localhost <span class="nt">-U</span> kyc <span class="nt">-d</span> postgres <span class="nt">-f</span> &lt;REDACTED_USERNAME&gt;_full.sql

    <span class="c"># Backup the schema only data of kyc, &lt;REDACTED_USERNAME&gt; and &lt;REDACTED_USERNAME&gt;</span>
    pg_dump <span class="nt">-h</span> localhost <span class="nt">-U</span> kyc <span class="nt">-d</span> postgres <span class="nt">-s</span> <span class="nt">-f</span> kyc_schema.sql
    pg_dump <span class="nt">-h</span> localhost <span class="nt">-U</span> kyc <span class="nt">-d</span> postgres <span class="nt">-s</span> <span class="nt">-f</span> &lt;REDACTED_USERNAME&gt;_schema.sql
    pg_dump <span class="nt">-h</span> localhost <span class="nt">-U</span> kyc <span class="nt">-d</span> postgres <span class="nt">-s</span> <span class="nt">-f</span> &lt;REDACTED_USERNAME&gt;_schema.sql
</pre></td></tr></tbody></table></code></pre></div></div>
<ol>
  <li>Backup 2 tables (kyc.asp_config, kyc.asp_config_id_seq) in kyc schema, as these tables are not part of the schema only data.</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>    <span class="c"># Backup the kyc.asp_config and kyc.asp_config_id_seq tables</span>
    pg_dump <span class="nt">-h</span> localhost <span class="nt">-U</span> kyc <span class="nt">-d</span> postgres <span class="nt">-t</span> kyc.asp_config <span class="nt">-t</span> kyc.asp_config_id_seq <span class="nt">-f</span> kyc_tables.sql
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Copy the backup files to the new cluster nodes. Using your preferred method, copy the backup files to the new cluster nodes.</li>
</ol>

<h3 id="create-users-and-restore-the-data-in-the-new-cluster">Create users and restore the data in the new cluster</h3>

<p>We need this three user in our new cluster. Those are <code class="language-plaintext highlighter-rouge">&lt;REDACTED_USERNAME&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;REDACTED_USERNAME&gt;</code>, <code class="language-plaintext highlighter-rouge">kyc</code> and <code class="language-plaintext highlighter-rouge">&lt;REDACTED_USERNAME&gt;</code>. Lets create these users in the new cluster.</p>

<ol>
  <li>
    <p>Create the users in the new cluster.</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre>     <span class="c"># Log into the new cluster and switch to postgres bash user.</span>
     <span class="nb">sudo </span>su - postgres
    
     <span class="c"># Create the users</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"CREATE USER &lt;REDACTED_USERNAME&gt; SUPERUSER CREATEDB CREATEROLE;"</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"CREATE USER &lt;REDACTED_USERNAME&gt; SUPERUSER CREATEDB CREATEROLE;"</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"CREATE USER &lt;REDACTED_USERNAME&gt; SUPERUSER CREATEDB CREATEROLE;"</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"CREATE USER &lt;REDACTED_USERNAME&gt; SUPERUSER CREATEDB CREATEROLE PASSWORD '&lt;REDACTED_PASSWORD&gt;';"</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>
    <p>Restore the data in the new cluster.</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre>     <span class="c"># Log into the new cluster and switch to postgres bash user.</span>
     <span class="nb">sudo </span>su - postgres

     <span class="c"># Restore the &lt;REDACTED_USERNAME&gt; schema data</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-f</span> &lt;REDACTED_USERNAME&gt;_full.sql

     <span class="c"># Restore the schema only data of kyc, &lt;REDACTED_USERNAME&gt; and &lt;REDACTED_USERNAME&gt;</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-f</span> kyc_schema.sql
     psql <span class="nt">-d</span> postgres <span class="nt">-f</span> &lt;REDACTED_USERNAME&gt;_schema.sql
     psql <span class="nt">-d</span> postgres <span class="nt">-f</span> &lt;REDACTED_USERNAME&gt;_schema.sql

     <span class="c"># Restore the kyc.asp_config and kyc.asp_config_id_seq tables</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-f</span> kyc_tables.sql
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>
    <p>Test and verify the data in the new cluster.</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre>     <span class="c"># Log into the new cluster and switch to postgres bash user.</span>
     <span class="nb">sudo </span>su - postgres

     <span class="c"># Connect to the database and verify the data</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"</span><span class="se">\d</span><span class="s2">t"</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"SELECT * FROM kyc.asp_config;"</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"SELECT * FROM kyc.asp_config_id_seq;"</span>

     <span class="c"># Check if the schema data is restored correctly</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"</span><span class="se">\d</span><span class="s2">t &lt;REDACTED_USERNAME&gt;.*"</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"</span><span class="se">\d</span><span class="s2">t kyc.*"</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"</span><span class="se">\d</span><span class="s2">t &lt;REDACTED_USERNAME&gt;.*"</span>
     psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s2">"</span><span class="se">\d</span><span class="s2">t &lt;REDACTED_USERNAME&gt;.*"</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<h2 id="pgbackrest-setup">pgBackRest Setup</h2>

<p>This section covers setting up pgBackRest on a dedicated backup server to provide backup and recovery capabilities for your PostgreSQL cluster. The prerequisites are given below.</p>

<ul>
  <li>A dedicated server for pgBackRest (referred to as <REDACTED_HOSTNAME>)</REDACTED_HOSTNAME></li>
  <li>SSH access between PostgreSQL nodes and the backup server</li>
  <li>Sufficient storage space for backups on <REDACTED_HOSTNAME></REDACTED_HOSTNAME></li>
</ul>

<h3 id="host-configuration">Host Configuration</h3>

<ol>
  <li>Update the hosts file on the backup server:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c"># Add to /etc/hosts on &lt;REDACTED_HOSTNAME&gt;</span>
&lt;REDACTED_IP&gt; &lt;REDACTED_HOSTNAME&gt;
&lt;REDACTED_IP&gt; &lt;REDACTED_HOSTNAME&gt;
&lt;REDACTED_IP&gt; &lt;REDACTED_HOSTNAME&gt;
&lt;REDACTED_IP&gt; &lt;REDACTED_HOSTNAME&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="package-installation-1">Package Installation</h3>

<ol>
  <li>Install pgBackRest on the backup server:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> percona-pgbackrest
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="directory-setup">Directory Setup</h3>

<ol>
  <li>Create the repository directory:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /var/lib/postgresql/pgbackup
<span class="nb">sudo chown </span>postgres:postgres /var/lib/postgresql/pgbackup
<span class="nb">sudo chmod </span>750 /var/lib/postgresql/pgbackup
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="ssh-configuration">SSH Configuration</h3>

<ol>
  <li>Set up SSH for the postgres user:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c"># Create SSH directory</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /var/lib/postgresql/.ssh
<span class="nb">sudo chown </span>postgres:postgres /var/lib/postgresql/.ssh
<span class="nb">sudo chmod </span>700 /var/lib/postgresql/.ssh

<span class="c"># Generate SSH key pair (as postgres user)</span>
<span class="nb">sudo</span> <span class="nt">-u</span> postgres ssh-keygen <span class="nt">-t</span> rsa <span class="nt">-b</span> 4096 <span class="nt">-f</span> /var/lib/postgresql/.ssh/id_rsa <span class="nt">-N</span> <span class="s2">""</span>

<span class="c"># Set proper permissions</span>
<span class="nb">sudo chmod </span>600 /var/lib/postgresql/.ssh/id_rsa
<span class="nb">sudo chmod </span>600 /var/lib/postgresql/.ssh/id_rsa.pub
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Save the public key for later use:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Display public key (save this for configuring PostgreSQL nodes)</span>
<span class="nb">sudo cat</span> /var/lib/postgresql/.ssh/id_rsa.pub
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="pgbackrest-configuration">pgBackRest Configuration</h3>

<ol>
  <li>Create the pgBackRest configuration file:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
</pre></td><td class="rouge-code"><pre><span class="c"># Create configuration file</span>
<span class="nb">sudo </span>bash <span class="nt">-c</span> <span class="s2">"cat &gt; /etc/pgbackrest.conf &lt;&lt; 'EOL'
[global]
# Repository configuration
repo1-path=/var/lib/postgresql/pgbackup
repo1-retention-archive-type=full
repo1-retention-full=1

# Server options
process-max=12
log-level-console=info
log-level-file=info
start-fast=y
delta=y
backup-standby=y

[kyc]
pg1-host=&lt;REDACTED_HOSTNAME&gt;
pg1-host-user=postgres
pg1-port=5432
pg1-path=/var/lib/postgresql/12/main
pg1-socket-path=/var/run/postgresql

pg2-host=&lt;REDACTED_HOSTNAME&gt;
pg2-host-user=postgres
pg2-port=5432
pg2-path=/var/lib/postgresql/12/main
pg2-socket-path=/var/run/postgresql

pg3-host=&lt;REDACTED_HOSTNAME&gt;
pg3-host-user=postgres
pg3-port=5432
pg3-path=/var/lib/postgresql/12/main
pg3-socket-path=/var/run/postgresql
EOL"</span>

<span class="nb">sudo chown </span>postgres:postgres /etc/pgbackrest.conf
<span class="nb">sudo chmod </span>640 /etc/pgbackrest.conf
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="create-systemd-service">Create Systemd Service</h3>

<ol>
  <li>Create a systemd service for pgBackRest:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>bash <span class="nt">-c</span> <span class="s2">"cat &gt; /etc/systemd/system/pgbackrest.service &lt;&lt; 'EOL'
[Unit]
Description=pgBackRest Server
After=network.target

[Service]
Type=simple
User=postgres
Restart=always
RestartSec=1
ExecStart=/usr/bin/pgbackrest server
ExecReload=/bin/kill -HUP </span><span class="se">\$</span><span class="s2">MAINPID

[Install]
WantedBy=multi-user.target
EOL"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Enable and start the service:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl daemon-reload
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>pgbackrest
<span class="nb">sudo </span>systemctl start pgbackrest
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="postgresql-node-configuration-for-pgbackrest">PostgreSQL Node Configuration for pgBackRest</h2>

<p>Perform these steps on each PostgreSQL node (<REDACTED_HOSTNAME>, <REDACTED_HOSTNAME>, <REDACTED_HOSTNAME>).</REDACTED_HOSTNAME></REDACTED_HOSTNAME></REDACTED_HOSTNAME></p>

<h3 id="host-configuration-1">Host Configuration</h3>

<ol>
  <li>Update the hosts file:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Add to /etc/hosts on each PostgreSQL node</span>
&lt;REDACTED_IP&gt; &lt;REDACTED_HOSTNAME&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="package-installation-2">Package Installation</h3>

<ol>
  <li>Install pgBackRest on each node:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> percona-pgbackrest
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="ssh-configuration-1">SSH Configuration</h3>

<ol>
  <li>Set up SSH for the postgres user:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c"># Create SSH directory</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /var/lib/postgresql/.ssh
<span class="nb">sudo chown </span>postgres:postgres /var/lib/postgresql/.ssh
<span class="nb">sudo chmod </span>700 /var/lib/postgresql/.ssh

<span class="c"># Create authorized_keys file</span>
<span class="nb">sudo touch</span> /var/lib/postgresql/.ssh/authorized_keys
<span class="nb">sudo chown </span>postgres:postgres /var/lib/postgresql/.ssh/authorized_keys
<span class="nb">sudo chmod </span>600 /var/lib/postgresql/.ssh/authorized_keys
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Add the pgBackRest server’s public key to authorized_keys:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Add the pgBackRest server's public key</span>
<span class="nb">sudo </span>bash <span class="nt">-c</span> <span class="s2">"echo 'PASTE_PGBACKREST_PUBLIC_KEY_HERE' &gt;&gt; /var/lib/postgresql/.ssh/authorized_keys"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Generate SSH key for the postgres user on each PostgreSQL node:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Generate SSH key pair (as postgres user)</span>
<span class="nb">sudo</span> <span class="nt">-u</span> postgres ssh-keygen <span class="nt">-t</span> rsa <span class="nt">-b</span> 4096 <span class="nt">-f</span> /var/lib/postgresql/.ssh/id_rsa <span class="nt">-N</span> <span class="s2">""</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Add the PostgreSQL node’s public key to the pgBackRest server:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Display public key (to add to pgBackRest server)</span>
<span class="nb">sudo cat</span> /var/lib/postgresql/.ssh/id_rsa.pub
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Add the PostgreSQL node’s public key to the pgBackRest server’s authorized_keys file (perform this on <REDACTED_HOSTNAME> for each PostgreSQL node):</REDACTED_HOSTNAME></li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># On &lt;REDACTED_HOSTNAME&gt;</span>
<span class="nb">sudo </span>bash <span class="nt">-c</span> <span class="s2">"echo 'PASTE_POSTGRES_NODE_PUBLIC_KEY_HERE' &gt;&gt; /var/lib/postgresql/.ssh/authorized_keys"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Set up SSH known_hosts to prevent interactive prompts:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c"># Add pgBackRest host to known_hosts</span>
<span class="nb">sudo</span> <span class="nt">-u</span> postgres ssh-keyscan <span class="nt">-t</span> rsa &lt;REDACTED_HOSTNAME&gt; <span class="o">&gt;&gt;</span> /var/lib/postgresql/.ssh/known_hosts
<span class="nb">sudo chmod </span>600 /var/lib/postgresql/.ssh/known_hosts
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="pgbackrest-client-configuration">pgBackRest Client Configuration</h3>

<ol>
  <li>Create the pgBackRest configuration file on each PostgreSQL node:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>bash <span class="nt">-c</span> <span class="s2">"cat &gt; /etc/pgbackrest.conf &lt;&lt; 'EOL'
[global]
repo1-host=&lt;REDACTED_HOSTNAME&gt;
repo1-host-user=postgres

# General options
process-max=16
log-level-console=info
log-level-file=debug

[kyc]
pg1-path=/var/lib/postgresql/12/main
EOL"</span>

<span class="nb">sudo chown </span>postgres:postgres /etc/pgbackrest.conf
<span class="nb">sudo chmod </span>640 /etc/pgbackrest.conf
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="patroni-integration">Patroni Integration</h2>

<p>Configure Patroni to use pgBackRest for WAL archiving on the primary PostgreSQL node.</p>

<ol>
  <li>Update Patroni configuration (on the primary node):</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>patronictl <span class="nt">-c</span> /etc/patroni/patroni.yml edit-config <span class="nt">-s</span> postgresql.parameters.archive_mode<span class="o">=</span>on <span class="se">\</span>
<span class="nt">-s</span> postgresql.parameters.archive_command<span class="o">=</span><span class="s2">"pgbackrest --stanza=kyc archive-push %p"</span> <span class="se">\</span>
<span class="nt">-s</span> postgresql.recovery_conf.restore_command<span class="o">=</span><span class="s2">"pgbackrest --config=/etc/pgbackrest.conf --stanza=kyc archive-get %f %p"</span> <span class="se">\</span>
<span class="nt">--force</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Reload Patroni configuration:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>patronictl <span class="nt">-c</span> /etc/patroni/patroni.yml reload kyc <span class="nt">--force</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="stanza-creation-and-initial-backup">Stanza Creation and Initial Backup</h2>

<p>After setting up all components, create the pgBackRest stanza and perform an initial backup.</p>

<ol>
  <li>Create the pgBackRest stanza (on <REDACTED_HOSTNAME>):</REDACTED_HOSTNAME></li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-iu</span> postgres pgbackrest <span class="nt">--stanza</span><span class="o">=</span>kyc stanza-create
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Create a full backup:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-iu</span> postgres pgbackrest <span class="nt">--stanza</span><span class="o">=</span>kyc <span class="nt">--type</span><span class="o">=</span>full backup
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Check the backup status:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-iu</span> postgres pgbackrest <span class="nt">--stanza</span><span class="o">=</span>kyc info
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="verification">Verification</h2>

<p>Verify that pgBackRest is properly configured and that backups are working correctly.</p>

<ol>
  <li>Check the status of the pgBackRest service on the backup server:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl status pgbackrest
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Verify that WAL archiving is working by checking the log files:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-u</span> postgres pgbackrest <span class="nt">--stanza</span><span class="o">=</span>kyc check
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Create a test backup and verify that it completes successfully:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-u</span> postgres pgbackrest <span class="nt">--stanza</span><span class="o">=</span>kyc <span class="nt">--type</span><span class="o">=</span>full backup
<span class="nb">sudo</span> <span class="nt">-u</span> postgres pgbackrest <span class="nt">--stanza</span><span class="o">=</span>kyc info
</pre></td></tr></tbody></table></code></pre></div></div>
<ol>
  <li>Add cron job for regular backups:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c"># Edit the crontab for the postgres user</span>
<span class="nb">sudo </span>crontab <span class="nt">-u</span> postgres <span class="nt">-e</span>
<span class="c"># Add the following line to schedule a daily backup at 2 AM</span>
0 2 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> /usr/bin/pgbackrest <span class="nt">--stanza</span><span class="o">=</span>kyc <span class="nt">--type</span><span class="o">=</span>full backup
</pre></td></tr></tbody></table></code></pre></div></div>
<ol>
  <li>Monitor the backup logs:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Check the backup logs</span>
<span class="nb">sudo tail</span> <span class="nt">-f</span> /var/log/pgbackrest/pgbackrest.log
</pre></td></tr></tbody></table></code></pre></div></div>
<ol>
  <li>Test the restore process:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c"># Stop the PostgreSQL service on the primary node</span>
<span class="nb">sudo </span>systemctl stop patroni
<span class="c"># Restore the backup</span>
<span class="nb">sudo</span> <span class="nt">-u</span> postgres pgbackrest <span class="nt">--stanza</span><span class="o">=</span>kyc restore
<span class="c"># Start the PostgreSQL service</span>
<span class="nb">sudo </span>systemctl start patroni
<span class="c"># Check the status of the Patroni cluster</span>
patronictl <span class="nt">-c</span> /etc/patroni/patroni.yml list
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="monitoring-postgresql-with-percona-monitoring-and-management-pmm">Monitoring PostgreSQL with Percona Monitoring and Management (PMM)</h2>

<p>This section covers setting up Percona Monitoring and Management (PMM) to monitor your PostgreSQL cluster, providing insights into performance, health, and resource utilization. The prerequisites are given below.</p>

<ul>
  <li>A dedicated server for PMM Server (can be installed on <REDACTED_HOSTNAME>)</REDACTED_HOSTNAME></li>
  <li>Network connectivity between PostgreSQL nodes and PMM server</li>
  <li>Minimum requirements for PMM server:
    <ul>
      <li>2 CPU cores</li>
      <li>4 GB RAM</li>
      <li>100 GB disk space</li>
    </ul>
  </li>
</ul>

<h3 id="pmm-server-installation-on-">PMM Server Installation on <REDACTED_HOSTNAME></REDACTED_HOSTNAME></h3>

<ol>
  <li>Install Docker prerequisites:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c"># Update package lists</span>
<span class="nb">sudo </span>apt update

<span class="c"># Install prerequisites</span>
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> apt-transport-https ca-certificates curl software-properties-common gnupg
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Add Docker repository:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c"># Add Docker's official GPG key</span>
curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg | <span class="nb">sudo </span>apt-key add -

<span class="c"># Add Docker repository</span>
<span class="nb">sudo </span>add-apt-repository <span class="s2">"deb [arch=amd64] https://download.docker.com/linux/ubuntu </span><span class="si">$(</span>lsb_release <span class="nt">-cs</span><span class="si">)</span><span class="s2"> stable"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Install Docker:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> docker-ce docker-ce-cli containerd.io
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>docker
<span class="nb">sudo </span>systemctl start docker
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Install PMM Server using the easy install script:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>curl <span class="nt">-fsSL</span> https://raw.githubusercontent.com/percona/&lt;REDACTED_USERNAME&gt;/refs/heads/v3/get-&lt;REDACTED_USERNAME&gt;.sh | <span class="nb">sudo </span>bash
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Wait for PMM Server to become available (this may take a few minutes):</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># You can monitor the container status</span>
<span class="nb">sudo </span>docker ps | <span class="nb">grep</span> &lt;REDACTED_USERNAME&gt;-server
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Change the default admin password:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>docker <span class="nb">exec</span> <span class="nt">-t</span> &lt;REDACTED_USERNAME&gt;-server change-admin-password &lt;REDACTED_PASSWORD&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Record your PMM Server access information:
    <ul>
      <li>URL: https://YOUR_SERVER_IP:443</li>
      <li>Username: admin</li>
      <li>Password: <REDACTED_PASSWORD> (use your chosen password)</REDACTED_PASSWORD></li>
    </ul>
  </li>
</ol>

<h3 id="install-pmm-client-on-postgresql-nodes">Install PMM Client on PostgreSQL Nodes</h3>

<p>Perform these steps on each PostgreSQL node (<REDACTED_HOSTNAME>, <REDACTED_HOSTNAME>, <REDACTED_HOSTNAME>):</REDACTED_HOSTNAME></REDACTED_HOSTNAME></REDACTED_HOSTNAME></p>

<ol>
  <li>Add Percona repository:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>wget <span class="nt">-O</span> - https://repo.percona.com/apt/percona-release_latest.generic_all.deb <span class="o">&gt;</span> /tmp/percona-release.deb
<span class="nb">sudo </span>dpkg <span class="nt">-i</span> /tmp/percona-release.deb
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Enable PMM client repository:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>percona-release <span class="nb">enable</span> &lt;REDACTED_USERNAME&gt;3-client release
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Install PMM Client:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> &lt;REDACTED_USERNAME&gt;-client
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Install pg_stat_monitor package:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt <span class="nb">install</span> <span class="nt">-y</span> percona-pg-stat-monitor12
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="configure-postgresql-for-monitoring">Configure PostgreSQL for Monitoring</h3>

<p>Perform these steps on each PostgreSQL node:</p>

<ol>
  <li>Create PostgreSQL monitoring user:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-u</span> postgres psql <span class="nt">-c</span> <span class="s2">"CREATE USER &lt;REDACTED_USERNAME&gt; WITH SUPERUSER PASSWORD '&lt;REDACTED_PASSWORD&gt;';"</span> <span class="o">||</span> <span class="nb">sudo</span> <span class="nt">-u</span> postgres psql <span class="nt">-c</span> <span class="s2">"ALTER USER &lt;REDACTED_USERNAME&gt; WITH SUPERUSER PASSWORD '&lt;REDACTED_PASSWORD&gt;';"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Update pg_hba.conf to allow PMM user local access:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Add this line to /var/lib/postgresql/12/main/pg_hba.conf</span>
<span class="nb">echo</span> <span class="s2">"local   all             &lt;REDACTED_USERNAME&gt;                                     md5"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /var/lib/postgresql/12/main/pg_hba.conf
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Enable pg_stat_monitor in shared_preload_libraries:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c"># Update Patroni configuration</span>
<span class="nb">sudo </span>patronictl <span class="nt">-c</span> /etc/patroni/patroni.yml edit-config <span class="nt">-s</span> postgresql.parameters.shared_preload_libraries<span class="o">=</span><span class="s2">"pg_stat_monitor"</span> <span class="se">\</span>
<span class="nt">-s</span> postgresql.parameters.pg_stat_monitor.pgsm_query_max_len<span class="o">=</span>2048 <span class="se">\</span>
<span class="nt">--force</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Apply configuration changes:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c"># Reload Patroni configuration</span>
<span class="nb">sudo </span>patronictl <span class="nt">-c</span> /etc/patroni/patroni.yml reload kyc <span class="nt">--force</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Create the pg_stat_monitor extension:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-u</span> postgres psql <span class="nt">-d</span> postgres <span class="nt">-c</span> <span class="s1">'CREATE EXTENSION IF NOT EXISTS pg_stat_monitor;'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="configure-pmm-client-on-postgresql-nodes">Configure PMM Client on PostgreSQL Nodes</h3>

<p>Perform these steps on each PostgreSQL node:</p>

<ol>
  <li>Configure PMM Client to connect to the PMM Server:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> &lt;REDACTED_USERNAME&gt;-admin config <span class="nt">--server-insecure-tls</span> <span class="nt">--server-url</span><span class="o">=</span>https://admin:&lt;REDACTED_PASSWORD&gt;@PMM_SERVER_IP:443 <span class="nt">--force</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Add PostgreSQL monitoring:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c"># Remove any existing PostgreSQL monitoring (optional)</span>
<span class="nb">sudo</span> &lt;REDACTED_USERNAME&gt;-admin remove postgresql <span class="o">||</span> <span class="nb">true</span>

<span class="c"># Add PostgreSQL monitoring</span>
<span class="nb">sudo</span> &lt;REDACTED_USERNAME&gt;-admin add postgresql <span class="nt">--username</span><span class="o">=</span>&lt;REDACTED_USERNAME&gt; <span class="nt">--password</span><span class="o">=</span>&lt;REDACTED_PASSWORD&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Verify PMM Client status:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> &lt;REDACTED_USERNAME&gt;-admin status
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="verify-monitoring-setup">Verify Monitoring Setup</h2>

<ol>
  <li>
    <p>Access the PMM Web UI at <code class="language-plaintext highlighter-rouge">https://[PMM-Server-IP]</code> using the credentials you set.</p>
  </li>
  <li>Navigate to the PostgreSQL dashboards to verify that your nodes are being monitored:
    <ul>
      <li>PostgreSQL Instance Summary</li>
      <li>PostgreSQL Database Activity</li>
      <li>PostgreSQL Query Analytics</li>
      <li>High Availability dashboard</li>
    </ul>
  </li>
  <li>Check that all PostgreSQL nodes are properly reporting metrics:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> &lt;REDACTED_USERNAME&gt;-admin list
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="setting-up-alerts">Setting Up Alerts</h2>

<ol>
  <li>Navigate to the PMM web interface</li>
  <li>Go to Configuration → Alert Rules</li>
  <li>Create alert rules for:
    <ul>
      <li>Replication lag</li>
      <li>High CPU/Memory usage</li>
      <li>Disk space utilization</li>
      <li>Connection pool saturation</li>
    </ul>
  </li>
</ol>

<h2 id="maintenance-tips">Maintenance Tips</h2>

<ol>
  <li>Create a maintenance script for regular cleanup:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>bash <span class="nt">-c</span> <span class="s2">"cat &gt; /usr/local/bin/&lt;REDACTED_USERNAME&gt;-maintenance.sh &lt;&lt; 'EOL'
#!/bin/bash
# Purge old monitoring data (adjust retention as needed)
&lt;REDACTED_USERNAME&gt;-admin maintenance --retention 14d

# Check PMM client status
&lt;REDACTED_USERNAME&gt;-admin list
EOL"</span>

<span class="nb">sudo chmod</span> +x /usr/local/bin/&lt;REDACTED_USERNAME&gt;-maintenance.sh
</pre></td></tr></tbody></table></code></pre></div></div>

<ol>
  <li>Add a weekly cron job:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="o">(</span>crontab <span class="nt">-l</span> 2&gt;/dev/null<span class="p">;</span> <span class="nb">echo</span> <span class="s2">"0 2 * * 0 /usr/local/bin/&lt;REDACTED_USERNAME&gt;-maintenance.sh &gt; /var/log/&lt;REDACTED_USERNAME&gt;-maintenance.log 2&gt;&amp;1"</span><span class="o">)</span> | crontab -
</pre></td></tr></tbody></table></code></pre></div></div>

<h1 id="conclusion">Conclusion</h1>

<p>By following this guide, you have successfully set up a highly available PostgreSQL cluster using Patroni, etcd, HAProxy, and Keepalived. This configuration provides automatic failover, load balancing, and high availability for PostgreSQL databases, ensuring that your applications can rely on a robust and resilient database infrastructure. You can now deploy your applications with confidence, knowing that your database cluster is capable of handling failures and maintaining consistent performance under various conditions.</p>]]></content><author><name>Moin Uddin Ahmed</name></author><category term="PostgreSQL" /><category term="backup" /><category term="etcd" /><category term="haproxy" /><category term="high-availability" /><category term="patroni" /><category term="pgbackrest" /><category term="postgresql" /><summary type="html"><![CDATA[1. Overview 2. Architecture 3. Prerequisites 4. Initial Setup - Hostname Configuration - Package Installation]]></summary></entry><entry><title type="html">PostgreSQL 17 + Pgpool-II HA Cluster — Configuration Guide</title><link href="https://marufmoinuddin.github.io/blog/2026/06/postgresql-17-pgpool2-ha-cluster-configuration-guide/" rel="alternate" type="text/html" title="PostgreSQL 17 + Pgpool-II HA Cluster — Configuration Guide" /><published>2026-06-10T00:00:00+00:00</published><updated>2026-06-10T00:00:00+00:00</updated><id>https://marufmoinuddin.github.io/blog/2026/06/postgresql-17-pgpool2-ha-cluster-configuration-guide</id><content type="html" xml:base="https://marufmoinuddin.github.io/blog/2026/06/postgresql-17-pgpool2-ha-cluster-configuration-guide/"><![CDATA[<h1 id="postgresql-17--pgpool-ii-ha-cluster--configuration-guide">PostgreSQL 17 + Pgpool-II HA Cluster — Configuration Guide</h1>

<h2 id="environment">Environment</h2>

<h3 id="cluster-topology">Cluster Topology</h3>

<table>
  <thead>
    <tr>
      <th>Role</th>
      <th>Hostname</th>
      <th>IP</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Backend Primary</td>
      <td><code class="language-plaintext highlighter-rouge">db-node-01</code></td>
      <td><code class="language-plaintext highlighter-rouge">10.0.0.1</code></td>
    </tr>
    <tr>
      <td>Backend Standby 1</td>
      <td><code class="language-plaintext highlighter-rouge">db-node-02</code></td>
      <td><code class="language-plaintext highlighter-rouge">10.0.0.2</code></td>
    </tr>
    <tr>
      <td>Backend Standby 2</td>
      <td><code class="language-plaintext highlighter-rouge">db-node-03</code></td>
      <td><code class="language-plaintext highlighter-rouge">10.0.0.3</code></td>
    </tr>
    <tr>
      <td>Virtual IP (VIP)</td>
      <td>—</td>
      <td><code class="language-plaintext highlighter-rouge">10.0.0.100</code></td>
    </tr>
  </tbody>
</table>

<table>
  <thead>
    <tr>
      <th>Service</th>
      <th>Port</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Pgpool Port</td>
      <td><code class="language-plaintext highlighter-rouge">9999</code></td>
    </tr>
    <tr>
      <td>PCP Port</td>
      <td><code class="language-plaintext highlighter-rouge">9898</code></td>
    </tr>
  </tbody>
</table>

<ul>
  <li><strong>Network Interface:</strong> <code class="language-plaintext highlighter-rouge">ens160</code></li>
  <li><strong>Hardware:</strong> 12 vCPU / 20 GB RAM / 80 GB Storage</li>
</ul>

<hr />

<h2 id="step-0--download-and-install-the-rpms-all-3-nodes">STEP 0 — Download and Install the RPMs (All 3 Nodes)</h2>

<h3 id="postgresql-17-installation-all-3-nodes">PostgreSQL 17 Installation (All 3 Nodes)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
</pre></td><td class="rouge-code"><pre><span class="c"># Add PostgreSQL repository (EL-9)</span>
<span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm

<span class="c"># Disable built-in PostgreSQL module</span>
<span class="nb">sudo </span>dnf <span class="nt">-qy</span> module disable postgresql

<span class="c"># Install PostgreSQL 17</span>
<span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> postgresql17-server postgresql17-contrib

<span class="c"># Prepare PGDATA directory</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /data/pgdata/17/data
<span class="nb">sudo chown</span> <span class="nt">-R</span> postgres:postgres /data/pgdata

<span class="c"># Set PGDATA in systemd</span>
<span class="nb">sudo </span>systemctl edit postgresql-17.service
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Add in the editor:</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nn">[Service]</span>
<span class="py">Environment</span><span class="p">=</span><span class="s">PGDATA=/data/pgdata/17/data</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl daemon-reload

<span class="c"># Initialize database cluster</span>
<span class="nb">sudo</span> /usr/pgsql-17/bin/postgresql-17-setup initdb

<span class="c"># Enable and start PostgreSQL</span>
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>postgresql-17.service
<span class="nb">sudo </span>systemctl start postgresql-17.service

<span class="c"># Add Pgpool repository (4.5 for PG17)</span>
<span class="nb">sudo </span>yum <span class="nb">install</span> <span class="nt">-y</span> https://www.pgpool.net/yum/rpms/4.5/redhat/rhel-9-x86_64/pgpool-II-release-4.5-1.noarch.rpm

<span class="c"># If GPG check issue arises</span>
<span class="nb">sudo </span>vim /etc/yum.repos.d/pgpool-II-release-45.repo
<span class="c"># Set: gpgcheck=0</span>

<span class="c"># Install Pgpool for PostgreSQL 17</span>
<span class="nb">sudo </span>yum <span class="nb">install</span> <span class="nt">-y</span> pgpool-II-pg17 pgpool-II-pg17-extensions pgpool-II-pg17-devel

<span class="c"># If dependency issues arise</span>
wget https://rpmfind.net/linux/centos-stream/9-stream/CRB/x86_64/os/Packages/libmemcached-awesome-1.1.0-12.el9.x86_64.rpm
wget https://rpmfind.net/linux/centos-stream/9-stream/BaseOS/x86_64/os/Packages/libcap-2.48-9.el9.x86_64.rpm
<span class="nb">sudo </span>yum <span class="nb">install</span> ./libmemcached-awesome-<span class="k">*</span>.rpm ./libcap-<span class="k">*</span>.rpm
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="step-1--prepare-data-directory-all-3-nodes">STEP 1 — Prepare Data Directory (All 3 Nodes)</h2>

<p>Run on all three nodes before doing anything else.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c"># Create the PostgreSQL data directory</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /data/pgdata/17/data
<span class="nb">sudo chown</span> <span class="nt">-R</span> postgres:postgres /data/pgdata
<span class="nb">sudo chmod </span>700 /data/pgdata/17/data

<span class="c"># Create log directories for Pgpool</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /var/log/pgpool_log/oiddir
<span class="nb">sudo chown</span> <span class="nt">-R</span> postgres:postgres /var/log/pgpool_log
<span class="nb">sudo chmod </span>755 /var/log/pgpool_log

<span class="c"># Create Pgpool PID directory</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /var/run/pgpool-II
<span class="nb">sudo chown </span>postgres:postgres /var/run/pgpool-II
<span class="nb">sudo chmod </span>755 /var/run/pgpool-II
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="step-2--configure-pgdata-in-systemd-all-3-nodes">STEP 2 — Configure PGDATA in Systemd (All 3 Nodes)</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl edit postgresql-17.service
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Add the following block:</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nn">[Service]</span>
<span class="py">Environment</span><span class="p">=</span><span class="s">PGDATA=/data/pgdata/17/data</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl daemon-reload
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="step-3--initialize-postgresql-primary-node-10001-only">STEP 3 — Initialize PostgreSQL (Primary Node: 10.0.0.1 Only)</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> /usr/pgsql-17/bin/postgresql-17-setup initdb
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>postgresql-17.service
<span class="nb">sudo </span>systemctl start postgresql-17.service
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="step-4--postgresql-tuning-config-all-nodes">STEP 4 — PostgreSQL Tuning Config (All Nodes)</h2>

<p>All custom PostgreSQL parameters go into <code class="language-plaintext highlighter-rouge">/data/pgdata/17/data/conf.d/</code> so the default <code class="language-plaintext highlighter-rouge">postgresql.conf</code> is never touched. PostgreSQL automatically reads all <code class="language-plaintext highlighter-rouge">*.conf</code> files in this directory via the <code class="language-plaintext highlighter-rouge">include_dir</code> directive.</p>

<h3 id="41--enable-confd-include-once-on-primary">4.1 — Enable conf.d include (once, on primary)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c"># Add include_dir to the bottom of postgresql.conf (one-time only)</span>
<span class="nb">echo</span> <span class="s2">"include_dir = 'conf.d'"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /data/pgdata/17/data/postgresql.conf
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /data/pgdata/17/data/conf.d
<span class="nb">sudo chown </span>postgres:postgres /data/pgdata/17/data/conf.d
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="42--create-the-tuning-file">4.2 — Create the tuning file</h3>

<blockquote>
  <p><strong>Reference:</strong> <a href="https://pgtune.leopard.in.ua/">https://pgtune.leopard.in.ua/</a> — calculate exact values for your server.</p>
</blockquote>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-u</span> postgres <span class="nb">tee</span> /data/pgdata/17/data/conf.d/01-performance.conf <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
# =============================================================
# PostgreSQL 17 Tuning — 12 vCPU / 20 GB RAM / 80 GB Storage
# =============================================================

# ============================================================
# Connection Settings
# ============================================================
max_connections = 3200
reserved_connections = 10
superuser_reserved_connections = 5
password_encryption = scram-sha-256

# ============================================================
# Memory
# ============================================================
shared_buffers = 8GB
huge_pages = try
work_mem = 4MB
maintenance_work_mem = 2GB
autovacuum_work_mem = 2GB
temp_buffers = 16MB
effective_cache_size = 24GB

# ============================================================
# Parallelism
# ============================================================
max_worker_processes = 16
max_parallel_workers_per_gather = 4
max_parallel_workers = 16
max_parallel_maintenance_workers = 4
parallel_setup_cost = 500

# ============================================================
# Query Planner
# ============================================================
random_page_cost = 1.1
effective_io_concurrency = 200
cpu_tuple_cost = 0.01
cpu_index_tuple_cost = 0.005
default_statistics_target = 100
jit = off

# ============================================================
# WAL &amp; Checkpoints
# ============================================================
wal_level = replica
wal_buffers = 16MB
wal_compression = lz4
wal_keep_size = 4096
wal_sender_timeout = 60s
min_wal_size = 1GB
max_wal_size = 4GB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9
synchronous_commit = local

# ============================================================
# Replication
# ============================================================
max_wal_senders = 10
max_replication_slots = 10
hot_standby = on
hot_standby_feedback = on

# ============================================================
# Autovacuum
# ============================================================
autovacuum = on
autovacuum_max_workers = 4
autovacuum_naptime = 30s
autovacuum_vacuum_cost_delay = 2ms
autovacuum_vacuum_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.02
log_autovacuum_min_duration = 250ms

# ============================================================
# Logging
# ============================================================
log_destination = 'stderr'
logging_collector = on
log_directory = 'log'
log_filename = 'postgresql-%a.log'
log_rotation_age = 1d
log_rotation_size = 1GB
log_truncate_on_rotation = on
log_min_duration_statement = 1000
log_checkpoints = on
log_connections = off
log_disconnections = off
log_lock_waits = on
log_temp_files = 10MB
log_error_verbosity = verbose
log_line_prefix = '%m [%p]: [%l-1] db=%d,user=%u,app=%a,client=%h'
log_statement = 'ddl'
log_replication_commands = on
log_timezone = 'Your/Timezone'

# ============================================================
# Session Behavior
# ============================================================
idle_in_transaction_session_timeout = 60000
lock_timeout = 0
statement_timeout = 0
track_io_timing = on
track_activity_query_size = 4096
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="step-5--pg_hbaconf-all-3-nodes">STEP 5 — pg_hba.conf (All 3 Nodes)</h2>

<h3 id="51--on-the-primary-edit-now-on-standbys-this-file-will-come-from-pg_basebackup">5.1 — On the primary, edit now. On standbys, this file will come from pg_basebackup.</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-u</span> postgres <span class="nb">tee</span> /data/pgdata/17/data/conf.d/02-hba-rules.conf <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
# This file does NOT override pg_hba.conf.
# HBA rules must be added directly to pg_hba.conf.
# This file documents what was added for reference only.
#
# Lines added to /data/pgdata/17/data/pg_hba.conf:
#   host replication repl_user 10.0.0.0/24 scram-sha-256
#   host all          pgpool   10.0.0.0/24 scram-sha-256
#   host all          app_user 10.0.0.0/24 scram-sha-256
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>Now actually edit <code class="language-plaintext highlighter-rouge">pg_hba.conf</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-u</span> postgres <span class="nb">tee</span> <span class="nt">-a</span> /data/pgdata/17/data/pg_hba.conf <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'

# --- Custom rules ---
host replication repl_user 10.0.0.0/24 scram-sha-256
host all          pgpool   10.0.0.0/24 scram-sha-256
host all          app_user 10.0.0.0/24 scram-sha-256
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>Restart PostgreSQL to apply:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl restart postgresql-17.service
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="step-6--create-postgresql-users-and-database-primary-10001-only">STEP 6 — Create PostgreSQL Users and Database (Primary: 10.0.0.1 Only)</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>su - postgres
psql
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Run in <code class="language-plaintext highlighter-rouge">psql</code>:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="c1">-- Replication user</span>
<span class="k">SET</span> <span class="n">password_encryption</span> <span class="o">=</span> <span class="s1">'scram-sha-256'</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">ROLE</span> <span class="n">repl_user</span> <span class="k">WITH</span> <span class="n">REPLICATION</span> <span class="n">LOGIN</span><span class="p">;</span>
<span class="err">\</span><span class="n">password</span> <span class="n">repl_user</span>
<span class="c1">-- Enter: ChangeMe123!</span>

<span class="c1">-- Pgpool health/SR check user</span>
<span class="k">CREATE</span> <span class="k">ROLE</span> <span class="n">pgpool</span> <span class="k">WITH</span> <span class="n">LOGIN</span><span class="p">;</span>
<span class="err">\</span><span class="n">password</span> <span class="n">pgpool</span>
<span class="c1">-- Enter: ChangeMe123!</span>
<span class="k">GRANT</span> <span class="n">pg_monitor</span> <span class="k">TO</span> <span class="n">pgpool</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">CONNECT</span> <span class="k">ON</span> <span class="k">DATABASE</span> <span class="n">postgres</span> <span class="k">TO</span> <span class="n">pgpool</span><span class="p">;</span>

<span class="c1">-- Application database and user</span>
<span class="k">CREATE</span> <span class="k">DATABASE</span> <span class="n">appdb</span><span class="p">;</span>
<span class="k">SET</span> <span class="n">password_encryption</span> <span class="o">=</span> <span class="s1">'scram-sha-256'</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">ROLE</span> <span class="n">app_user</span> <span class="k">WITH</span> <span class="n">LOGIN</span><span class="p">;</span>
<span class="err">\</span><span class="n">password</span> <span class="n">app_user</span>
<span class="c1">-- Enter: AppUserPass123!</span>
<span class="k">GRANT</span> <span class="k">CONNECT</span> <span class="k">ON</span> <span class="k">DATABASE</span> <span class="n">appdb</span> <span class="k">TO</span> <span class="n">app_user</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">USAGE</span> <span class="k">ON</span> <span class="k">SCHEMA</span> <span class="k">public</span> <span class="k">TO</span> <span class="n">app_user</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">SELECT</span><span class="p">,</span> <span class="k">INSERT</span><span class="p">,</span> <span class="k">UPDATE</span><span class="p">,</span> <span class="k">DELETE</span> <span class="k">ON</span> <span class="k">ALL</span> <span class="n">TABLES</span> <span class="k">IN</span> <span class="k">SCHEMA</span> <span class="k">public</span> <span class="k">TO</span> <span class="n">app_user</span><span class="p">;</span>
<span class="k">ALTER</span> <span class="k">DEFAULT</span> <span class="k">PRIVILEGES</span> <span class="k">IN</span> <span class="k">SCHEMA</span> <span class="k">public</span> <span class="k">GRANT</span> <span class="k">ALL</span> <span class="k">PRIVILEGES</span> <span class="k">ON</span> <span class="n">TABLES</span> <span class="k">TO</span> <span class="n">app_user</span><span class="p">;</span>

<span class="c1">-- Verify</span>
<span class="err">\</span><span class="n">du</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">exit</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="step-7--initialize-standby-nodes-db02-and-db03-only">STEP 7 — Initialize Standby Nodes (DB02 and DB03 Only)</h2>

<p>Run the following on <code class="language-plaintext highlighter-rouge">10.0.0.2</code> and <code class="language-plaintext highlighter-rouge">10.0.0.3</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
</pre></td><td class="rouge-code"><pre><span class="c"># Stop PostgreSQL if running</span>
<span class="nb">sudo </span>systemctl stop postgresql-17 <span class="o">||</span> <span class="nb">true</span>

<span class="c"># Remove old data directory</span>
<span class="nb">sudo rm</span> <span class="nt">-rf</span> /data/pgdata/17/data/<span class="k">*</span>

<span class="c"># Take base backup from primary (includes conf.d, pg_hba.conf, everything)</span>
<span class="nb">sudo</span> <span class="nt">-u</span> postgres pg_basebackup <span class="se">\</span>
  <span class="nt">-h</span> 10.0.0.1 <span class="se">\</span>
  <span class="nt">-U</span> repl_user <span class="se">\</span>
  <span class="nt">-p</span> 5432 <span class="se">\</span>
  <span class="nt">-D</span> /data/pgdata/17/data/ <span class="se">\</span>
  <span class="nt">-Fp</span> <span class="nt">-Xs</span> <span class="nt">-P</span> <span class="nt">-R</span>
<span class="c"># Password: ChangeMe123!</span>
<span class="c"># -R creates standby.signal and populates primary_conninfo automatically</span>

<span class="c"># Start standby</span>
<span class="nb">sudo </span>systemctl start postgresql-17
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>postgresql-17
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="71--verify-replication-back-on-primary-10001">7.1 — Verify Replication (back on Primary: 10.0.0.1)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-u</span> postgres psql <span class="nt">-c</span> <span class="s2">"SELECT client_addr, state, sync_state, sent_lsn, write_lsn, flush_lsn, replay_lsn FROM pg_stat_replication;"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Both standbys should appear with <code class="language-plaintext highlighter-rouge">state = streaming</code>.</p>

<hr />

<h2 id="step-8--configure-firewall-all-3-nodes">STEP 8 — Configure Firewall (All 3 Nodes)</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="rouge-code"><pre><span class="c"># PostgreSQL</span>
<span class="nb">sudo </span>firewall-cmd <span class="nt">--permanent</span> <span class="nt">--add-port</span><span class="o">=</span>5432/tcp

<span class="c"># Pgpool client port</span>
<span class="nb">sudo </span>firewall-cmd <span class="nt">--permanent</span> <span class="nt">--add-port</span><span class="o">=</span>9999/tcp

<span class="c"># PCP admin port</span>
<span class="nb">sudo </span>firewall-cmd <span class="nt">--permanent</span> <span class="nt">--add-port</span><span class="o">=</span>9898/tcp

<span class="c"># Watchdog</span>
<span class="nb">sudo </span>firewall-cmd <span class="nt">--permanent</span> <span class="nt">--add-port</span><span class="o">=</span>9000/tcp

<span class="c"># Heartbeat (UDP)</span>
<span class="nb">sudo </span>firewall-cmd <span class="nt">--permanent</span> <span class="nt">--add-port</span><span class="o">=</span>9694/udp

<span class="c"># Trust internal subnet</span>
<span class="nb">sudo </span>firewall-cmd <span class="nt">--permanent</span> <span class="nt">--zone</span><span class="o">=</span>trusted <span class="nt">--add-source</span><span class="o">=</span>10.0.0.0/24

<span class="nb">sudo </span>firewall-cmd <span class="nt">--reload</span>
<span class="nb">sudo </span>firewall-cmd <span class="nt">--list-all</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="step-9--pgpool-ii-configuration">STEP 9 — Pgpool-II Configuration</h2>

<h3 id="91--set-node-id-each-node--different-per-node">9.1 — Set Node ID (Each Node — Different Per Node)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c"># On 10.0.0.1 (Node 0)</span>
<span class="nb">echo </span>0 | <span class="nb">sudo tee</span> /etc/pgpool-II/pgpool_node_id

<span class="c"># On 10.0.0.2 (Node 1)</span>
<span class="nb">echo </span>1 | <span class="nb">sudo tee</span> /etc/pgpool-II/pgpool_node_id

<span class="c"># On 10.0.0.3 (Node 2)</span>
<span class="nb">echo </span>2 | <span class="nb">sudo tee</span> /etc/pgpool-II/pgpool_node_id

<span class="c"># On ALL nodes after setting the ID:</span>
<span class="nb">sudo chown </span>postgres:postgres /etc/pgpool-II/pgpool_node_id
<span class="nb">sudo chmod </span>644 /etc/pgpool-II/pgpool_node_id
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="92--configure-pool_hbaconf-all-3-nodes">9.2 — Configure pool_hba.conf (All 3 Nodes)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="nb">sudo tee</span> /etc/pgpool-II/pool_hba.conf <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
# TYPE  DATABASE  USER       CIDR-ADDRESS  METHOD
local   all       all                      trust
host    all       postgres   0.0.0.0/0     trust
host    all       pgpool     0.0.0.0/0     scram-sha-256
host    all       app_user   0.0.0.0/0     scram-sha-256
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="93--create-pgpool-encryption-key-and-pool_passwd-all-3-nodes">9.3 — Create Pgpool Encryption Key and pool_passwd (All 3 Nodes)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>su - postgres

<span class="c"># Create decryption key file</span>
<span class="nb">cat</span> <span class="o">&gt;</span> ~/.pgpoolkey <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
YourEncryptionKey
</span><span class="no">EOF
</span><span class="nb">chmod </span>600 ~/.pgpoolkey

<span class="c"># Encrypt pgpool user password</span>
pg_enc <span class="nt">-m</span> <span class="nt">-k</span> ~/.pgpoolkey <span class="nt">-u</span> pgpool <span class="nt">-p</span>
<span class="c"># Enter: ChangeMe123!</span>

<span class="c"># Encrypt application user password</span>
pg_enc <span class="nt">-m</span> <span class="nt">-k</span> ~/.pgpoolkey <span class="nt">-u</span> app_user <span class="nt">-p</span>
<span class="c"># Enter: AppUserPass123!</span>

<span class="c"># Verify both users appear</span>
<span class="nb">cat</span> /etc/pgpool-II/pool_passwd
<span class="c"># Should show two AES-encrypted lines</span>

<span class="nb">exit</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="94--configure-pcpconf-all-3-nodes">9.4 — Configure pcp.conf (All 3 Nodes)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c"># Generate MD5 hash of the PCP password</span>
<span class="c"># Replace 'ChangeMe123!' with your actual PCP password</span>
<span class="nb">echo</span> <span class="nt">-n</span> <span class="s1">'ChangeMe123!'</span> | <span class="nb">md5sum</span> | <span class="nb">awk</span> <span class="s1">'{print $1}'</span>
<span class="c"># Example output: a7eb3760e8253f69f17dcfa5e0c3d0eb</span>

<span class="c"># Add to pcp.conf</span>
<span class="nb">sudo tee</span> /etc/pgpool-II/pcp.conf <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
pgpool:a7eb3760e8253f69f17dcfa5e0c3d0eb
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="95--create-pcppass-all-3-nodes">9.5 — Create .pcppass (All 3 Nodes)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>su - postgres
<span class="nb">cat</span> <span class="o">&gt;</span> ~/.pcppass <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
*:*:pgpool:ChangeMe123!
</span><span class="no">EOF
</span><span class="nb">chmod </span>600 ~/.pcppass
<span class="nb">exit</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="96--configure-sudoers-for-vip-all-3-nodes">9.6 — Configure Sudoers for VIP (All 3 Nodes)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>visudo
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Add at the end:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>postgres ALL = NOPASSWD: /sbin/ip, /usr/sbin/arping
Defaults:postgres !requiretty
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="97--configure-systemd-file-limits-all-3-nodes">9.7 — Configure Systemd File Limits (All 3 Nodes)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl edit pgpool.service
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Add:</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nn">[Service]</span>
<span class="py">LimitNOFILE</span><span class="p">=</span><span class="s">131072</span>
<span class="py">LimitNPROC</span><span class="p">=</span><span class="s">131072</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl daemon-reload
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="98--deploy-pgpoolconf-all-3-nodes">9.8 — Deploy pgpool.conf (All 3 Nodes)</h3>

<p>Pgpool does not support a <code class="language-plaintext highlighter-rouge">conf.d</code> include by default. Place the full custom configuration in <code class="language-plaintext highlighter-rouge">/etc/pgpool-II/pgpool.conf</code> — back up the default first.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo cp</span> /etc/pgpool-II/pgpool.conf /etc/pgpool-II/pgpool.conf.default
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
</pre></td><td class="rouge-code"><pre><span class="nb">sudo tee</span> /etc/pgpool-II/pgpool.conf <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
# =============================================================
# Pgpool-II 4.5 Configuration — HA Cluster
# Generated for: 12 vCPU / 20GB RAM, 3-node watchdog
# =============================================================

# ---------------- CLUSTERING MODE ----------------
backend_clustering_mode = 'streaming_replication'

# ---------------- CONNECTIONS ----------------
listen_addresses = '*'
port = 9999
reserved_connections = 50
listen_backlog_multiplier = 2
serialize_accept = off
pcp_listen_addresses = '*'
pcp_port = 9898
unix_socket_directories = '/var/run/pgpool-II'
pcp_socket_dir = '/var/run/pgpool-II'
wd_ipc_socket_dir = '/var/run/pgpool-II'

# ---------------- CONNECTION POOLING ----------------
num_init_children = 1000
max_pool = 2
child_life_time = 5
minchild_max_connections = 1000
connection_life_time = 600
client_idle_limit = 5min
connection_cache = on
reset_query_list = 'ABORT; DISCARD ALL'

# ---------------- AUTHENTICATION ----------------
enable_pool_hba = on
pool_passwd = 'pool_passwd'
authentication_timeout = 1min

# ---------------- BACKEND (POSTGRESQL) NODES ----------------
backend_hostname0 = '10.0.0.1'
backend_port0 = 5432
backend_weight0 = 1
backend_data_directory0 = '/data/pgdata/17/data'
backend_flag0 = 'ALLOW_TO_FAILOVER'
backend_application_name0 = 'db-node-01'

backend_hostname1 = '10.0.0.2'
backend_port1 = 5432
backend_weight1 = 2
backend_data_directory1 = '/data/pgdata/17/data'
backend_flag1 = 'ALLOW_TO_FAILOVER'
backend_application_name1 = 'db-node-02'

backend_hostname2 = '10.0.0.3'
backend_port2 = 5432
backend_weight2 = 2
backend_data_directory2 = '/data/pgdata/17/data'
backend_flag2 = 'ALLOW_TO_FAILOVER'
backend_application_name2 = 'db-node-03'

# ---------------- REPLICATION MODE ----------------
replicate_select = off

# ---------------- LOGGING ----------------
log_destination = 'stderr'
logging_collector = on
log_hostname = on
log_connections = off
log_disconnections = off
log_pcp_processes = on
log_per_node_statement = off
log_statement = off
log_client_messages = off
log_standby_delay = 'if_over_threshold'
log_error_verbosity = default
log_min_messages = warning
log_directory = '/data/log/pgpool'
log_filename = 'pgpool-%Y-%m-%d.log'
log_truncate_on_rotation = on
log_rotation_age = 1d
log_rotation_size = 0
pid_file_name = '/var/run/pgpool-II/pgpool.pid'

# ---------------- LOAD BALANCING ----------------
load_balance_mode = on
ignore_leading_white_space = on
allow_sql_comments = off
disable_load_balance_on_write = 'transaction'
statement_level_load_balance = on
black_function_list = 'currval,lastval,nextval,setval'

# ---------------- STREAMING REPLICATION CHECK ----------------
sr_check_period = 10
sr_check_user = 'pgpool'
sr_check_password = ''
sr_check_database = 'postgres'
delay_threshold = 10000000

# ---------------- HEALTH CHECK ----------------
health_check_period = 10
health_check_timeout = 20
health_check_user = 'pgpool'
health_check_password = ''
health_check_database = 'postgres'
health_check_max_retries = 3
health_check_retry_delay = 5
connect_timeout = 10000

# ---------------- FAILOVER ----------------
failover_on_backend_error = on
detach_false_primary = off
search_primary_node_timeout = 5min
auto_failback = off
auto_failback_interval = 1min

# ---------------- WATCHDOG ----------------
use_watchdog = on
trusted_servers = '10.0.0.1,10.0.0.2,10.0.0.3'
hostname0 = '10.0.0.1'
wd_port0 = 9000
pgpool_port0 = 9999
hostname1 = '10.0.0.2'
wd_port1 = 9000
pgpool_port1 = 9999
hostname2 = '10.0.0.3'
wd_port2 = 9000
pgpool_port2 = 9999
wd_priority = 1

# ---------------- VIRTUAL IP ----------------
delegate_ip = '10.0.0.100'
if_cmd_path = '/sbin'
if_up_cmd = '/usr/bin/sudo /sbin/ip addr add </span><span class="nv">$_IP_$/</span><span class="sh">23 dev ens160 label ens160:0'
if_down_cmd = '/usr/bin/sudo /sbin/ip addr del </span><span class="nv">$_IP_$/</span><span class="sh">23 dev ens160'
arping_cmd = '/usr/bin/sudo /usr/sbin/arping -U </span><span class="nv">$_IP_$ </span><span class="sh">-w 1 -I ens160'

# ---------------- WATCHDOG BEHAVIOR ----------------
clear_memqcache_on_escalation = on
failover_when_quorum_exists = on
failover_require_consensus = on
allow_multiple_failover_requests_from_node = off
enable_consensus_with_half_votes = on

# ---------------- WATCHDOG LIFECHECK ----------------
wd_monitoring_interfaces_list = 'ens160'
wd_lifecheck_method = 'heartbeat'
wd_interval = 10
heartbeat_hostname0 = '10.0.0.1'
heartbeat_port0 = 9694
heartbeat_device0 = 'ens160'
heartbeat_hostname1 = '10.0.0.2'
heartbeat_port1 = 9694
heartbeat_device1 = 'ens160'
heartbeat_hostname2 = '10.0.0.3'
heartbeat_port2 = 9694
heartbeat_device2 = 'ens160'
wd_life_point = 3
wd_lifecheck_query = 'SELECT 1'
wd_lifecheck_dbname = 'template1'
wd_lifecheck_user = 'pgpool'
wd_lifecheck_password = ''

# ---------------- MEMORY QUERY CACHE (disabled) ----------------
memory_cache_enabled = off
memqcache_method = 'shmem'
memqcache_total_size = 2GB
memqcache_max_num_cache = 1000000
memqcache_expire = 3600
memqcache_auto_cache_invalidation = on
memqcache_oiddir = '/data/log/pgpool/oiddir'
</span><span class="no">EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="step-10--start-pgpool-cluster-all-3-nodes">STEP 10 — Start Pgpool Cluster (All 3 Nodes)</h2>

<p>Start one node at a time, waiting for each to come up:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c"># Start Pgpool (do this on each node, one at a time)</span>
<span class="nb">sudo </span>systemctl start pgpool.service
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>pgpool.service

<span class="c"># Check status</span>
<span class="nb">sudo </span>systemctl status pgpool.service
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="step-11--verify-the-cluster">STEP 11 — Verify the Cluster</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="c"># Show pool node status (run from any node or via VIP)</span>
psql <span class="nt">-h</span> 10.0.0.100 <span class="nt">-p</span> 9999 <span class="nt">-U</span> pgpool postgres <span class="nt">-c</span> <span class="s2">"SHOW POOL_NODES;"</span>
<span class="c"># Password: ChangeMe123!</span>

<span class="c"># Check watchdog status</span>
<span class="nb">sudo</span> <span class="nt">-u</span> postgres pcp_watchdog_info <span class="nt">-h</span> 10.0.0.100 <span class="nt">-U</span> pgpool <span class="nt">-p</span> 9898 <span class="nt">-v</span>
<span class="c"># Password: ChangeMe123!</span>

<span class="c"># Check which node holds the VIP</span>
<span class="k">for </span>ip <span class="k">in </span>10.0.0.1 10.0.0.2 10.0.0.3<span class="p">;</span> <span class="k">do
  </span><span class="nb">echo</span> <span class="s2">"=== </span><span class="nv">$ip</span><span class="s2"> ==="</span>
  ssh postgres@<span class="nv">$ip</span> <span class="s2">"ip addr show ens160 | grep 10.0.0.100"</span> 2&gt;/dev/null <span class="o">||</span> <span class="nb">echo</span> <span class="s2">"VIP not here"</span>
<span class="k">done</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Expected <code class="language-plaintext highlighter-rouge">SHOW POOL_NODES</code> output: all 3 nodes with status <code class="language-plaintext highlighter-rouge">up</code>, one as primary, two as standbys.</p>

<hr />

<h2 id="step-12--application-connection-test">STEP 12 — Application Connection Test</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="c"># Test connection via VIP</span>
psql <span class="nt">-h</span> 10.0.0.100 <span class="nt">-p</span> 9999 <span class="nt">-U</span> app_user appdb <span class="se">\</span>
  <span class="nt">-c</span> <span class="s2">"SELECT current_database(), inet_server_addr(), inet_server_port();"</span>
<span class="c"># Password: AppUserPass123!</span>

<span class="c"># Run 5 times to verify load balancing hits different backends</span>
<span class="k">for </span>i <span class="k">in</span> <span class="o">{</span>1..5<span class="o">}</span><span class="p">;</span> <span class="k">do
  </span>psql <span class="nt">-h</span> 10.0.0.100 <span class="nt">-p</span> 9999 <span class="nt">-U</span> app_user appdb <span class="se">\</span>
    <span class="nt">-c</span> <span class="s2">"SELECT inet_server_addr();"</span> <span class="nt">-t</span> 2&gt;/dev/null | <span class="nb">tr</span> <span class="nt">-d</span> <span class="s1">' '</span>
<span class="k">done</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="quick-reference--credentials-replace-with-your-own">Quick Reference — Credentials (Replace with your own)</h2>

<table>
  <thead>
    <tr>
      <th>User</th>
      <th>Password</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">repl_user</code></td>
      <td><code class="language-plaintext highlighter-rouge">ChangeMe123!</code></td>
      <td>PostgreSQL streaming replication</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">pgpool</code></td>
      <td><code class="language-plaintext highlighter-rouge">ChangeMe123!</code></td>
      <td>Pgpool health/SR check</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">app_user</code></td>
      <td><code class="language-plaintext highlighter-rouge">AppUserPass123!</code></td>
      <td>Application database user</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="quick-reference--common-monitoring-commands">Quick Reference — Common Monitoring Commands</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
</pre></td><td class="rouge-code"><pre><span class="c"># Node status</span>
psql <span class="nt">-h</span> 10.0.0.100 <span class="nt">-p</span> 9999 <span class="nt">-U</span> pgpool postgres <span class="nt">-c</span> <span class="s2">"SHOW POOL_NODES;"</span>

<span class="c"># Active processes</span>
psql <span class="nt">-h</span> 10.0.0.100 <span class="nt">-p</span> 9999 <span class="nt">-U</span> pgpool postgres <span class="nt">-c</span> <span class="s2">"SHOW POOL_PROCESSES;"</span>

<span class="c"># Connection pool state</span>
psql <span class="nt">-h</span> 10.0.0.100 <span class="nt">-p</span> 9999 <span class="nt">-U</span> pgpool postgres <span class="nt">-c</span> <span class="s2">"SHOW POOL_POOLS;"</span>

<span class="c"># PCP node info</span>
pcp_node_info <span class="nt">-h</span> 10.0.0.100 <span class="nt">-U</span> pgpool <span class="nt">-p</span> 9898 <span class="nt">-n</span> 0
pcp_node_info <span class="nt">-h</span> 10.0.0.100 <span class="nt">-U</span> pgpool <span class="nt">-p</span> 9898 <span class="nt">-n</span> 1
pcp_node_info <span class="nt">-h</span> 10.0.0.100 <span class="nt">-U</span> pgpool <span class="nt">-p</span> 9898 <span class="nt">-n</span> 2

<span class="c"># Replication lag (on primary)</span>
psql <span class="nt">-h</span> 10.0.0.1 <span class="nt">-U</span> postgres <span class="nt">-c</span> <span class="se">\</span>
  <span class="s2">"SELECT client_addr, state, sent_lsn, replay_lsn, </span><span class="se">\</span><span class="s2">
   (sent_lsn - replay_lsn) AS lag_bytes FROM pg_stat_replication;"</span>

<span class="c"># Re-attach a detached node</span>
pcp_attach_node <span class="nt">-h</span> 10.0.0.100 <span class="nt">-U</span> pgpool <span class="nt">-n</span> 1 <span class="nt">-p</span> 9898
pcp_attach_node <span class="nt">-h</span> 10.0.0.100 <span class="nt">-U</span> pgpool <span class="nt">-n</span> 2 <span class="nt">-p</span> 9898
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="troubleshooting">Troubleshooting</h2>

<h3 id="vip-not-assigned">VIP not assigned</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre>pcp_watchdog_info <span class="nt">-h</span> 10.0.0.1 <span class="nt">-U</span> pgpool <span class="nt">-p</span> 9898 <span class="nt">-v</span>
<span class="nb">sudo</span> <span class="nt">-u</span> postgres <span class="nb">sudo</span> /sbin/ip addr show ens160

<span class="c"># Manual emergency VIP assignment on the primary node:</span>
<span class="nb">sudo</span> /sbin/ip addr add 10.0.0.100/23 dev ens160 label ens160:0
<span class="nb">sudo</span> /usr/sbin/arping <span class="nt">-U</span> 10.0.0.100 <span class="nt">-w</span> 1 <span class="nt">-I</span> ens160
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="node-stuck-in-detached-state">Node stuck in detached state</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>systemctl status postgresql-17
pcp_attach_node <span class="nt">-h</span> 10.0.0.100 <span class="nt">-U</span> pgpool <span class="nt">-p</span> 9898 <span class="nt">-n</span> 1

<span class="c"># If still failing:</span>
<span class="nb">sudo </span>systemctl restart pgpool
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="pool_passwd-errors--auth-failures">pool_passwd errors / auth failures</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="nb">sudo cat</span> /etc/pgpool-II/pool_passwd

<span class="c"># Re-encrypt if missing:</span>
<span class="nb">sudo </span>su - postgres
pg_enc <span class="nt">-m</span> <span class="nt">-k</span> ~/.pgpoolkey <span class="nt">-u</span> pgpool <span class="nt">-p</span>
pg_enc <span class="nt">-m</span> <span class="nt">-k</span> ~/.pgpoolkey <span class="nt">-u</span> app_user <span class="nt">-p</span>
<span class="nb">exit</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="check-pg_hbaconf-is-loaded">Check pg_hba.conf is loaded</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>psql <span class="nt">-h</span> 10.0.0.1 <span class="nt">-U</span> postgres <span class="nt">-c</span> <span class="s2">"SELECT pg_reload_conf();"</span>
psql <span class="nt">-h</span> 10.0.0.1 <span class="nt">-U</span> postgres <span class="nt">-c</span> <span class="s2">"SELECT * FROM pg_hba_file_rules;"</span>
</pre></td></tr></tbody></table></code></pre></div></div>]]></content><author><name>Moin Uddin Ahmed</name></author><category term="PostgreSQL" /><category term="postgresql" /><category term="pgpool" /><category term="high-availability" /><category term="clustering" /><category term="streaming-replication" /><category term="load-balancing" /><category term="failover" /><summary type="html"><![CDATA[A comprehensive step-by-step guide to deploying a highly available PostgreSQL 17 cluster with Pgpool-II 4.5, including streaming replication, watchdog, virtual IP failover, and connection pooling.]]></summary></entry><entry><title type="html">ETL Server Setup to transfer data from Production to a Example. Data Warehouse Guide: Apache Airflow &amp;amp; PipelineWise</title><link href="https://marufmoinuddin.github.io/blog/2025/07/etl-server-setup-guide-apache-airflow-pipelinewise/" rel="alternate" type="text/html" title="ETL Server Setup to transfer data from Production to a Example. Data Warehouse Guide: Apache Airflow &amp;amp; PipelineWise" /><published>2025-07-29T00:00:00+00:00</published><updated>2025-07-29T00:00:00+00:00</updated><id>https://marufmoinuddin.github.io/blog/2025/07/etl-server-setup-guide-apache-airflow-pipelinewise</id><content type="html" xml:base="https://marufmoinuddin.github.io/blog/2025/07/etl-server-setup-guide-apache-airflow-pipelinewise/"><![CDATA[<h1 id="etl-server-setup-guide-apache-airflow--pipelinewise">ETL Server Setup Guide: Apache Airflow &amp; PipelineWise</h1>

<p>This guide provides clear, step-by-step instructions for setting up <strong>Apache Airflow</strong> and <strong>PipelineWise</strong> to manage and orchestrate ETL (Extract, Transform, Load) pipelines. It is designed to be beginner-friendly, ensuring interns and new users can follow along easily. Apache Airflow is used to schedule and monitor workflows, while PipelineWise simplifies data extraction and loading.</p>

<h2 id="prerequisites">Prerequisites</h2>
<p>Before starting, ensure you have:</p>
<ul>
  <li><strong>Operating System</strong>: Ubuntu 20.04 LTS (or compatible Linux distribution)</li>
  <li><strong>Python</strong>: Version 3.6 or higher</li>
  <li><strong>Docker</strong> and <strong>Docker Compose</strong>: Installed and configured</li>
  <li><strong>Basic Knowledge</strong>: Familiarity with command-line operations and ETL concepts</li>
  <li><strong>User Permissions</strong>: A non-root user with <code class="language-plaintext highlighter-rouge">sudo</code> privileges</li>
</ul>

<p>Run the following to verify prerequisites:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="c"># Check Ubuntu version</span>
lsb_release <span class="nt">-a</span>

<span class="c"># Check Python version</span>
python3 <span class="nt">--version</span>

<span class="c"># Check Docker and Docker Compose</span>
docker <span class="nt">--version</span>
docker-compose <span class="nt">--version</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="table-of-contents">Table of Contents</h2>
<ol>
  <li><a href="#apache-airflow-setup">Apache Airflow Setup</a>
    <ul>
      <li><a href="#step-1-set-up-airflow-home-directory">Step 1: Set Up Airflow Home Directory</a></li>
      <li><a href="#step-2-create-a-python-virtual-environment">Step 2: Create a Python Virtual Environment</a></li>
      <li><a href="#step-3-install-apache-airflow">Step 3: Install Apache Airflow</a></li>
      <li><a href="#step-4-install-additional-providers-optional">Step 4: Install Additional Providers (Optional)</a></li>
      <li><a href="#step-5-configure-postgresql-database">Step 5: Configure PostgreSQL Database</a></li>
      <li><a href="#step-6-create-an-admin-user">Step 6: Create an Admin User</a></li>
      <li><a href="#step-7-configure-permissions">Step 7: Configure Permissions</a></li>
      <li><a href="#step-8-set-up-airflow-as-a-systemd-service">Step 8: Set Up Airflow as a Systemd Service</a></li>
      <li><a href="#step-9-manage-the-airflow-service">Step 9: Manage the Airflow Service</a></li>
      <li><a href="#step-10-manage-dags">Step 10: Manage DAGs</a></li>
      <li><a href="#step-11-troubleshoot-airflow">Step 11: Troubleshoot Airflow</a></li>
    </ul>
  </li>
  <li><a href="#pipelinewise-setup">PipelineWise Setup</a>
    <ul>
      <li><a href="#step-1-pull-and-tag-the-docker-image">Step 1: Pull and Tag the Docker Image</a></li>
      <li><a href="#step-2-create-configuration-directory">Step 2: Create Configuration Directory</a></li>
      <li><a href="#step-3-create-a-helper-script-plw">Step 3: Create a Helper Script (<code class="language-plaintext highlighter-rouge">plw</code>)</a></li>
      <li><a href="#step-4-import-configurations">Step 4: Import Configurations</a></li>
      <li><a href="#step-5-common-pipelinewise-commands">Step 5: Common PipelineWise Commands</a></li>
      <li><a href="#step-6-troubleshoot-pipelinewise">Step 6: Troubleshoot PipelineWise</a></li>
    </ul>
  </li>
  <li><a href="#next-steps">Next Steps</a></li>
</ol>

<hr />

<h2 id="apache-airflow-setup">Apache Airflow Setup</h2>

<p>This section guides you through setting up Apache Airflow in <strong>standalone mode</strong> using a PostgreSQL database.</p>

<h3 id="step-1-set-up-airflow-home-directory">Step 1: Set Up Airflow Home Directory</h3>
<p>Create a directory to store Airflow configurations, logs, and DAGs (Directed Acyclic Graphs).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">mkdir</span> <span class="nt">-p</span> ~/airflow
<span class="nb">export </span><span class="nv">AIRFLOW_HOME</span><span class="o">=</span>~/airflow
</pre></td></tr></tbody></table></code></pre></div></div>

<p>To make <code class="language-plaintext highlighter-rouge">AIRFLOW_HOME</code> persistent, add it to your shell configuration:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">echo</span> <span class="s1">'export AIRFLOW_HOME=~/airflow'</span> <span class="o">&gt;&gt;</span> ~/.bashrc  <span class="c"># or ~/.zshrc for Zsh users</span>
<span class="nb">source</span> ~/.bashrc
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="step-2-create-a-python-virtual-environment">Step 2: Create a Python Virtual Environment</h3>
<p>Using a virtual environment prevents package conflicts.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="c"># Update package lists and install dependencies</span>
<span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> python3-pip python3-venv

<span class="c"># Create and activate a virtual environment</span>
python3 <span class="nt">-m</span> venv ~/venv
<span class="nb">source</span> ~/venv/bin/activate
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Note</strong>: Run all subsequent Airflow commands within this virtual environment. To confirm activation, your terminal prompt should show <code class="language-plaintext highlighter-rouge">(venv)</code>.</p>

<h3 id="step-3-install-apache-airflow">Step 3: Install Apache Airflow</h3>
<p>Install Airflow with version-specific constraints to ensure compatibility.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c"># Set version variables</span>
<span class="nb">export </span><span class="nv">AIRFLOW_VERSION</span><span class="o">=</span>2.10.1
<span class="nb">export </span><span class="nv">PYTHON_VERSION</span><span class="o">=</span><span class="si">$(</span>python3 <span class="nt">--version</span> | <span class="nb">cut</span> <span class="nt">-d</span> <span class="s2">" "</span> <span class="nt">-f</span> 2 | <span class="nb">cut</span> <span class="nt">-d</span> <span class="s2">"."</span> <span class="nt">-f</span> 1-2<span class="si">)</span>

<span class="c"># Install dependencies and Airflow</span>
<span class="nv">CONSTRAINT_URL</span><span class="o">=</span><span class="s2">"https://raw.githubusercontent.com/apache/airflow/constraints-</span><span class="k">${</span><span class="nv">AIRFLOW_VERSION</span><span class="k">}</span><span class="s2">/constraints-</span><span class="k">${</span><span class="nv">PYTHON_VERSION</span><span class="k">}</span><span class="s2">.txt"</span>
pip <span class="nb">install</span> <span class="s2">"psycopg2-binary==2.9.6"</span>
pip <span class="nb">install</span> <span class="s2">"apache-airflow==</span><span class="k">${</span><span class="nv">AIRFLOW_VERSION</span><span class="k">}</span><span class="s2">"</span> <span class="nt">--constraint</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CONSTRAINT_URL</span><span class="k">}</span><span class="s2">"</span>

<span class="c"># Verify installation</span>
airflow version
</pre></td></tr></tbody></table></code></pre></div></div>

<p>If the <code class="language-plaintext highlighter-rouge">airflow version</code> command outputs the version (e.g., 2.10.1), the installation is successful.</p>

<h3 id="step-4-install-additional-providers-optional">Step 4: Install Additional Providers (Optional)</h3>
<p>Install provider packages for integrations like Slack, AWS, or GCP if needed.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>pip <span class="nb">install </span>apache-airflow-providers-slack <span class="nt">--constraint</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CONSTRAINT_URL</span><span class="k">}</span><span class="s2">"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Replace <code class="language-plaintext highlighter-rouge">slack</code> with other providers (e.g., <code class="language-plaintext highlighter-rouge">amazon</code>, <code class="language-plaintext highlighter-rouge">google</code>) as required.</p>

<h3 id="step-5-configure-postgresql-database">Step 5: Configure PostgreSQL Database</h3>
<p>Airflow requires a PostgreSQL database for production use.</p>

<ol>
  <li><strong>Install PostgreSQL</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> postgresql postgresql-contrib
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Create a database and user</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo</span> <span class="nt">-u</span> postgres psql
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>Inside the PostgreSQL prompt, run:</p>
    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">USER</span> <span class="n">airflow</span> <span class="k">WITH</span> <span class="n">PASSWORD</span> <span class="s1">'your_secure_password'</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">DATABASE</span> <span class="n">airflowdb</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">ALL</span> <span class="k">PRIVILEGES</span> <span class="k">ON</span> <span class="k">DATABASE</span> <span class="n">airflowdb</span> <span class="k">TO</span> <span class="n">airflow</span><span class="p">;</span>
<span class="err">\</span><span class="n">q</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>Replace <code class="language-plaintext highlighter-rouge">your_secure_password</code> with a strong password.</p>
  </li>
  <li><strong>Add Airflow configuration</strong>:
Edit <code class="language-plaintext highlighter-rouge">~/airflow/airflow.cfg</code> with a text editor (e.g., <code class="language-plaintext highlighter-rouge">nano</code>):
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>nano ~/airflow/airflow.cfg
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>And set:</p>
    <div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nn">[core]</span>
<span class="py">sql_alchemy_conn</span> <span class="p">=</span> <span class="s">postgresql+psycopg2://airflow:your_secure_password@localhost:5432/airflowdb</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Initialize the database</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>airflow db init
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<p>This creates the necessary metadata tables in <code class="language-plaintext highlighter-rouge">airflowdb</code>.</p>

<h3 id="step-6-create-an-admin-user">Step 6: Create an Admin User</h3>
<p>Create an admin user for the Airflow web interface.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre>airflow <span class="nb">users </span>create <span class="se">\</span>
  <span class="nt">--username</span> admin <span class="se">\</span>
  <span class="nt">--password</span> your_secure_password <span class="se">\</span>
  <span class="nt">--firstname</span> Admin <span class="se">\</span>
  <span class="nt">--lastname</span> User <span class="se">\</span>
  <span class="nt">--role</span> Admin <span class="se">\</span>
  <span class="nt">--email</span> admin@example.com
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Use a strong password and update the email as needed.</p>

<h3 id="step-7-configure-permissions">Step 7: Configure Permissions</h3>
<p>Ensure the Airflow directory has correct ownership and permissions.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">sudo chown</span> <span class="nt">-R</span> &lt;YOUR-USERNAME&gt;:&lt;YOUR-USERNAME&gt; ~/airflow
<span class="nb">sudo chmod</span> <span class="nt">-R</span> 755 ~/airflow
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="step-8-set-up-airflow-as-a-systemd-service">Step 8: Set Up Airflow as a Systemd Service</h3>
<p>Run Airflow as a single <code class="language-plaintext highlighter-rouge">systemd</code> service in standalone mode, which includes both the scheduler and webserver.</p>

<ol>
  <li><strong>Create the service file</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>nano /etc/systemd/system/airflow.service
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>Add the following, replacing <code class="language-plaintext highlighter-rouge">&lt;YOUR-USERNAME&gt;</code> with your actual username:</p>
    <div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="nn">[Unit]</span>
<span class="py">Description</span><span class="p">=</span><span class="s">Apache Airflow (Standalone)</span>
<span class="py">After</span><span class="p">=</span><span class="s">network.target</span>

<span class="nn">[Service]</span>
<span class="py">User</span><span class="p">=</span><span class="s">&lt;YOUR-USERNAME&gt;</span>
<span class="py">Environment</span><span class="p">=</span><span class="s">"PATH=/home/&lt;YOUR-USERNAME&gt;/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"</span>
<span class="py">Environment</span><span class="p">=</span><span class="s">"AIRFLOW_HOME=/home/&lt;YOUR-USERNAME&gt;/airflow"</span>
<span class="py">ExecStart</span><span class="p">=</span><span class="s">/home/&lt;YOUR-USERNAME&gt;/venv/bin/airflow standalone</span>
<span class="py">Restart</span><span class="p">=</span><span class="s">always</span>
<span class="py">Type</span><span class="p">=</span><span class="s">simple</span>

<span class="nn">[Install]</span>
<span class="py">WantedBy</span><span class="p">=</span><span class="s">multi-user.target</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Enable and start the service</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl daemon-reload
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>airflow
<span class="nb">sudo </span>systemctl start airflow
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Verify the service</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl status airflow
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<p>The Airflow webserver will be accessible at <code class="language-plaintext highlighter-rouge">http://localhost:8080</code>.</p>

<h3 id="step-9-manage-the-airflow-service">Step 9: Manage the Airflow Service</h3>
<p>Use these commands to control the Airflow service:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c"># Start the service</span>
<span class="nb">sudo </span>systemctl start airflow

<span class="c"># Stop the service</span>
<span class="nb">sudo </span>systemctl stop airflow

<span class="c"># Restart the service</span>
<span class="nb">sudo </span>systemctl restart airflow

<span class="c"># View real-time logs</span>
journalctl <span class="nt">-u</span> airflow <span class="nt">-f</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="step-10-manage-dags">Step 10: Manage DAGs</h3>
<p>DAGs define your workflows and are stored in <code class="language-plaintext highlighter-rouge">~/airflow/dags/</code>.</p>

<ol>
  <li><strong>Add a DAG</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">cp </span>your_dag.py ~/airflow/dags/
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>Or extract multiple DAGs:</p>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">tar</span> <span class="nt">-xzvf</span> airflow_dags.tar.gz <span class="nt">-C</span> ~/airflow/dags/
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Airflow automatically detects new DAGs. Adjust the scanning interval in <code class="language-plaintext highlighter-rouge">airflow.cfg</code> under <code class="language-plaintext highlighter-rouge">[scheduler]</code> if needed.</li>
</ol>

<h3 id="step-11-troubleshoot-airflow">Step 11: Troubleshoot Airflow</h3>
<ul>
  <li><strong>Reinstall Airflow</strong> (if dependencies break):
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>pip <span class="nb">install</span> <span class="s2">"apache-airflow==</span><span class="k">${</span><span class="nv">AIRFLOW_VERSION</span><span class="k">}</span><span class="s2">"</span> <span class="nt">--constraint</span> <span class="s2">"</span><span class="k">${</span><span class="nv">CONSTRAINT_URL</span><span class="k">}</span><span class="s2">"</span> <span class="nt">--force-reinstall</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Check logs</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>journalctl <span class="nt">-u</span> airflow <span class="nt">-f</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Database issues</strong>:
    <ul>
      <li>Verify <code class="language-plaintext highlighter-rouge">sql_alchemy_conn</code> in <code class="language-plaintext highlighter-rouge">~/airflow/airflow.cfg</code>.</li>
      <li>Ensure PostgreSQL is running (<code class="language-plaintext highlighter-rouge">sudo systemctl status postgresql</code>).</li>
      <li>Check the <code class="language-plaintext highlighter-rouge">airflow</code> user’s password and database access.</li>
    </ul>
  </li>
  <li><strong>Webserver access</strong>:
    <ul>
      <li>Confirm port <code class="language-plaintext highlighter-rouge">8080</code> is open and not blocked by a firewall.</li>
    </ul>
  </li>
</ul>

<hr />

<h2 id="pipelinewise-setup">PipelineWise Setup</h2>

<p>This section explains how to set up PipelineWise using Docker to extract and load data.</p>

<h3 id="step-1-pull-and-tag-the-docker-image">Step 1: Pull and Tag the Docker Image</h3>
<ol>
  <li><strong>Pull the PipelineWise image</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker pull docker.io/transferwiseworkspace/pipelinewise:latest
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Tag the image</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker tag transferwiseworkspace/pipelinewise:latest pipelinewise:latest
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<h3 id="step-2-create-configuration-directory">Step 2: Create Configuration Directory</h3>
<p>PipelineWise stores configurations in <code class="language-plaintext highlighter-rouge">~/.pipelinewise</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="nb">mkdir</span> <span class="nt">-p</span> ~/.pipelinewise
<span class="nb">sudo chown</span> <span class="nt">-R</span> &lt;YOUR-USERNAME&gt;:&lt;YOUR-USERNAME&gt; ~/.pipelinewise
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="step-3-create-a-helper-script-plw">Step 3: Create a Helper Script (<code class="language-plaintext highlighter-rouge">plw</code>)</h3>
<p>The <code class="language-plaintext highlighter-rouge">plw</code> script simplifies running PipelineWise commands via Docker.</p>

<ol>
  <li><strong>Create the script</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>nano /usr/bin/plw
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>Add the following:</p>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
</pre></td><td class="rouge-code"><pre><span class="c">#!/usr/bin/env bash</span>
<span class="c"># Helper script to run PipelineWise in Docker</span>
<span class="nv">IMAGE</span><span class="o">=</span>pipelinewise
<span class="nv">VERSION</span><span class="o">=</span>latest

<span class="c"># Mount current directory and PipelineWise config</span>
<span class="nv">HOST_DIR</span><span class="o">=</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>
<span class="nv">CONT_WORK_DIR</span><span class="o">=</span>/app/wrk
<span class="nv">HOST_CONFIG_DIR</span><span class="o">=</span><span class="k">${</span><span class="nv">HOME</span><span class="k">}</span>/.pipelinewise
<span class="nv">CONT_CONFIG_DIR</span><span class="o">=</span>/app/.pipelinewise

<span class="c"># Process --dir argument for custom working directory</span>
<span class="nv">ARGS</span><span class="o">=</span><span class="s2">""</span>
<span class="k">while</span> <span class="o">[[</span> <span class="nv">$# </span><span class="nt">-gt</span> 0 <span class="o">]]</span><span class="p">;</span> <span class="k">do
    case</span> <span class="nv">$1</span> <span class="k">in</span>
        <span class="nt">--dir</span><span class="p">)</span>
            <span class="nv">HOST_DIR</span><span class="o">=</span><span class="si">$(</span><span class="nb">cd</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">dirname</span> <span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span><span class="p">;</span> <span class="nb">pwd</span><span class="si">)</span>/<span class="si">$(</span><span class="nb">basename</span> <span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span><span class="si">)</span>
            <span class="nv">ARGS</span><span class="o">=</span><span class="s2">"</span><span class="nv">$ARGS</span><span class="s2"> --dir </span><span class="nv">$CONT_WORK_DIR</span><span class="s2">"</span>
            <span class="nb">shift
            shift</span>
            <span class="p">;;</span>
        <span class="k">*</span><span class="p">)</span>
            <span class="nv">ARGS</span><span class="o">=</span><span class="s2">"</span><span class="nv">$ARGS</span><span class="s2"> </span><span class="nv">$1</span><span class="s2">"</span>
            <span class="nb">shift</span>
            <span class="p">;;</span>
    <span class="k">esac</span>
<span class="k">done</span>

<span class="c"># Validate directories</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-d</span> <span class="s2">"</span><span class="k">${</span><span class="nv">HOST_DIR</span><span class="k">}</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"Error: Directory </span><span class="k">${</span><span class="nv">HOST_DIR</span><span class="k">}</span><span class="s2"> does not exist"</span>
    <span class="nb">exit </span>1
<span class="k">fi

if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-d</span> <span class="s2">"</span><span class="k">${</span><span class="nv">HOST_CONFIG_DIR</span><span class="k">}</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">mkdir</span> <span class="nt">-p</span> <span class="s2">"</span><span class="k">${</span><span class="nv">HOST_CONFIG_DIR</span><span class="k">}</span><span class="s2">"</span>
<span class="k">fi</span>

<span class="c"># Run Docker container</span>
docker run <span class="se">\</span>
    <span class="nt">--rm</span> <span class="se">\</span>
    <span class="nt">-v</span> <span class="s2">"</span><span class="k">${</span><span class="nv">HOST_CONFIG_DIR</span><span class="k">}</span><span class="s2">:</span><span class="k">${</span><span class="nv">CONT_CONFIG_DIR</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    <span class="nt">-v</span> <span class="s2">"</span><span class="k">${</span><span class="nv">HOST_DIR</span><span class="k">}</span><span class="s2">:</span><span class="k">${</span><span class="nv">CONT_WORK_DIR</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    <span class="s2">"</span><span class="k">${</span><span class="nv">IMAGE</span><span class="k">}</span><span class="s2">:</span><span class="k">${</span><span class="nv">VERSION</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
    <span class="k">${</span><span class="nv">ARGS</span><span class="k">}</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Make the script executable</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo chmod</span> +x /usr/bin/plw
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Verify the script</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>plw <span class="nt">--help</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>You should see PipelineWise’s help text.</p>
  </li>
</ol>

<h3 id="step-4-import-configurations">Step 4: Import Configurations</h3>
<p>Import existing PipelineWise configurations if available.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>plw import <span class="nt">--dir</span> /path/to/warehouse_configs/
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This merges configurations into <code class="language-plaintext highlighter-rouge">~/.pipelinewise</code>.</p>

<h3 id="step-5-common-pipelinewise-commands">Step 5: Common PipelineWise Commands</h3>
<ul>
  <li><strong>List available taps and targets</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>plw list
plw list_taps
plw status
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Run a tap</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>plw run_tap <span class="nt">--tap</span> &lt;tap_name&gt; <span class="nt">--target</span> &lt;target_name&gt; <span class="nt">--extra_log</span> <span class="nt">--debug</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>Use <code class="language-plaintext highlighter-rouge">--extra_log</code> for verbose output and <code class="language-plaintext highlighter-rouge">--debug</code> for detailed debugging.</p>
  </li>
  <li><strong>Import configurations</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>plw import <span class="nt">--dir</span> /home/&lt;YOUR-USERNAME&gt;/warehouse_configs/
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ul>

<h3 id="step-6-troubleshoot-pipelinewise">Step 6: Troubleshoot PipelineWise</h3>
<ul>
  <li><strong>Verify script</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>which plw
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Docker permissions</strong>:
Ensure your user can run Docker without <code class="language-plaintext highlighter-rouge">sudo</code>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>usermod <span class="nt">-aG</span> docker &lt;YOUR-USERNAME&gt;
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>Log out and back in for changes to take effect.</p>
  </li>
  <li><strong>Check logs</strong>:
Logs appear in the console. For persistent logs, check <code class="language-plaintext highlighter-rouge">~/.pipelinewise/&lt;target&gt;/&lt;tap&gt;/log/</code>.</li>
  <li><strong>Configuration errors</strong>:
Validate YAML/JSON files in <code class="language-plaintext highlighter-rouge">~/.pipelinewise</code> for syntax errors.</li>
</ul>

<hr />

<h2 id="next-steps">Next Steps</h2>
<p>You now have:</p>
<ul>
  <li><strong>Apache Airflow</strong> running in standalone mode with a PostgreSQL database (<code class="language-plaintext highlighter-rouge">airflowdb</code>), accessible at <code class="language-plaintext highlighter-rouge">http://localhost:8080</code>.</li>
  <li><strong>PipelineWise</strong> set up with a Docker-based helper script (<code class="language-plaintext highlighter-rouge">plw</code>) for managing ETL tasks.</li>
</ul>

<p>Use Airflow to schedule and monitor PipelineWise tasks, which extract data from sources and load it into targets like data warehouses. For advanced setups (e.g., distributed Airflow executors or high-availability configurations), refer to:</p>
<ul>
  <li><a href="https://airflow.apache.org/docs/">Apache Airflow Documentation</a></li>
  <li><a href="https://transferwise.github.io/pipelinewise/">PipelineWise Documentation</a></li>
</ul>]]></content><author><name>Moin Uddin Ahmed</name></author><category term="Data Engineering" /><category term="airflow" /><category term="data-engineering" /><category term="docker" /><category term="linux" /><category term="pipelinewise" /><category term="postgresql" /><category term="ubuntu" /><summary type="html"><![CDATA[This guide provides clear, step-by-step instructions for setting up Apache Airflow and PipelineWise to manage and orchestrate ETL (Extract, Transform, Load) pipelines. It is designed to be beginner-friendly, ensuring…]]></summary></entry><entry><title type="html">Crunchy PGO 5.6.7 Kubernetes 29 Cluster Basic Documentation</title><link href="https://marufmoinuddin.github.io/blog/2025/07/crunchy-pgo-567-kubernetes-29-cluster-basic-documentation/" rel="alternate" type="text/html" title="Crunchy PGO 5.6.7 Kubernetes 29 Cluster Basic Documentation" /><published>2025-07-08T00:00:00+00:00</published><updated>2025-07-08T00:00:00+00:00</updated><id>https://marufmoinuddin.github.io/blog/2025/07/crunchy-pgo-567-kubernetes-29-cluster-basic-documentation</id><content type="html" xml:base="https://marufmoinuddin.github.io/blog/2025/07/crunchy-pgo-567-kubernetes-29-cluster-basic-documentation/"><![CDATA[<h1 id="crunchy-pgo-567-kubernetes-29-cluster-basic-documentation">Crunchy PGO 5.6.7 Kubernetes 29 Cluster Basic Documentation</h1>

<p>This documentation provides step-by-step instructions for setting up a Kubernetes 29 cluster, installing necessary components such as Cilium, Rook Ceph storage, and Crunchy PGO, and deploying a PostgreSQL cluster.</p>

<h2 id="table-of-contents">Table of Contents</h2>

<ol>
  <li><a href="#setup-kubernetes-29-cluster">Setup Kubernetes 29 Cluster</a></li>
  <li><a href="#initialize-kubernetes-and-install-cilium">Initialize Kubernetes and Install Cilium</a></li>
  <li><a href="#setup-and-install-rook-ceph-storage">Setup and Install Rook Ceph Storage</a></li>
  <li><a href="#setup-crunchy-pgo">Setup Crunchy PGO</a></li>
  <li><a href="#setup-kubectl-pgo-client">Setup kubectl-pgo Client</a></li>
  <li><a href="#setup-namespace-and-install-database-cluster">Setup Namespace and Install Database Cluster</a></li>
</ol>

<h3 id="1-setup-kubernetes-29-cluster">1. Setup Kubernetes 29 Cluster</h3>

<p>Use the provided auto script to set up the Kubernetes 29 cluster.</p>

<h4 id="command">Command</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c"># Clone the repository and run the installation script</span>
git clone &lt;your-git-repository-url&gt;
<span class="nb">cd </span>cluster-doc
bash install-k8s-1.29.sh
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="explanation">Explanation</h4>

<ul>
  <li>This script automates the setup of a Kubernetes 29 cluster. Ensure you have the necessary permissions to execute the script.</li>
</ul>

<h3 id="2-initialize-kubernetes-and-install-cilium">2. Initialize Kubernetes and Install Cilium</h3>

<p>Initialize the Kubernetes cluster and install Cilium for networking.</p>

<h4 id="commands">Commands</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c"># Initialize Kubernetes with a specific pod network CIDR</span>
<span class="nb">sudo </span>kubeadm init <span class="nt">--pod-network-cidr</span><span class="o">=</span>&lt;REDACTED_POD_NETWORK_CIDR&gt;

<span class="c"># Download and install Cilium CLI</span>
wget https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz
<span class="nb">sudo tar </span>xzvfC cilium-linux-amd64.tar.gz /usr/local/bin

<span class="c"># Install Cilium</span>
cilium <span class="nb">install</span>

<span class="c"># Monitor Cilium status</span>
watch <span class="s1">'cilium status'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="explanation-1">Explanation</h4>

<ul>
  <li><code class="language-plaintext highlighter-rouge">sudo kubeadm init --pod-network-cidr=&lt;REDACTED_POD_NETWORK_CIDR&gt;</code>: Initializes the Kubernetes cluster with a specified pod network CIDR.</li>
  <li><code class="language-plaintext highlighter-rouge">wget ...</code>: Downloads the latest Cilium CLI.</li>
  <li><code class="language-plaintext highlighter-rouge">sudo tar xzvfC ...</code>: Extracts the downloaded Cilium CLI to <code class="language-plaintext highlighter-rouge">/usr/local/bin</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">cilium install</code>: Installs Cilium in the Kubernetes cluster.</li>
  <li><code class="language-plaintext highlighter-rouge">watch 'cilium status'</code>: Monitors the status of Cilium.</li>
</ul>

<h3 id="3-setup-and-install-rook-ceph-storage">3. Setup and Install Rook Ceph Storage</h3>

<p>Install Rook Ceph storage to provide persistent storage for your Kubernetes cluster.</p>

<h4 id="commands-1">Commands</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="rouge-code"><pre><span class="c"># Clone the Rook repository and checkout the specific version</span>
git clone <span class="nt">--single-branch</span> <span class="nt">--branch</span> v1.14.8 https://github.com/rook/rook.git
<span class="nb">cd </span>rook/deploy/examples

<span class="c"># Deploy Rook Ceph components</span>
kubectl create <span class="nt">-f</span> crds.yaml <span class="nt">-f</span> common.yaml <span class="nt">-f</span> operator.yaml
<span class="nb">sleep </span>5
kubectl create <span class="nt">-f</span> cluster.yaml

<span class="c"># Create the Rook Ceph storage class</span>
kubectl apply <span class="nt">-f</span> csi/rbd/storageclass.yaml

<span class="c"># Patch the storage class to make it the default</span>
kubectl patch storageclass rook-ceph-block <span class="nt">-p</span> <span class="s1">'{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="explanation-2">Explanation</h4>

<ul>
  <li><code class="language-plaintext highlighter-rouge">git clone ... --branch v1.14.8</code>: Clones the Rook repository and checks out version 1.14.8.</li>
  <li><code class="language-plaintext highlighter-rouge">kubectl create -f ...</code>: Creates Rook Ceph components in the cluster.</li>
  <li><code class="language-plaintext highlighter-rouge">kubectl apply -f csi/rbd/storageclass.yaml</code>: Applies the Rook Ceph storage class configuration.</li>
  <li><code class="language-plaintext highlighter-rouge">kubectl patch storageclass ...</code>: Patches the storage class to make it the default storage class.</li>
</ul>

<h3 id="4-setup-crunchy-pgo">4. Setup Crunchy PGO</h3>

<p>Install the Crunchy PostgreSQL Operator (PGO) to manage PostgreSQL clusters.</p>

<h4 id="commands-2">Commands</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c"># Clone the PostgreSQL Operator examples repository</span>
git clone https://github.com/CrunchyData/postgres-operator-examples.git
<span class="nb">cd </span>postgres-operator-examples/

<span class="c"># Install the Crunchy PostgreSQL Operator</span>
kubectl apply <span class="nt">-k</span> kustomize/install/namespace
kubectl apply <span class="nt">--server-side</span> <span class="nt">-k</span> kustomize/install/default
kubectl apply <span class="nt">-k</span> kustomize/postgres
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="explanation-3">Explanation</h4>

<ul>
  <li><code class="language-plaintext highlighter-rouge">git clone ...</code>: Clones the Crunchy PostgreSQL Operator examples repository.</li>
  <li><code class="language-plaintext highlighter-rouge">kubectl apply --server-side -k ...</code>: Applies the Crunchy PostgreSQL Operator configurations using Kustomize.</li>
</ul>

<h3 id="5-setup-kubectl-pgo-client">5. Setup kubectl-pgo Client</h3>

<p>Install the <code class="language-plaintext highlighter-rouge">kubectl-pgo</code> client to interact with the Crunchy PostgreSQL Operator.</p>

<h4 id="commands-3">Commands</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c"># Download and install the kubectl-pgo client</span>
wget https://github.com/CrunchyData/postgres-operator-client/releases/download/v0.4.2/kubectl-pgo-linux-amd64
<span class="nb">sudo mv </span>kubectl-pgo-linux-amd64 /usr/local/bin/kubectl-pgo
<span class="nb">sudo chmod</span> +x /usr/local/bin/kubectl-pgo
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="explanation-4">Explanation</h4>

<ul>
  <li><code class="language-plaintext highlighter-rouge">wget ...</code>: Downloads the <code class="language-plaintext highlighter-rouge">kubectl-pgo</code> client binary.</li>
  <li><code class="language-plaintext highlighter-rouge">sudo mv ...</code>: Moves the binary to <code class="language-plaintext highlighter-rouge">/usr/local/bin</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">sudo chmod +x ...</code>: Makes the binary executable.</li>
</ul>

<h3 id="6-setup-namespace-and-install-database-cluster">6. Setup Namespace and Install Database Cluster</h3>

<p>Create the namespace and deploy your PostgreSQL cluster.</p>

<h4 id="commands-4">Commands</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
</pre></td><td class="rouge-code"><pre><span class="c"># Create the db-ns namespace</span>
kubectl create ns db-ns

<span class="c"># Create a directory for your cluster configuration</span>
<span class="nb">mkdir </span>mydatabase-k8s/

<span class="c"># Save the following YAML configuration to a file named mydatabase-13-demo.yaml</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> &gt; mydatabase-13-demo.yaml
apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PostgresCluster
metadata:
  name: mydatabase
  namespace: db-ns
  labels:
    pg-cluster: mydatabase
    pgo-version: 5.6.7
spec:
  image: registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-13.8-1
  postgresVersion: 13
  instances:
  - name: mydatabase
    replicas: 1
    dataVolumeClaimSpec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 5Gi
      storageClassName: rook-ceph-block
    resources:
      requests:
        cpu: "2"
        memory: "4Gi"
      limits:
        cpu: "2"
        memory: "4Gi"
  backups:
    pgbackrest:
      image: registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:ubi8-2.40-1
      repos:
      - name: repo1
        volume:
          volumeClaimSpec:
            accessModes: ["ReadWriteOnce"]
            resources:
              requests:
                storage: 5Gi
            storageClassName: rook-ceph-block
  monitoring:
    pgmonitor:
      exporter:
        image: registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-15.7-1
  users:
  - name: mydatabaseuser
    databases: [mydb]
  patroni:
    dynamicConfiguration:
      postgresql:
        parameters:
          max_connections: "600"
          shared_buffers: "4GB"
          effective_cache_size: "6GB"
          maintenance_work_mem: "2GB"
          checkpoint_completion_target: "0.9"
          wal_buffers: "16MB"
          default_statistics_target: "100"
          random_page_cost: "1.1"
          effective_io_concurrency: "200"
          min_wal_size: "1GB"
          max_wal_size: "4GB"
          max_worker_processes: "8"
          max_parallel_workers_per_gather: "4"
          max_parallel_workers: "8"
          max_parallel_maintenance_workers: "4"
          wal_keep_size: "2048MB"
          max_standby_archive_delay: "-1"
          hot_standby: "on"
</span><span class="no">EOF

</span><span class="c"># Apply the YAML configuration to create the PostgreSQL cluster</span>
kubectl apply <span class="nt">-f</span> mydatabase-13-demo.yaml
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="explanation-5">Explanation</h4>

<ul>
  <li><code class="language-plaintext highlighter-rouge">kubectl create ns db-ns</code>: Creates a new namespace named <code class="language-plaintext highlighter-rouge">db-ns</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">mkdir mydatabase-k8s/</code>: Creates a directory for the PostgreSQL cluster configuration.</li>
  <li><code class="language-plaintext highlighter-rouge">cat &lt;&lt;EOF &gt; mydatabase-13-demo.yaml ... EOF</code>: Saves the PostgreSQL cluster configuration to a file named <code class="language-plaintext highlighter-rouge">mydatabase-13-demo.yaml</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">kubectl apply -f mydatabase-13-demo.yaml</code>: Applies the configuration to create the PostgreSQL cluster.</li>
</ul>

<p>By following these detailed steps, you can set up a Kubernetes 29 cluster with Crunchy PGO 5.6.7, install necessary components, and deploy a PostgreSQL cluster.</p>]]></content><author><name>Moin Uddin Ahmed</name></author><category term="Kubernetes" /><category term="ceph" /><category term="cilium" /><category term="kubernetes" /><category term="linux" /><category term="postgresql" /><category term="rbd" /><category term="rook" /><summary type="html"><![CDATA[This documentation provides step-by-step instructions for setting up a Kubernetes 29 cluster, installing necessary components such as Cilium, Rook Ceph storage, and Crunchy PGO, and deploying a PostgreSQL cluster.]]></summary></entry><entry><title type="html">FAQ Document: Database and Backup Alerts Troubleshooting (Updated with PGO Crunchy FAQs)</title><link href="https://marufmoinuddin.github.io/blog/2025/07/faq-document-database-and-backup-alerts-troubleshooting-updated-with-pgo-crunchy/" rel="alternate" type="text/html" title="FAQ Document: Database and Backup Alerts Troubleshooting (Updated with PGO Crunchy FAQs)" /><published>2025-07-08T00:00:00+00:00</published><updated>2025-07-08T00:00:00+00:00</updated><id>https://marufmoinuddin.github.io/blog/2025/07/faq-document-database-and-backup-alerts-troubleshooting-updated-with-pgo-crunchy</id><content type="html" xml:base="https://marufmoinuddin.github.io/blog/2025/07/faq-document-database-and-backup-alerts-troubleshooting-updated-with-pgo-crunchy/"><![CDATA[<h1 id="faq-document-database-and-backup-alerts-troubleshooting-updated-with-pgo-crunchy-faqs">FAQ Document: Database and Backup Alerts Troubleshooting (Updated with PGO Crunchy FAQs)</h1>

<p>This FAQ document provides guidance on resolving common database, replication, and backup alerts, including additional scenarios specific to the Crunchy Data PostgreSQL Operator (PGO). Follow the instructions carefully to address each issue.</p>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ol>
  <li><a href="#1-database-alert-pod-in-error-state">Database Alert: Pod in Error State</a></li>
  <li><a href="#2-database-alert-container-status-unknown">Database Alert: Container Status Unknown</a></li>
  <li><a href="#3-backup-alert-missing-or-old-backup">Backup Alert: Missing or Old Backup</a></li>
  <li><a href="#4-db-replication-alert-replica-lag">DB Replication Alert: Replica Lag</a></li>
  <li><a href="#5-pgo-crunchy-high-cpu-usage-on-primary-pod">PGO Crunchy: High CPU Usage on Primary Pod</a></li>
  <li><a href="#6-pgo-crunchy-failed-to-create-new-replica">PGO Crunchy: Failed to Create New Replica</a></li>
  <li><a href="#7-pgo-crunchy-backup-job-stuck-in-pending-state">PGO Crunchy: Backup Job Stuck in Pending State</a></li>
  <li><a href="#8-pgo-crunchy-wal-archiving-failure">PGO Crunchy: WAL Archiving Failure</a></li>
</ol>

<hr />

<h2 id="1-database-alert-pod-in-error-state">1. Database Alert: Pod in Error State</h2>

<p><strong>Details:</strong></p>

<ul>
  <li><strong>Timestamp:</strong> 2025-04-09 07:00:03</li>
  <li><strong>Total Master DBs:</strong> 117</li>
  <li><strong>Errors:</strong> 2</li>
  <li><strong>Affected Pod Details:</strong>
    <ul>
      <li><strong>Name:</strong> nakadi-repo1-full</li>
      <li><strong>Pod:</strong> nakadi-repo1-full-29068975-fdnds</li>
      <li><strong>Ready:</strong> 0/1</li>
      <li><strong>Status:</strong> Error</li>
      <li><strong>Restart Count:</strong> 0</li>
      <li><strong>Age:</strong> 6h4m</li>
      <li><strong>IP:</strong> <pod-ip></pod-ip></li>
      <li><strong>Node:</strong> <worker-node-hostname></worker-node-hostname></li>
    </ul>
  </li>
</ul>

<p><strong>Explanation:</strong>
The pod <code class="language-plaintext highlighter-rouge">nakadi-repo1-full-29068975-fdnds</code> is in an error state (not ready, 0 restart count), which may indicate application failure or resource constraints. This could be due to insufficient resources, configuration issues, or application errors, potentially causing service disruption.</p>

<p><strong>Solution:</strong>
Delete the affected pod to resolve the error state and allow Kubernetes to recreate it.</p>

<p><strong>Steps:</strong></p>

<ol>
  <li>Open a terminal or command-line interface with <code class="language-plaintext highlighter-rouge">kubectl</code> access to the cluster.</li>
  <li>Run the following command to delete the pod:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl delete pod nakadi-repo1-full-29068975-fdnds
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Verify that a new pod is automatically recreated by Kubernetes:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get pods -l app=nakadi-repo1-full
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Check the status of the new pod to ensure it is in a <code class="language-plaintext highlighter-rouge">Running</code> state and <code class="language-plaintext highlighter-rouge">Ready: 1/1</code>.</li>
</ol>

<hr />

<h2 id="2-database-alert-container-status-unknown">2. Database Alert: Container Status Unknown</h2>

<p><strong>Details:</strong></p>

<ul>
  <li><strong>Timestamp:</strong> 2025-04-08 11:30:03</li>
  <li><strong>Total Master DBs:</strong> 117</li>
  <li><strong>Errors:</strong> 1</li>
  <li><strong>Affected Pod Details:</strong>
    <ul>
      <li><strong>Name:</strong> merchantfundtransfer-rsync-hqreplica</li>
      <li><strong>Pod:</strong> merchantfundtransfer-rsync-hqreplica-846c8dd6-t5zt6</li>
      <li><strong>Ready:</strong> 0/1</li>
      <li><strong>Status:</strong> ContainerStatusUnknown</li>
      <li><strong>Restart Count:</strong> 1</li>
      <li><strong>Age:</strong> 18d</li>
      <li><strong>IP:</strong> <none></none></li>
      <li><strong>Node:</strong> <worker-node-hostname></worker-node-hostname></li>
    </ul>
  </li>
</ul>

<p><strong>Solution:</strong>
Attempt the following steps in order to resolve the issue.</p>

<h3 id="attempt-1-restart-the-deployment">Attempt 1: Restart the Deployment</h3>
<p>In some cases, simply restarting the deployment can resolve the issue.</p>

<ol>
  <li>Open a terminal with <code class="language-plaintext highlighter-rouge">kubectl</code> access to the cluster.</li>
  <li>Restart the deployment to trigger a pod recreation:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl rollout restart deployment merchantfundtransfer-rsync-hqreplica
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Check the status of the pods:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get pods -l app=merchantfundtransfer-rsync-hqreplica
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>If the pod status becomes <code class="language-plaintext highlighter-rouge">Running</code> and <code class="language-plaintext highlighter-rouge">Ready: 1/1</code>, the issue is resolved. If not, proceed to Attempt 2.</li>
</ol>

<h3 id="attempt-2-delete-and-reapply-the-yaml">Attempt 2: Delete and Reapply the YAML</h3>
<p>If the pod remains in an unknown state, you may need to delete and reapply the original YAML configuration.</p>

<ol>
  <li>Delete the deployment:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl delete deployment merchantfundtransfer-rsync-hqreplica
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Reapply the original YAML configuration file (ensure you have the correct YAML file available):
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl apply -f merchantfundtransfer-rsync-hqreplica.yaml
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Verify the new pod is running:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get pods -l app=merchantfundtransfer-rsync-hqreplica
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<hr />

<h2 id="3-backup-alert-missing-or-old-backup">3. Backup Alert: Missing or Old Backup</h2>

<p><strong>Details:</strong></p>

<ul>
  <li><strong>Timestamp:</strong> 2025-04-08 08:30:53</li>
  <li><strong>Backups Done:</strong> 116</li>
  <li><strong>Missing/Old:</strong> 1</li>
  <li><strong>Affected Cluster Details:</strong>
    <ul>
      <li><strong>Cluster:</strong> banglalinkussd</li>
      <li><strong>Backup Status:</strong> 20250406-202522F (1 day old)</li>
    </ul>
  </li>
</ul>

<p><strong>Explanation:</strong>
The backup for the <code class="language-plaintext highlighter-rouge">banglalinkussd</code> cluster is either missing or outdated (older than 1 day). This could be due to a failed backup job, misconfiguration, or resource constraints.</p>

<p><strong>Solution:</strong>
Check if the backup process is still running by inspecting the corresponding backup pod.</p>

<p><strong>Steps:</strong></p>

<ol>
  <li>Open a terminal with <code class="language-plaintext highlighter-rouge">kubectl</code> access to the cluster.</li>
  <li>List the backup pods associated with the <code class="language-plaintext highlighter-rouge">banglalinkussd</code> cluster:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get pods -l cluster=banglalinkussd,role=backup
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Look for a pod with a status of <code class="language-plaintext highlighter-rouge">Running</code> instead of <code class="language-plaintext highlighter-rouge">Completed</code>. If found, the backup is still in progress—monitor it until completion:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl describe pod &lt;backup-pod-name&gt;
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>If no backup pod is running, investigate further (e.g., check backup logs or cronjob schedules) to determine why the backup is missing or outdated.</li>
</ol>

<hr />

<h2 id="4-db-replication-alert-replica-lag">4. DB Replication Alert: Replica Lag</h2>

<p><strong>Details:</strong></p>

<ul>
  <li><strong>Timestamp:</strong> 2025-04-04 09:00:34</li>
  <li><strong>Replicas with Lag:</strong> 1</li>
  <li><strong>Affected Pod Details:</strong>
    <ul>
      <li><strong>Pod:</strong> banglalinkussd-cluster-l7p8-0</li>
      <li><strong>Lag:</strong> 16 MB</li>
    </ul>
  </li>
</ul>

<p><strong>Explanation:</strong>
The replica <code class="language-plaintext highlighter-rouge">banglalinkussd-cluster-l7p8-0</code> is experiencing replication lag of 16 MB, which may indicate network issues, high load on the primary, or resource constraints on the replica.</p>

<p><strong>Solution:</strong>
Monitor the replication lag and take action if it increases. The action may involve reducing the number of replicas temporarily to allow the remaining replica to catch up.</p>

<p><strong>Steps:</strong></p>

<ol>
  <li>Access the pod to check the current lag:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl exec -it banglalinkussd-cluster-l7p8-0 -- patronictl list
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Review the output to confirm the lag value for the replica. If the lag is increasing over time, proceed with the next steps.</li>
  <li>Reduce the number of replicas to 1 to remove the lagging replica:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl patch postgrescluster.postgres-operator.crunchydata.com -n db-ns --type='json' -p='[{"op": "replace", "path": "/spec/instances/0/replicas", "value":1}]' banglalinkussd
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Wait for the cluster to stabilize, then verify the replica count:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get postgrescluster -n db-ns banglalinkussd -o jsonpath='{.spec.instances[0].replicas}'
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Increase the replica count back to 2:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl patch postgrescluster.postgres-operator.crunchydata.com -n db-ns --type='json' -p='[{"op": "replace", "path": "/spec/instances/0/replicas", "value":2}]' banglalinkussd
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Confirm the new replica is running and lag is resolved:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl exec -it banglalinkussd-cluster-l7p8-0 -- patronictl list
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<hr />

<h2 id="5-pgo-crunchy-high-cpu-usage-on-primary-pod">5. PGO Crunchy: High CPU Usage on Primary Pod</h2>

<p><strong>Details:</strong></p>

<ul>
  <li><strong>Timestamp:</strong> 2025-04-10 14:15:22</li>
  <li><strong>Cluster:</strong> flds</li>
  <li><strong>Affected Pod Details:</strong>
    <ul>
      <li><strong>Pod:</strong> flds-cluster-5f7d9c8b-kj9p2</li>
      <li><strong>Status:</strong> Running</li>
      <li><strong>CPU Usage:</strong> 95% (exceeding threshold of 80%)</li>
      <li><strong>Memory Usage:</strong> Normal</li>
      <li><strong>Node:</strong> <worker-node-hostname></worker-node-hostname></li>
    </ul>
  </li>
</ul>

<p><strong>Explanation:</strong>
The primary pod <code class="language-plaintext highlighter-rouge">flds-cluster-5f7d9c8b-kj9p2</code> is experiencing high CPU usage (95%), which may indicate a performance issue, such as long-running queries or insufficient resources allocated to the pod. This could lead to degraded performance for the database cluster and impact application functionality.</p>

<p><strong>Solution:</strong>
Investigate the high CPU usage and scale resources if necessary.</p>

<p><strong>Steps:</strong></p>

<ol>
  <li>Look for the master pod:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get pods -n db-ns  -l postgres-operator.crunchydata.com/role=master,postgres-operator.crunchydata.com/cluster=flds
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Check the pod logs for unusual activity:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl logs flds-cluster-5f7d9c8b-kj9p2 -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Exec into the pod and run <code class="language-plaintext highlighter-rouge">patronictl list</code> to verify the primary status and active queries:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl exec -it flds-cluster-5f7d9c8b-kj9p2 -n db-ns -- patronictl list
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Identify long-running queries using:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl exec -it flds-cluster-5f7d9c8b-kj9p2 -n db-ns -- psql -U postgres -c "SELECT pid, query, state, wait_event FROM pg_stat_activity WHERE state = 'active';"
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>If a specific query is causing the issue, terminate it:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl exec -it flds-cluster-5f7d9c8b-kj9p2 -n db-ns -- psql -U postgres -c "SELECT pg_terminate_backend(&lt;pid&gt;);"
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>If CPU usage remains high due to workload, scale the primary pod’s resources by editing the PostgresCluster spec:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl edit postgrescluster flds -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <ul>
      <li>Update the <code class="language-plaintext highlighter-rouge">resources</code> section under <code class="language-plaintext highlighter-rouge">spec.instances[0]</code>:
        <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="na">resources</span><span class="pi">:</span>
  <span class="na">requests</span><span class="pi">:</span>
    <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2"</span>
    <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">4Gi"</span>
  <span class="na">limits</span><span class="pi">:</span>
    <span class="na">cpu</span><span class="pi">:</span> <span class="s2">"</span><span class="s">4"</span>
    <span class="na">memory</span><span class="pi">:</span> <span class="s2">"</span><span class="s">8Gi"</span>
</pre></td></tr></tbody></table></code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>Apply the changes and monitor CPU usage:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl top pod flds-cluster-5f7d9c8b-kj9p2 -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<hr />

<h2 id="6-pgo-crunchy-failed-to-create-new-replica">6. PGO Crunchy: Failed to Create New Replica</h2>

<p><strong>Details:</strong></p>

<ul>
  <li><strong>Timestamp:</strong> 2025-04-11 09:45:12</li>
  <li><strong>Cluster:</strong> mydatabase</li>
  <li><strong>Event:</strong> Replica creation failed</li>
  <li><strong>Error Message:</strong> “Pod mydatabase-cluster-7d8f9c4b-mn5k3 in CrashLoopBackOff”</li>
  <li><strong>Affected Pod Details:</strong>
    <ul>
      <li><strong>Pod:</strong> mydatabase-cluster-7d8f9c4b-mn5k3</li>
      <li><strong>Status:</strong> CrashLoopBackOff</li>
      <li><strong>Restart Count:</strong> 5</li>
      <li><strong>Node:</strong> <worker-node-hostname></worker-node-hostname></li>
    </ul>
  </li>
</ul>

<p><strong>Explanation:</strong>
The replica pod <code class="language-plaintext highlighter-rouge">mydatabase-cluster-7d8f9c4b-mn5k3</code> is in a <code class="language-plaintext highlighter-rouge">CrashLoopBackOff</code> state, indicating repeated failures during startup. This could be due to misconfiguration, resource constraints, or issues with the primary pod.</p>

<p><strong>Solution:</strong>
Diagnose the crash and recreate the replica. If the issue persists, check the primary pod’s WAL sender status.</p>

<p><strong>Steps:</strong></p>

<ol>
  <li>Check the pod logs for the root cause:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl logs mydatabase-cluster-7d8f9c4b-mn5k3 -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Describe the pod to identify events or resource issues:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl describe pod mydatabase-cluster-7d8f9c4b-mn5k3 -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>If the issue is due to misconfiguration (e.g., WAL sync failure), delete the failing pod:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl delete pod mydatabase-cluster-7d8f9c4b-mn5k3 -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Verify that PGO recreates the replica:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get pods -l postgres-operator.crunchydata.com/cluster=mydatabase -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>If the replica continues to fail, check the primary’s WAL sender status:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl exec -it mydatabase-primary-6g5h8j9k-lp2m3 -n db-ns -- psql -U postgres -c "SELECT * FROM pg_stat_replication;"
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>If necessary, temporarily reduce replicas to stabilize, then increase again:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl patch postgrescluster mydatabase -n db-ns --type='json' -p='[{"op": "replace", "path": "/spec/instances/0/replicas", "value":1}]'
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>After stabilization:</p>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl patch postgrescluster mydatabase -n db-ns --type='json' -p='[{"op": "replace", "path": "/spec/instances/0/replicas", "value":2}]'
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<hr />

<h2 id="7-pgo-crunchy-backup-job-stuck-in-pending-state">7. PGO Crunchy: Backup Job Stuck in Pending State</h2>

<p><strong>Details:</strong></p>

<ul>
  <li><strong>Timestamp:</strong> 2025-04-12 03:10:45</li>
  <li><strong>Cluster:</strong> seblbanktransfer</li>
  <li><strong>Backup Job:</strong> seblbanktransfer-backup-20250412-0310</li>
  <li><strong>Status:</strong> Pending</li>
  <li><strong>Reason:</strong> “Insufficient resources or PVC binding failure”</li>
</ul>

<p><strong>Explanation:</strong>
The backup job <code class="language-plaintext highlighter-rouge">seblbanktransfer-backup-20250412-0310</code> is stuck in a pending state, indicating that it cannot be scheduled due to insufficient resources or issues with the Persistent Volume Claim (PVC) binding.</p>

<p><strong>Solution:</strong>
Resolve resource constraints or Persistent Volume Claim (PVC) issues.</p>

<p><strong>Steps:</strong></p>

<ol>
  <li>Check the backup job status:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get job seblbanktransfer-backup-20250412-0310 -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Describe the job to identify the issue:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl describe job seblbanktransfer-backup-20250412-0310 -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>If the pod is stuck due to resource limits, check node capacity:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl top nodes
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>If a PVC issue is reported, verify the PVC status:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get pvc -l postgres-operator.crunchydata.com/cluster=seblbanktransfer -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>If the PVC is not bound, ensure the storage class and capacity match the requirements, then delete and recreate the job:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl delete job seblbanktransfer-backup-20250412-0310 -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Trigger a new backup manually via the PostgresCluster spec:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> kubectl-pgo backup --repoName="repo1" --options="--type=full" -n db-ns seblbanktransfer
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Monitor the new backup job:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get jobs -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<hr />

<h2 id="8-pgo-crunchy-wal-archiving-failure">8. PGO Crunchy: WAL Archiving Failure</h2>

<h2 id="8-pgo-crunchy-wal-archiving-failure-1">8. PGO Crunchy: WAL Archiving Failure</h2>

<p><strong>Details:</strong></p>

<ul>
  <li><strong>Timestamp:</strong> 2025-04-13 06:20:15</li>
  <li><strong>Cluster:</strong> spinthewheel</li>
  <li><strong>Alert:</strong> WAL archiving failed</li>
  <li><strong>Error Message:</strong> “archive_command failed: could not connect to S3 bucket”</li>
  <li><strong>Affected Pod:</strong> spinthewheel-cluster-8k9j5h7d-pq3m4</li>
</ul>

<p><strong>Explanation:</strong>
The Write-Ahead Log (WAL) archiving process is failing for the <code class="language-plaintext highlighter-rouge">spinthewheel</code> cluster. WAL files are critical for point-in-time recovery and replication. The error indicates connectivity issues with the storage destination, affecting backup integrity and potentially causing disk space issues if WALs accumulate.</p>

<p><strong>Solution:</strong>
Diagnose and fix the WAL archiving configuration.</p>

<p><strong>Steps:</strong></p>

<ol>
  <li>Check the primary pod logs for detailed errors:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> kubectl logs spinthewheel-cluster-8k9j5h7d-pq3m4 -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Verify the <code class="language-plaintext highlighter-rouge">pgBackRest</code> configuration in the PostgresCluster spec:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> kubectl get postgrescluster spinthewheel -n db-ns -o yaml | grep -A 15 pgbackrest
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Check the storage configuration and permissions:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> kubectl get pvc -l postgres-operator.crunchydata.com/cluster=spinthewheel -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Ensure the storage volume has sufficient space:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> kubectl exec -it spinthewheel-cluster-8k9j5h7d-pq3m4 -n db-ns -- df -h
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Review PostgreSQL configuration for WAL archiving settings:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> kubectl exec -it spinthewheel-cluster-8k9j5h7d-pq3m4 -n db-ns -- psql -U postgres -c "SHOW archive_command;"
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Check the status of recent WAL archives:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> kubectl exec -it spinthewheel-cluster-8k9j5h7d-pq3m4 -n db-ns -- psql -U postgres -c "SELECT * FROM pg_stat_archiver;"
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Restart the pgBackRest repository host pod to reset connections:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> kubectl delete pod spinthewheel-repo-host -n db-ns
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Monitor archiving status after changes:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre> kubectl exec -it spinthewheel-cluster-8k9j5h7d-pq3m4 -n db-ns -- psql -U postgres -c "SELECT pg_walfile_name(pg_current_wal_lsn()), pg_walfile_name(pg_last_wal_receive_lsn()), pg_walfile_name(pg_last_wal_replay_lsn());"
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>]]></content><author><name>Moin Uddin Ahmed</name></author><category term="PostgreSQL" /><category term="backup" /><category term="kubernetes" /><category term="postgresql" /><category term="rsync" /><summary type="html"><![CDATA[This FAQ document provides guidance on resolving common database, replication, and backup alerts, including additional scenarios specific to the Crunchy Data PostgreSQL Operator (PGO). Follow the instructions carefully…]]></summary></entry><entry><title type="html">Logical Replication Tutorial</title><link href="https://marufmoinuddin.github.io/blog/2025/07/logical-replication-tutorial/" rel="alternate" type="text/html" title="Logical Replication Tutorial" /><published>2025-07-08T00:00:00+00:00</published><updated>2025-07-08T00:00:00+00:00</updated><id>https://marufmoinuddin.github.io/blog/2025/07/logical-replication-tutorial</id><content type="html" xml:base="https://marufmoinuddin.github.io/blog/2025/07/logical-replication-tutorial/"><![CDATA[<h1 id="logical-replication-tutorial">Logical Replication Tutorial</h1>

<h2 id="table-of-contents">Table of Contents</h2>
<ol>
  <li><a href="#introduction">Introduction</a></li>
  <li><a href="#step-1-export-schema-from-old-master">Step 1: Export Schema from Old Master</a></li>
  <li><a href="#step-2-copy-schema-backup-to-local-machine">Step 2: Copy Schema Backup to Local Machine</a></li>
  <li><a href="#step-3-copy-schema-backup-to-new-master">Step 3: Copy Schema Backup to New Master</a></li>
  <li><a href="#step-4-restore-schema-to-new-master">Step 4: Restore Schema to New Master</a></li>
  <li><a href="#step-5-enable-logical-replication-in-new-master">Step 5: Enable Logical Replication in New Master</a></li>
  <li><a href="#step-6-create-publication-on-old-master">Step 6: Create Publication on Old Master</a></li>
  <li><a href="#step-7-create-subscription-on-new-master">Step 7: Create Subscription on New Master</a></li>
  <li><a href="#step-8-verify-replication">Step 8: Verify Replication</a></li>
  <li><a href="#drawbacks-and-troubleshooting">Drawbacks and Troubleshooting</a></li>
</ol>

<h2 id="introduction">Introduction</h2>

<p>Logical replication allows you to replicate data between PostgreSQL databases at the table level. This is useful for migrating data, synchronizing databases, and setting up high availability. It helps in scenarios where you need a live copy of your data, without the need for physical replication, which might be more complex or unsuitable for certain use cases.</p>

<h2 id="step-1-export-schema-from-old-master">Step 1: Export Schema from Old Master</h2>

<p>First, we need to export the schema from the old master PostgreSQL cluster.</p>

<h3 id="command">Command</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl <span class="nb">exec</span> <span class="nt">-it</span> <span class="nt">-n</span> db-ns <span class="nt">-c</span> database <span class="si">$(</span>kubectl get pods <span class="nt">-n</span> db-ns <span class="nt">--selector</span><span class="o">=</span><span class="s1">'postgres-operator.crunchydata.com/cluster=mydatabase,postgres-operator.crunchydata.com/role=master'</span> <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.items[0].metadata.name}'</span><span class="si">)</span> <span class="nt">--</span> pg_dump <span class="nt">-s</span> <span class="nt">-U</span> postgres <span class="nt">-d</span> mydb <span class="nt">-f</span> /tmp/mydatabase_schema_bkp_old.sql
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Explanation:</strong> This command runs <code class="language-plaintext highlighter-rouge">pg_dump</code> inside the old master pod to export the schema (<code class="language-plaintext highlighter-rouge">-s</code>) of the <code class="language-plaintext highlighter-rouge">mydb</code> database to a file <code class="language-plaintext highlighter-rouge">/tmp/mydatabase_schema_bkp_old.sql</code>.</p>

<h2 id="step-2-copy-schema-backup-to-local-machine">Step 2: Copy Schema Backup to Local Machine</h2>

<p>Next, copy the schema backup file from the pod to your local machine.</p>

<h3 id="commands">Commands</h3>

<ol>
  <li><strong>Set the Old Master Pod Variable:</strong>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nv">OLD_MASTER_POD</span><span class="o">=</span><span class="si">$(</span>kubectl get pods <span class="nt">-n</span> db-ns <span class="nt">--selector</span><span class="o">=</span><span class="s1">'postgres-operator.crunchydata.com/cluster=mydatabase,postgres-operator.crunchydata.com/role=master'</span> <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.items[0].metadata.name}'</span><span class="si">)</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Copy the File from the Pod to Your Local Machine:</strong>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl <span class="nb">cp </span>db-ns/<span class="nv">$OLD_MASTER_POD</span>:/tmp/mydatabase_schema_bkp_old.sql /tmp/mydatabase_schema_bkp_old.sql
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<p><strong>Explanation:</strong> The <code class="language-plaintext highlighter-rouge">kubectl cp</code> command copies the backup file from the old master pod to your local machine, which is necessary for restoring the schema on the new master.</p>

<h2 id="step-3-copy-schema-backup-to-new-master-and-prepare-the-new-master-cluster-target">Step 3: Copy Schema Backup to New Master and Prepare the New Master Cluster (Target)</h2>

<p>Make sure that the new master has similar configuration of postgres or patroni settings as the old master for replication.</p>

<p><strong>Sample <code class="language-plaintext highlighter-rouge">postgresql.conf</code> settings:</strong></p>

<div class="language-conf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="n">wal_level</span> = <span class="n">logical</span>
<span class="n">max_replication_slots</span> = <span class="m">4</span>
<span class="n">max_wal_senders</span> = <span class="m">4</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Copy the schema backup file from your local machine to the new master PostgreSQL pod.</strong></p>

<h3 id="commands-1">Commands</h3>

<ol>
  <li><strong>Set the New Master Pod Variable:</strong>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nv">NEW_MASTER_POD</span><span class="o">=</span><span class="si">$(</span>kubectl get pods <span class="nt">-n</span> db-ns <span class="nt">--selector</span><span class="o">=</span><span class="s1">'postgres-operator.crunchydata.com/cluster=mydatabase-new,postgres-operator.crunchydata.com/role=master'</span> <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.items[0].metadata.name}'</span><span class="si">)</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Copy the File to the New Master Pod:</strong>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl <span class="nb">cp</span> /tmp/mydatabase_schema_bkp_old.sql db-ns/<span class="nv">$NEW_MASTER_POD</span>:/tmp/mydatabase_schema_bkp_old.sql
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<p><strong>Explanation:</strong> This command uploads the schema backup file to the new master pod where it can be restored.</p>

<h2 id="step-4-restore-schema-to-new-master">Step 4: Restore Schema to New Master</h2>

<p>Restore the schema on the new master PostgreSQL cluster.</p>

<h3 id="command-1">Command</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl <span class="nb">exec</span> <span class="nt">-it</span> <span class="nt">-n</span> db-ns <span class="nt">-c</span> database <span class="nv">$NEW_MASTER_POD</span> <span class="nt">--</span> psql <span class="nt">-U</span> postgres <span class="nt">-d</span> mydb <span class="nt">-f</span> /tmp/mydatabase_schema_bkp_old.sql
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Explanation:</strong> This command restores the schema from the backup file to the new master database. If there are errors related to missing roles, you may need to manually create these roles.</p>

<p><strong>Create Missing Roles:</strong></p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">ROLE</span> <span class="n">missing_role_name</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="step-5-enable-logical-replication-in-old-master">Step 5: Enable Logical Replication in Old Master</h2>

<p>Modify the YAML configuration of the old master PostgreSQL cluster to enable logical replication.</p>

<h3 id="with-yaml-configuration">With YAML Configuration</h3>
<p>Add the following section under <code class="language-plaintext highlighter-rouge">spec</code> in the old master’s YAML configuration:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="na">users</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">mydatabaseuser</span>
    <span class="na">databases</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">mydb</span>
    <span class="na">options</span><span class="pi">:</span> <span class="s2">"</span><span class="s">REPLICATION"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Log into the old master PostgreSQL cluster and create a publication.</p>

<h3 id="commands-2">Commands</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>   bash <span class="nt">-c</span> <span class="s1">'kubectl exec -it -n db-ns -c database \
  $(kubectl get pods -n db-ns --selector='</span>postgres-operator.crunchydata.com/cluster<span class="o">=</span>mydatabase,postgres-operator.crunchydata.com/role<span class="o">=</span>master<span class="s1">' -o name) -- psql mydb'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Run this inside the PostgreSQL session:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="n">PUBLICATION</span> <span class="n">mydatabase_pub</span> <span class="k">FOR</span> <span class="k">ALL</span> <span class="n">TABLES</span><span class="p">;</span>
<span class="err">\</span><span class="n">q</span>
</pre></td></tr></tbody></table></code></pre></div></div>
<p><strong>Explanation:</strong> This command creates a publication on the old master, which will send changes to the new master.</p>

<h3 id="without-yaml-configuration">Without YAML Configuration</h3>
<p>If you dont want to edit the yaml, do this:</p>

<p>Log into the old master PostgreSQL cluster and create a publication.</p>

<h3 id="commands-3">Commands</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>   bash <span class="nt">-c</span> <span class="s1">'kubectl exec -it -n db-ns -c database \
  $(kubectl get pods -n db-ns --selector='</span>postgres-operator.crunchydata.com/cluster<span class="o">=</span>mydatabase,postgres-operator.crunchydata.com/role<span class="o">=</span>master<span class="s1">' -o name) -- psql mydb'</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Run this inside the PostgreSQL session:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">ALTER</span> <span class="k">ROLE</span> <span class="n">mydatabaseuser</span> <span class="k">WITH</span> <span class="n">REPLICATION</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="n">PUBLICATION</span> <span class="n">mydatabase_pub</span> <span class="k">FOR</span> <span class="k">ALL</span> <span class="n">TABLES</span><span class="p">;</span>

</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Explanation:</strong> This command promotes the user to replication user and creates a publication on the old master, which will send changes to the new master</p>

<h2 id="step-6-create-subscription-on-new-master">Step 6: Create Subscription on New Master</h2>

<p>Get the connection information for the old master and create a subscription on the new master. Or if you have those already, you can directly insert them by Skipping below 1.</p>

<h3 id="commands-4">Commands</h3>

<ol>
  <li><strong>Get Connection Information:</strong>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre>kubectl <span class="nt">-n</span> db-ns get secrets mydatabase-pguser-mydatabaseuser <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">={</span>.data.host<span class="o">}</span> | <span class="nb">base64</span> <span class="nt">-d</span> 
<span class="nb">echo
</span>kubectl <span class="nt">-n</span> db-ns get secrets mydatabase-pguser-mydatabaseuser <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">={</span>.data.user<span class="o">}</span> | <span class="nb">base64</span> <span class="nt">-d</span> 
<span class="nb">echo
</span>kubectl <span class="nt">-n</span> db-ns get secrets mydatabase-pguser-mydatabaseuser <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">={</span>.data.password<span class="o">}</span> | <span class="nb">base64</span> <span class="nt">-d</span> 
<span class="nb">echo</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Create Subscription:</strong>
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>bash <span class="nt">-c</span> <span class="s1">'kubectl exec -it -n db-ns -c database \
$(kubectl get pods -n db-ns --selector='</span>postgres-operator.crunchydata.com/cluster<span class="o">=</span>mydatabase-new,postgres-operator.crunchydata.com/role<span class="o">=</span>master<span class="s1">' -o name) -- psql mydb'</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>

    <p>Inside the PostgreSQL session:</p>
    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="n">SUBSCRIPTION</span> <span class="n">mydatabase_sub</span> <span class="k">CONNECTION</span> <span class="s1">'host=&lt;REDACTED_IP&gt; port=&lt;REDACTED_PORT&gt; user=&lt;REDACTED_USERNAME&gt; dbname=mydb password=&lt;REDACTED_PASSWORD&gt;'</span> <span class="n">PUBLICATION</span> <span class="n">mydatabase_pub</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<p><strong>Explanation:</strong> This sets up a subscription on the new master to receive changes from the publication created on the old master.</p>

<blockquote>
  <p>Note: Replace <code class="language-plaintext highlighter-rouge">&lt;OLD_MASTER_HOST&gt;</code> with the IP address, <code class="language-plaintext highlighter-rouge">&lt;OLD_MASTER_EXPOSED_PORT&gt;</code> with the exposed port, <code class="language-plaintext highlighter-rouge">&lt;OLD_MASTER_USER&gt;</code> with the db replication user (you must make the schema user as replication user) and <code class="language-plaintext highlighter-rouge">&lt;OLD_MASTER_PASSWORD&gt;</code> with the password you have. 
Note: If you dont want to edit the yaml, enter to your pod  <code class="language-plaintext highlighter-rouge">ALTER ROLE mydatabaseuser WITH REPLICATION;</code></p>
</blockquote>

<h2 id="step-7-verify-replication">Step 7: Verify Replication</h2>

<p>Ensure that data is being replicated from the old master to the new master.</p>

<h3 id="insert-record-on-old-master">Insert Record on Old Master</h3>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">mydatabaseuser</span><span class="p">.</span><span class="n">ecommerce_payment_ecommercepaymentaddmoney</span> <span class="p">(</span>
    <span class="n">id</span><span class="p">,</span> <span class="n">to_wallet</span><span class="p">,</span> <span class="n">to_wallet_id</span><span class="p">,</span> <span class="n">amount</span><span class="p">,</span> <span class="n">currency</span><span class="p">,</span> <span class="k">source</span><span class="p">,</span> <span class="n">description</span><span class="p">,</span> 
    <span class="n">initiated_by</span><span class="p">,</span> <span class="n">trx_id</span><span class="p">,</span> <span class="n">batch_id</span><span class="p">,</span> <span class="n">order_id</span><span class="p">,</span> <span class="n">session_id</span><span class="p">,</span> <span class="n">create_order_url</span><span class="p">,</span> 
    <span class="n">status</span><span class="p">,</span> <span class="n">medium</span><span class="p">,</span> <span class="n">initiated_at</span><span class="p">,</span> <span class="n">completed_at</span><span class="p">,</span> <span class="n">card_number</span><span class="p">,</span> <span class="n">card_brand</span><span class="p">,</span> 
    <span class="n">reason_description</span><span class="p">,</span> <span class="n">confirm_response</span><span class="p">,</span> <span class="n">service_charge_details</span><span class="p">,</span> 
    <span class="n">sys_update_datetime</span><span class="p">,</span> <span class="n">rrn_number</span>
<span class="p">)</span> <span class="k">VALUES</span> <span class="p">(</span>
    <span class="s1">'5f6d52b6-0a22-44b8-8c4c-fbde9e2b38b8'</span><span class="p">,</span> <span class="s1">'0987654321'</span><span class="p">,</span> <span class="mi">987654</span><span class="p">,</span> <span class="mi">1500</span><span class="p">.</span><span class="mi">00</span><span class="p">,</span> <span class="s1">'EUR'</span><span class="p">,</span> <span class="s1">'promotion'</span><span class="p">,</span> 
    <span class="s1">'summer sale'</span><span class="p">,</span> <span class="s1">'customer'</span><span class="p">,</span> <span class="s1">'trx_2027'</span><span class="p">,</span> <span class="s1">'batch_2027'</span><span class="p">,</span> <span class="mi">987654321</span><span class="p">,</span> 
    <span class="s1">'session_2027'</span><span class="p">,</span> <span class="s1">'https://example.com/sale'</span><span class="p">,</span> <span class="s1">'completed'</span><span class="p">,</span> <span class="s1">'mobile app'</span><span class="p">,</span> 
    <span class="n">NOW</span><span class="p">(),</span> <span class="n">NOW</span><span class="p">(),</span> <span class="s1">'8765-4321-0987-6543'</span><span class="p">,</span> <span class="s1">'Visa'</span><span class="p">,</span> <span class="s1">'seasonal discount'</span><span class="p">,</span> 
    <span class="s1">'confirmed'</span><span class="p">,</span> <span class="s1">'{"details": "discount applied"}'</span><span class="p">,</span> <span class="n">NOW</span><span class="p">(),</span> <span class="s1">'rrn_2027'</span>
<span class="p">);</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="verify-on-new-master">Verify on New Master</h3>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">mydatabaseuser</span><span class="p">.</span><span class="n">ecommerce_payment_ecommercepaymentaddmoney</span> 
<span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="s1">'5f6d52b6-0a22-44b8-8c4c-fbde9e2b38b8'</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="drawbacks-and-troubleshooting">Drawbacks and Troubleshooting</h2>

<h3 id="drawbacks">Drawbacks</h3>
<ul>
  <li><strong>Sequential Numbering:</strong> Logical replication uses a sequence number to track changes. If the sequence number isn’t managed properly, it can lead to data inconsistencies or missing changes.</li>
  <li><strong>Replication Lag:</strong> Logical replication may experience lag, especially with large volumes of data or high update rates.</li>
  <li><strong>Schema Changes:</strong> Changes to the schema in the source database (e.g., adding/removing columns) need to be carefully managed as they can affect replication.</li>
</ul>

<h3 id="troubleshooting">Troubleshooting</h3>
<ul>
  <li><strong>Check Replication Status:</strong>
    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">pg_stat_subscription</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
    <p>This view shows the status of subscriptions and can help identify issues.</p>
  </li>
  <li>
    <p><strong>Examine Logs:</strong> Look at the PostgreSQL logs on both the source and destination servers for errors or warnings related to replication.</p>
  </li>
  <li>
    <p><strong>Verify Roles and Permissions:</strong> Ensure that the replication user has the necessary permissions to access and replicate data.</p>
  </li>
  <li><strong>Monitor Replication Lag:</strong> Use tools like <code class="language-plaintext highlighter-rouge">pg_stat_replication</code> to monitor replication lag and ensure it is within acceptable limits.</li>
</ul>

<h3 id="troubleshooting-replication-slot-error-in-postgresql-subscription-management">Troubleshooting: Replication Slot Error in PostgreSQL Subscription Management</h3>

<h4 id="scenario">Scenario</h4>

<p>In a PostgreSQL environment, you encountered issues with managing a replication subscription between an old master and a new master. The specific problem involved a replication slot error when attempting to drop a subscription. Here’s a detailed description of the scenario:</p>

<ol>
  <li><strong>Subscription and Replication Slot Details:</strong>
    <ul>
      <li><strong>Old Master Logs:</strong>
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>2024-09-06 15:38:06,638 INFO: Lock owner: mydatabase-5c7875bf8d-zppgg; I am mydatabase-5c7875bf8d-zppgg
2024-09-06 15:38:06,649 INFO: no action.  i am the leader with the lock
2024-09-06 15:38:09.855 UTC [24782] ERROR:  replication slot "mydatabase_sub" does not exist
2024-09-06 15:38:14.890 UTC [24800] ERROR:  replication slot "mydatabase_sub" does not exist
</pre></td></tr></tbody></table></code></pre></div>        </div>
      </li>
      <li><strong>New Master Logs:</strong>
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre>postgres=# \c mydb 
You are now connected to database "mydb" as user "postgres".
mydb=# select * from pg_subscription;
  oid  | subdbid |  subname   | subowner | subenabled |                                         subconninfo                                          | subslotname | subsynccommit | subpublications 
-------+---------+------------+----------+------------+----------------------------------------------------------------------------------------------+-------------+---------------+-----------------
  16801 |   16406 | mydatabase_sub |       10 | t          | host=&lt;REDACTED_IP&gt; port=&lt;REDACTED_PORT&gt; user=&lt;REDACTED_USERNAME&gt; dbname=mydb password=&lt;REDACTED_PASSWORD&gt; | mydatabase_sub  | off           | {mydatabase_pub}
(1 row)
     
mydb=# drop subscription mydatabase_sub;
ERROR:  could not drop the replication slot "mydatabase_sub" on publisher
DETAIL:  The error was: ERROR:  replication slot "mydatabase_sub" does not exist
</pre></td></tr></tbody></table></code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li><strong>Troubleshooting Steps Taken:</strong>
    <ul>
      <li><strong>Direct Query Attempts:</strong>
        <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="n">mydb</span><span class="o">=#</span> <span class="k">DELETE</span> <span class="k">FROM</span> <span class="n">pg_subscription</span> <span class="k">WHERE</span> <span class="n">subname</span> <span class="o">=</span> <span class="s1">'mydatabase_sub'</span><span class="p">;</span>
<span class="k">DELETE</span> <span class="mi">1</span>
<span class="n">mydb</span><span class="o">=#</span> <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">pg_subscription</span><span class="p">;</span>
 <span class="n">oid</span> <span class="o">|</span> <span class="n">subdbid</span> <span class="o">|</span> <span class="n">subname</span> <span class="o">|</span> <span class="n">subowner</span> <span class="o">|</span> <span class="n">subenabled</span> <span class="o">|</span> <span class="n">subconninfo</span> <span class="o">|</span> <span class="n">subslotname</span> <span class="o">|</span> <span class="n">subsynccommit</span> <span class="o">|</span> <span class="n">subpublications</span> 
<span class="c1">-----+---------+---------+----------+------------+-------------+-------------+---------------+-----------------</span>
<span class="p">(</span><span class="mi">0</span> <span class="k">rows</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div>        </div>
      </li>
    </ul>
  </li>
</ol>

<h4 id="troubleshooting-steps-and-resolutions">Troubleshooting Steps and Resolutions</h4>

<ol>
  <li><strong>Verify Replication Slot Existence:</strong>
    <ul>
      <li>On the old master, check if the replication slot <code class="language-plaintext highlighter-rouge">mydatabase_sub</code> still exists. This can be done by querying the <code class="language-plaintext highlighter-rouge">pg_replication_slots</code> view:
        <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">pg_replication_slots</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div>        </div>
      </li>
      <li>If the replication slot does not exist on the old master, it is likely that the slot was manually removed or the old master has not been correctly updated.</li>
    </ul>
  </li>
  <li><strong>Manually Remove Subscription Metadata:</strong>
    <ul>
      <li>Since the <code class="language-plaintext highlighter-rouge">pg_subscription</code> table entry was successfully removed, verify that there are no remnants of the subscription in other related catalog tables such as <code class="language-plaintext highlighter-rouge">pg_publication</code> if you plan to clean up completely:
        <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">pg_publication</span><span class="p">;</span>
<span class="k">DELETE</span> <span class="k">FROM</span> <span class="n">pg_publication</span> <span class="k">WHERE</span> <span class="n">pubname</span> <span class="o">=</span> <span class="s1">'mydatabase_pub'</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li><strong>Check for Subscription and Slot Cleanup on New Master:</strong>
    <ul>
      <li>On the new master, ensure that all subscription-related metadata is cleaned up. Confirm that no orphaned subscription entries or replication slots exist.</li>
    </ul>
  </li>
  <li><strong>Recreate Subscription if Needed:</strong>
    <ul>
      <li>If you plan to recreate the subscription, ensure that the replication slots are properly set up and there are no conflicts. Create the subscription again with the correct configurations.</li>
    </ul>
  </li>
  <li><strong>Validate Configuration and Permissions:</strong>
    <ul>
      <li>Ensure that the user roles and permissions are correctly set up for both the new master and the old master. Verify that the connection settings and credentials are correct.</li>
    </ul>
  </li>
  <li><strong>Monitor Logs for Additional Errors:</strong>
    <ul>
      <li>Continue to monitor the PostgreSQL logs on both master and standby nodes for any additional errors or warnings that may indicate underlying issues.</li>
    </ul>
  </li>
</ol>

<p>Following these steps and guidelines should help you set up and troubleshoot logical replication between PostgreSQL clusters effectively.</p>]]></content><author><name>Moin Uddin Ahmed</name></author><category term="PostgreSQL" /><category term="backup" /><category term="high-availability" /><category term="logical-replication" /><category term="patroni" /><category term="postgresql" /><summary type="html"><![CDATA[1. Introduction 2. Step 1: Export Schema from Old Master 3. Step 2: Copy Schema Backup to Local Machine 4. Step 3: Copy Schema Backup to New Master]]></summary></entry><entry><title type="html">PostgreSQL High-Availability Cluster Deployment Guide</title><link href="https://marufmoinuddin.github.io/blog/2025/07/postgresql-high-availability-cluster-deployment-guide/" rel="alternate" type="text/html" title="PostgreSQL High-Availability Cluster Deployment Guide" /><published>2025-07-08T00:00:00+00:00</published><updated>2025-07-08T00:00:00+00:00</updated><id>https://marufmoinuddin.github.io/blog/2025/07/postgresql-high-availability-cluster-deployment-guide</id><content type="html" xml:base="https://marufmoinuddin.github.io/blog/2025/07/postgresql-high-availability-cluster-deployment-guide/"><![CDATA[<h1 id="postgresql-high-availability-cluster-deployment-guide">PostgreSQL High-Availability Cluster Deployment Guide</h1>

<h2 id="executive-summary">Executive Summary</h2>
<p>This comprehensive guide provides end-to-end instructions for deploying a resilient PostgreSQL cluster using the Crunchy Data PostgreSQL Operator on Kubernetes. The solution integrates high availability, automated backups, performance optimization, and robust monitoring to ensure enterprise-grade database operations. The document combines architectural guidance with actionable implementation steps.</p>

<hr />

<h2 id="1-prerequisites">1. Prerequisites</h2>

<p>Before deployment, ensure your environment meets these requirements. As a best practice, review the <a href="https://access.crunchydata.com/documentation/postgres-operator/latest/">Crunchy Data PostgreSQL Operator documentation</a> for the latest updates.</p>

<ol>
  <li><strong>Kubernetes Cluster</strong>:
    <ul>
      <li>Operational Kubernetes v1.20+ cluster with worker nodes</li>
      <li>Network policies allowing pod communication</li>
      <li><code class="language-plaintext highlighter-rouge">kubectl</code> configured with cluster access</li>
    </ul>
  </li>
  <li><strong>Infrastructure</strong>:
    <ul>
      <li>Nodes labeled <code class="language-plaintext highlighter-rouge">uclick=enabled</code> for database workloads</li>
      <li>Rook-Ceph storage provisioner installed (or equivalent CSI-compatible storage)</li>
    </ul>
  </li>
  <li><strong>Tools</strong>:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">git</code>, <code class="language-plaintext highlighter-rouge">wget</code>, and <code class="language-plaintext highlighter-rouge">curl</code> utilities</li>
      <li>Administrative access to install cluster operators</li>
    </ul>
  </li>
</ol>

<hr />

<h2 id="2-component-installation">2. Component Installation</h2>

<p>This section outlines the installation steps for the PostgreSQL Operator and the Operator Client Tool. This will work in conjunction with the Kubernetes cluster to manage the PostgreSQL database lifecycle. To learn more about the PostgreSQL Operator, refer to the <a href="https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/basic-setup">official documentation</a>.</p>

<h3 id="21-crunchy-postgresql-operator">2.1 Crunchy PostgreSQL Operator</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre>git clone https://github.com/CrunchyData/postgres-operator-examples.git
<span class="nb">cd </span>postgres-operator-examples/
kubectl apply <span class="nt">-k</span> kustomize/install/namespace
kubectl apply <span class="nt">--server-side</span> <span class="nt">-k</span> kustomize/install/default
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="22-operator-client-tool">2.2 Operator Client Tool</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>wget https://github.com/CrunchyData/postgres-operator-client/releases/download/v0.4.2/kubectl-pgo-linux-amd64
<span class="nb">sudo mv </span>kubectl-pgo-linux-amd64 /usr/local/bin/kubectl-pgo
<span class="nb">sudo chmod</span> +x /usr/local/bin/kubectl-pgo
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="3-cluster-architecture-configuration">3. Cluster Architecture Configuration</h2>

<p>This section defines the PostgreSQL cluster architecture, including the core components, topology rules, and storage configuration. The deployment follows best practices for high availability, performance optimization, and data protection. To do more advanced configurations, refer to the <a href="https://access.crunchydata.com/documentation/postgres-operator/latest/">Crunchy Data PostgreSQL Operator documentation</a>.</p>

<h3 id="31-core-components">3.1 Core Components</h3>
<p>| Component          | Specification                          | Purpose                                  |
|———————|—————————————-|——————————————|
| <strong>PostgreSQL</strong>      | v16 (Crunchy UBI8 image)              | Transactional database engine            |
| <strong>Replication</strong>     | 1 Primary + 1 Standby                 | High availability                        |
| <strong>Connection Pool</strong> | PgBouncer (optional)                  | Connection management                    |
| <strong>Backups</strong>         | pgBackRest with daily full backups     | Data protection                          |
| <strong>Monitoring</strong>      | Prometheus exporter                    | Performance metrics collection           |</p>

<h3 id="32-topology-rules">3.2 Topology Rules</h3>
<ul>
  <li><strong>Pod Anti-Affinity</strong>: Enforces replica distribution across nodes</li>
  <li><strong>Node Affinity</strong>: Restricts to <code class="language-plaintext highlighter-rouge">uclick=enabled</code> labeled nodes</li>
  <li><strong>Storage Class</strong>: Uses <code class="language-plaintext highlighter-rouge">rook-ceph-block</code> for persistent volumes</li>
</ul>

<hr />

<h2 id="4-deployment-configuration">4. Deployment Configuration</h2>

<p>This is a customized deployment configuration for the PostgreSQL cluster. It includes the namespace setup, cluster manifest, and deployment commands. The configuration is optimized for performance, security, and operational efficiency. For advanced configurations, refer to the <a href="https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/day-two">Crunchy Data PostgreSQL Operator documentation</a>. For CRD reference, refer to the <a href="https://access.crunchydata.com/documentation/postgres-operator/latest/references/crd">PostgresCluster CRD</a>.</p>

<h3 id="41-namespace-setup">4.1 Namespace Setup</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl create ns db-ns
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="42-cluster-manifest-yaml">4.2 Cluster Manifest (YAML)</h3>
<p>Create <code class="language-plaintext highlighter-rouge">mydatabase.yaml</code> with the following configuration:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
</pre></td><td class="rouge-code"><pre>  <span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> &gt; mydatabase.yaml
  #===============================================================================================
  # PostgreSQL Cluster Configuration using Crunchy Data PostgreSQL Operator
  #===============================================================================================

  # DB User Creation Secret
  apiVersion: v1
  kind: Secret
  metadata:
    name: mydatabaseuser-secret
    namespace: db-ns
    labels:
      postgres-operator.crunchydata.com/cluster: mydatabase
      postgres-operator.crunchydata.com/pguser: mydatabaseuser
  data:
    password: &lt;your-base64-encoded-password-here&gt;
  type: Opaque
  ---
  # Master Database Cluster Creation
  apiVersion: postgres-operator.crunchydata.com/v1beta1
  kind: PostgresCluster
  metadata:
    name: mydatabase
    namespace: db-ns
    annotations:
      postgres-operator.crunchydata.com/autoCreateUserSchema: "true"
  spec:
    image: registry.developers.crunchydata.com/crunchydata/crunchy-postgres:ubi8-16.1-0
    imagePullPolicy: IfNotPresent
    postgresVersion: 16

    instances:
      - name: cluster
        replicas: 2
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 2000m
            memory: 2Gi
        affinity:
          podAntiAffinity:
            requiredDuringSchedulingIgnoredDuringExecution:
              - labelSelector:
                  matchLabels:
                    postgres-operator.crunchydata.com/cluster: mydatabase
                    postgres-operator.crunchydata.com/instance-set: cluster
                topologyKey: kubernetes.io/hostname
          nodeAffinity:
            requiredDuringSchedulingIgnoredDuringExecution:
              nodeSelectorTerms:
                - matchExpressions:
                    - key: uclick
                      operator: In
                      values:
                        - enabled
        dataVolumeClaimSpec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 4Gi
          storageClassName: rook-ceph-block
        walVolumeClaimSpec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 4Gi
          storageClassName: rook-ceph-block

    # Patroni Dynamic Configuration for High Availability and Performance
    patroni:
      dynamicConfiguration:
        postgresql:
          parameters:
            archive_timeout: 60
            jit: true
            max_wal_senders: 6
            max_replication_slots: 6
            shared_preload_libraries: pgaudit,pg_stat_statements,pgnodemx
            temp_buffers: 8
            unix_socket_directories: /tmp
            work_mem: 16
            max_connections: 600
            log_directory: pg_log
            log_min_duration_statement: 60000
            log_statement: none
            log_destination: "stderr"
            logging_collector: "off"
            archive_mode: "on"
            shared_buffers: 1024
            effective_cache_size: 1024
            maintenance_work_mem: 410
            checkpoint_completion_target: 0.9
            default_statistics_target: 100
            random_page_cost: 1.1
            effective_io_concurrency: 200
            wal_level: logical
            wal_buffers: 16
            min_wal_size: 1024
            max_wal_size: 4096
            wal_keep_size: 2048
          pg_hba:
            - host all all &lt;REDACTED_IP&gt;/0 md5
            - local all all trust
        ttl: 30
        retry_timeout: 30
        maximum_lag_on_failover: 0

    # Database User Creation
    users:
      - name: mydatabaseuser
        databases: [mydatabasedb]

    # Backup Configuration with pgBackRest
    backups:
      pgbackrest:
        image: registry.developers.crunchydata.com/crunchydata/crunchy-pgbackrest:ubi8-2.47-1
        repos:
          - name: repo1
            schedules:
              full: "33 22 * * *"
            volume:
              volumeClaimSpec:
                accessModes:
                  - ReadWriteOnce
                resources:
                  requests:
                    storage: 4Gi
        sidecars:
          pgbackrest:
            resources:
              requests:
                cpu: 500m
                memory: 512Mi
              limits:
                cpu: 1000m
                memory: 1Gi
        global:
          repo1-retention-full: "3"
          repo1-retention-full-type: count
        repoHost:
          affinity:
            nodeAffinity:
              requiredDuringSchedulingIgnoredDuringExecution:
                nodeSelectorTerms:
                  - matchExpressions:
                      - key: uclick
                        operator: In
                        values:
                          - enabled

    # Monitoring Configuration
    monitoring:
      pgmonitor:
        exporter:
          image: registry.developers.crunchydata.com/crunchydata/crunchy-postgres-exporter:ubi8-5.6.0-0
          resources:
            requests:
              cpu: 50m
              memory: 100Mi
            limits:
              cpu: 100m
              memory: 200Mi
</span><span class="no">  EOF
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="43-cluster-deployment">4.3 Cluster Deployment</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl apply <span class="nt">-f</span> mydatabase.yaml
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="5-operational-configuration">5. Operational Configuration</h2>

<p>This section covers the operational aspects of the PostgreSQL cluster, including performance optimization, connection management, backup strategy, and security implementation. The configurations are designed to ensure the cluster’s reliability, scalability, and security. For advanced configurations, refer to the <a href="https://access.crunchydata.com/documentation/postgres-operator/latest/">Crunchy Data PostgreSQL Operator documentation</a>.</p>

<h3 id="51-performance-optimization">5.1 Performance Optimization</h3>
<p>| Parameter                  | Value      | Impact                                  |
|—————————-|————|—————————————–|
| <code class="language-plaintext highlighter-rouge">shared_buffers</code>           | 25% RAM    | Data caching efficiency                 |
| <code class="language-plaintext highlighter-rouge">work_mem</code>                 | 4-16MB     | Sort/hash operation performance         |
| <code class="language-plaintext highlighter-rouge">max_parallel_workers</code>     | 4          | Concurrent query processing             |
| <code class="language-plaintext highlighter-rouge">wal_buffers</code>              | 16MB       | Write-ahead log performance             |</p>

<h3 id="52-connection-management">5.2 Connection Management</h3>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
</pre></td><td class="rouge-code"><pre><span class="c1"># PgBouncer Deployment Configuration</span>
  <span class="na">proxy</span><span class="pi">:</span>
    <span class="na">pgBouncer</span><span class="pi">:</span>
      <span class="na">image</span><span class="pi">:</span> <span class="s">registry.developers.crunchydata.com/crunchydata/crunchy-pgbouncer:ubi8-1.23-2</span>
      <span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
      <span class="na">minAvailable</span><span class="pi">:</span> <span class="m">1</span>
      <span class="c1"># ===================================================</span>
      <span class="c1">#   Under config.global we can add pgBouncer configuration</span>
      <span class="c1">#   https://www.pgbouncer.org/config.html</span>
      <span class="c1"># ===================================================</span>
      <span class="na">config</span><span class="pi">:</span>
        <span class="na">global</span><span class="pi">:</span>
          <span class="na">default_pool_size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">20"</span>
          <span class="na">max_client_conn</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10000"</span>
          <span class="na">max_db_connections</span><span class="pi">:</span> <span class="s2">"</span><span class="s">5000"</span>
          <span class="na">min_pool_size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
          <span class="na">pool_mode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">session"</span>
          <span class="na">reserve_pool_size</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
          <span class="na">reserve_pool_timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">5"</span>
          <span class="na">query_timeout</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
          <span class="na">ignore_startup_parameters</span><span class="pi">:</span> <span class="s2">"</span><span class="s">extra_float_digits"</span>
          <span class="na">client_tls_sslmode</span><span class="pi">:</span> <span class="s2">"</span><span class="s">allow"</span>
      <span class="na">resources</span><span class="pi">:</span>
        <span class="na">requests</span><span class="pi">:</span>
          <span class="na">cpu</span><span class="pi">:</span> <span class="s">500m</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="s">256Mi</span>
        <span class="na">limits</span><span class="pi">:</span>
          <span class="na">cpu</span><span class="pi">:</span> <span class="s">1000m</span>
          <span class="na">memory</span><span class="pi">:</span> <span class="s">1Gi</span>
      <span class="na">affinity</span><span class="pi">:</span>
        <span class="na">podAntiAffinity</span><span class="pi">:</span>
          <span class="na">requiredDuringSchedulingIgnoredDuringExecution</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">labelSelector</span><span class="pi">:</span>
              <span class="na">matchLabels</span><span class="pi">:</span>
                <span class="na">postgres-operator.crunchydata.com/cluster</span><span class="pi">:</span> <span class="s">mydatabase</span>
                <span class="na">postgres-operator.crunchydata.com/instance-set</span><span class="pi">:</span> <span class="s">cluster</span>
            <span class="na">topologyKey</span><span class="pi">:</span> <span class="s">kubernetes.io/hostname</span>
        <span class="na">nodeAffinity</span><span class="pi">:</span>
          <span class="na">requiredDuringSchedulingIgnoredDuringExecution</span><span class="pi">:</span>
            <span class="na">nodeSelectorTerms</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="na">matchExpressions</span><span class="pi">:</span>
              <span class="pi">-</span> <span class="na">key</span><span class="pi">:</span> <span class="s">uclick</span>
                <span class="na">operator</span><span class="pi">:</span> <span class="s">In</span>
                <span class="na">values</span><span class="pi">:</span>
                <span class="pi">-</span> <span class="s">enabled</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="53-backup-strategy">5.3 Backup Strategy</h3>
<ul>
  <li><strong>Schedule</strong>: Daily full backups retained for 3 days</li>
  <li><strong>Storage</strong>: Dedicated volume with Ceph replication</li>
  <li><strong>Restoration</strong>: Performed via pgBackRest CLI</li>
</ul>

<hr />

<h2 id="6-security-implementation">6. Security Implementation</h2>

<p>This section outlines the security measures implemented in the PostgreSQL cluster to protect data integrity, confidentiality, and availability. The configurations include access controls, infrastructure security, and audit logging. The security measures are designed to meet industry standards and compliance requirements. For advanced configurations, refer to the <a href="https://access.crunchydata.com/documentation/postgres-operator/latest/">Crunchy Data PostgreSQL Operator documentation</a>.</p>

<h3 id="61-access-controls">6.1 Access Controls</h3>
<ul>
  <li><strong>Database Credentials</strong>: Stored in Kubernetes Secrets</li>
  <li><strong>Network Policies</strong>:
    <ul>
      <li>TLS encryption for client connections</li>
      <li>IP whitelisting through pg_hba rules</li>
    </ul>
  </li>
  <li><strong>Audit Logging</strong>: Enabled via pgaudit extension</li>
</ul>

<h3 id="62-infrastructure-security">6.2 Infrastructure Security</h3>
<ul>
  <li><strong>Node Isolation</strong>: Dedicated nodes for database workloads</li>
  <li><strong>Storage Encryption</strong>: Ceph RBD encryption at rest</li>
  <li><strong>Pod Security Policies</strong>: Restricted root access</li>
</ul>

<hr />

<h2 id="7-monitoring--maintenance">7. Monitoring &amp; Maintenance</h2>

<p>This section covers the monitoring and maintenance aspects of the PostgreSQL cluster, including metrics collection, log management, and routine maintenance tasks. The configurations are designed to provide visibility into the cluster’s health, performance, and resource utilization. For advanced configurations, refer to the <a href="https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/day-two/monitoring">Crunchy Data PostgreSQL Operator documentation</a>.</p>

<h3 id="71-metrics-collection">7.1 Metrics Collection</h3>
<ul>
  <li>Key metrics: Query latency, replication lag, connection stats</li>
  <li>Prometheus exporter for metrics scraping</li>
  <li>Grafana dashboard for visualization</li>
</ul>

<h3 id="72-log-management">7.2 Log Management</h3>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="na">postgresql</span><span class="pi">:</span>
  <span class="na">parameters</span><span class="pi">:</span>
    <span class="na">log_min_duration_statement</span><span class="pi">:</span> <span class="m">60000</span>  <span class="c1"># Log slow queries &gt;1min</span>
    <span class="na">log_statement</span><span class="pi">:</span> <span class="s">none</span>                <span class="c1"># Reduce verbose logging</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="73-maintenance-tasks">7.3 Maintenance Tasks</h3>
<ul>
  <li><strong>Vacuum Optimization</strong>: Auto-vacuum settings tuned for OLTP</li>
  <li><strong>Index Management</strong>: pg_stat_statements for query analysis</li>
  <li><strong>Capacity Planning</strong>: Storage auto-scaling configuration</li>
</ul>

<hr />

<h2 id="8-disaster-recovery">8. Disaster Recovery</h2>

<p>This section outlines the disaster recovery procedures for the PostgreSQL cluster, including failover processes, backup recovery, and data restoration. The configurations are designed to minimize downtime, data loss, and service disruptions in the event of a disaster. For advanced configurations, refer to the <a href="https://access.crunchydata.com/documentation/postgres-operator/latest/tutorials/backups-disaster-recovery">Crunchy Data PostgreSQL Operator documentation</a>.</p>

<h3 id="81-failover-process">8.1 Failover Process</h3>
<ol>
  <li>Detect primary failure via Patroni</li>
  <li>Promote standby with latest WAL records</li>
  <li>Redirect client connections to new primary</li>
</ol>

<h3 id="82-from-backup-recovery">8.2 From Backup Recovery</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl-pgo restore <span class="nt">-n</span> db-ns mydatabase <span class="nt">--repoName</span> repo1
</pre></td></tr></tbody></table></code></pre></div></div>
<hr />

<h2 id="9-post-deployment-checklist">9. Post-Deployment Checklist</h2>

<p>After deploying the PostgreSQL cluster, perform the following validation steps to ensure the cluster is operational and meets the defined requirements.</p>

<ol>
  <li>Verify cluster status:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl <span class="nt">-n</span> db-ns get postgresclusters
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>Validate backup completion:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl-pgo show backup <span class="nt">-n</span> db-ns mydatabase
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>
    <p>Configure Prometheus scraping targets</p>
  </li>
  <li>Establish performance baseline metrics</li>
</ol>

<hr />

<h2 id="10-operational-recommendations">10. Operational Recommendations</h2>

<p>To maintain the PostgreSQL cluster’s health and performance, follow these operational recommendations:</p>

<ol>
  <li><strong>Capacity Monitoring</strong>: Set alerts at 75% storage utilization</li>
  <li><strong>Connection Tuning</strong>: Adjust PgBouncer settings based on load patterns</li>
  <li><strong>Backup Validation</strong>: Perform monthly restore drills</li>
  <li><strong>Version Updates</strong>: Follow Crunchy Data’s upgrade path</li>
  <li><strong>Security Audits</strong>: Quarterly penetration testing</li>
</ol>

<p>This unified guide provides complete lifecycle management for your PostgreSQL cluster, from initial deployment through ongoing optimization. All configurations are validated for production workloads and include failsafe mechanisms for critical database operations.</p>]]></content><author><name>Moin Uddin Ahmed</name></author><category term="PostgreSQL" /><category term="backup" /><category term="ceph" /><category term="high-availability" /><category term="kubernetes" /><category term="linux" /><category term="pgbackrest" /><category term="postgresql" /><category term="prometheus" /><summary type="html"><![CDATA[This comprehensive guide provides end-to-end instructions for deploying a resilient PostgreSQL cluster using the Crunchy Data PostgreSQL Operator on Kubernetes. The solution integrates high availability, automated backups, performance optimization, and robust monitoring to ensure enterprise-grade database operations.]]></summary></entry><entry><title type="html">Rook Ceph RBD PVC Troubleshooting Guide for Multi-Attach and MountDevice Failures</title><link href="https://marufmoinuddin.github.io/blog/2025/07/rook-ceph-rbd-pvc-troubleshooting-guide-for-multi-attach-and-mountdevice-failure/" rel="alternate" type="text/html" title="Rook Ceph RBD PVC Troubleshooting Guide for Multi-Attach and MountDevice Failures" /><published>2025-07-08T00:00:00+00:00</published><updated>2025-07-08T00:00:00+00:00</updated><id>https://marufmoinuddin.github.io/blog/2025/07/rook-ceph-rbd-pvc-troubleshooting-guide-for-multi-attach-and-mountdevice-failure</id><content type="html" xml:base="https://marufmoinuddin.github.io/blog/2025/07/rook-ceph-rbd-pvc-troubleshooting-guide-for-multi-attach-and-mountdevice-failure/"><![CDATA[<h1 id="-rook-ceph-rbd-pvc-troubleshooting-guide-for-multi-attach-and-mountdevice-failures">🚑 Rook Ceph RBD PVC Troubleshooting Guide for Multi-Attach and MountDevice Failures</h1>

<p>This guide provides a detailed, step-by-step process to resolve <strong>stuck Rook Ceph RBD volumes</strong> in Kubernetes, particularly when pods encounter errors such as:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Multi-Attach error: Volume is already exclusively attached to one node</code></li>
  <li><code class="language-plaintext highlighter-rouge">rpc error: code = Aborted desc = an operation with the given Volume ID already exists</code></li>
  <li><code class="language-plaintext highlighter-rouge">MountVolume.MountDevice failed: rbd image is still being used</code></li>
</ul>

<p>These issues typically arise due to stale Kubernetes or Ceph state, often caused by node failures, force-deleted pods, or improper node shutdowns. This document combines best practices for identifying and resolving these problems, ensuring minimal disruption to your cluster.</p>

<hr />

<h2 id="-context-what-causes-these-issues">🔍 Context: What Causes These Issues?</h2>

<p>The errors occur when:</p>
<ul>
  <li><strong>Stale <code class="language-plaintext highlighter-rouge">VolumeAttachment</code> objects</strong> reference outdated or unreachable nodes, preventing Kubernetes from attaching the volume to a new node.</li>
  <li><strong>Ceph RBD watchers</strong> remain active from a previous client (e.g., a crashed node or deleted pod), locking the volume.</li>
  <li><strong>Node issues</strong>, such as crashes, ungraceful terminations, or network disruptions, leave behind stale state in Kubernetes or Ceph.</li>
  <li><strong>RBD map errors</strong>, such as <code class="language-plaintext highlighter-rouge">Cannot send after transport endpoint shutdown</code>, indicate communication issues between Ceph and the client.</li>
</ul>

<p>This guide addresses both Kubernetes (<code class="language-plaintext highlighter-rouge">VolumeAttachment</code>) and Ceph (RBD watcher) issues systematically.</p>

<hr />

<h2 id="-resolution-steps">✅ Resolution Steps</h2>

<p>Follow these steps in order to diagnose and resolve the issue. Each step builds on the previous one, ensuring thorough troubleshooting.</p>

<h3 id="-step-1-identify-the-problem-pod-and-pvc">🧩 Step 1: Identify the Problem Pod and PVC</h3>

<p>Start by examining the affected pod to confirm the issue and gather necessary details.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c"># List pods that are stuck in ContainerCreating or Pending</span>
kubectl get pods <span class="nt">-n</span> &lt;namespace&gt; | <span class="nb">grep</span> <span class="nt">-i</span> <span class="nt">-E</span> <span class="s1">'ContainerCreating|Pending'</span>

<span class="c"># Describe the specific stuck pod</span>
kubectl describe pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Look for errors in the output, such as:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">Multi-Attach error for volume "pvc-XXXX": Volume is already exclusively attached</code></li>
  <li><code class="language-plaintext highlighter-rouge">rpc error: code = Aborted desc = an operation with the given Volume ID already exists</code></li>
  <li><code class="language-plaintext highlighter-rouge">MountVolume.MountDevice failed: rbd image is still being used</code></li>
</ul>

<p><strong>What to note</strong>:</p>
<ul>
  <li>The <strong>PVC name</strong> (e.g., <code class="language-plaintext highlighter-rouge">pvc-febdd484-...</code>).</li>
  <li>Any <strong>node names</strong> mentioned in the error (indicating where the volume is incorrectly attached).</li>
  <li>The <strong>PV name</strong> or <strong>volume ID</strong> (e.g., <code class="language-plaintext highlighter-rouge">csi-vol-&lt;UUID&gt;</code>), which may appear in the error logs.</li>
</ul>

<hr />

<h3 id="-step-2-resolve-stale-volumeattachments">🔗 Step 2: Resolve Stale VolumeAttachments</h3>

<p>Stale <code class="language-plaintext highlighter-rouge">VolumeAttachment</code> objects may incorrectly bind the PVC to a node that is no longer relevant (e.g., a crashed or unreachable node). This step provides two approaches, with the <strong>Cordon Method</strong> being the recommended primary approach for StatefulSets.</p>

<h4 id="21-identify-volumeattachments">2.1 Identify VolumeAttachments</h4>

<p>Check for <code class="language-plaintext highlighter-rouge">VolumeAttachment</code> objects associated with the PVC:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get volumeattachments <span class="nt">-A</span> | <span class="nb">grep</span> &lt;pvc-uid&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p>Replace <code class="language-plaintext highlighter-rouge">&lt;pvc-uid&gt;</code> with the PVC UID or name from Step 1 (e.g., <code class="language-plaintext highlighter-rouge">pvc-febdd484-...</code>).</p>
</blockquote>

<p>Example output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>csi-abc123   rook-ceph.rbd.csi.ceph.com   pvc-febdd484-...   &lt;worker-node-1&gt;   true   9m20s
csi-xyz789   rook-ceph.rbd.csi.ceph.com   pvc-de73f086-...   &lt;worker-node-2&gt;   true   41s
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This output shows:</p>
<ul>
  <li><strong>Attachment name</strong>: <code class="language-plaintext highlighter-rouge">csi-abc123</code></li>
  <li><strong>PV name</strong>: <code class="language-plaintext highlighter-rouge">pvc-febdd484-...</code></li>
  <li><strong>Node</strong>: <code class="language-plaintext highlighter-rouge">&lt;worker-node-hostname&gt;</code></li>
  <li><strong>Status</strong>: Whether the attachment is active (<code class="language-plaintext highlighter-rouge">true</code>).</li>
</ul>

<h4 id="22-primary-method-cordon-node-and-scale-down-recommended-for-statefulsets">2.2 Primary Method: Cordon Node and Scale Down (Recommended for StatefulSets)</h4>

<p>This approach ensures the cleanest possible volume detachment by coordinating between Kubernetes scheduling and volume management. <strong>Use this method first, especially for StatefulSets.</strong></p>

<p><strong>Why this method works better:</strong></p>
<ul>
  <li><strong>Graceful shutdown</strong>: Scaling to 0 replicas ensures the StatefulSet controller properly terminates the pod</li>
  <li><strong>Forced rescheduling</strong>: Cordoning the node guarantees the pod will be scheduled on a different node</li>
  <li><strong>Cleaner state management</strong>: The StatefulSet controller handles volume detachment more reliably than force-deleting pods</li>
  <li><strong>Prevents race conditions</strong>: Avoids timing issues between pod deletion and volume detachment</li>
  <li><strong>Respects StatefulSet semantics</strong>: Maintains proper ordinal identity and volume binding relationships</li>
</ul>

<p><strong>Step-by-step process:</strong></p>

<ol>
  <li><strong>Cordon the node</strong> where the VolumeAttachment is currently located:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl cordon &lt;node-name&gt;
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Scale down the StatefulSet</strong> to 0 replicas:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl scale sts <span class="nt">-n</span> &lt;namespace&gt; <span class="nt">--replicas</span><span class="o">=</span>0 &lt;statefulset-name&gt;
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Delete the VolumeAttachment</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl delete volumeattachment &lt;volumeattachment-name&gt;
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Scale the StatefulSet back up</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl scale sts <span class="nt">-n</span> &lt;namespace&gt; <span class="nt">--replicas</span><span class="o">=</span>&lt;original-replica-count&gt; &lt;statefulset-name&gt;
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Uncordon the node</strong> (optional, if you want to allow scheduling back to it):
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl uncordon &lt;node-name&gt;
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<h4 id="23-alternative-method-direct-volumeattachment-deletion">2.3 Alternative Method: Direct VolumeAttachment Deletion</h4>

<p><strong>Use this method for Deployments or when the cordon method is not suitable.</strong></p>

<p>If the PVC is attached to an outdated, unreachable, or incorrect node (i.e., not the node where the pod is scheduled), delete the <code class="language-plaintext highlighter-rouge">VolumeAttachment</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl delete volumeattachment &lt;volumeattachment-name&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<p>For example:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl delete volumeattachment csi-abc123
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>CAUTION</strong>: Only delete a <code class="language-plaintext highlighter-rouge">VolumeAttachment</code> if:</p>
  <ul>
    <li>The node listed is down, unresponsive, or no longer hosting the pod.</li>
    <li>The pod using the volume has been deleted or rescheduled.</li>
    <li>You’ve confirmed the volume is not actively used by another pod.</li>
  </ul>
</blockquote>

<h4 id="24-restart-the-pod-for-alternative-method">2.4 Restart the Pod (For Alternative Method)</h4>

<p>After deleting the stale <code class="language-plaintext highlighter-rouge">VolumeAttachment</code>, restart the pod to trigger a reschedule and re-attach the volume:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c"># For a single pod</span>
kubectl delete pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;

<span class="c"># For a deployment</span>
kubectl rollout restart deployment <span class="nt">-n</span> &lt;namespace&gt; &lt;deployment-name&gt;

<span class="c"># For a StatefulSet (if not using the cordon method above)</span>
kubectl delete pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h3 id="-step-3-verify-the-pod-status">📋 Step 3: Verify the Pod Status</h3>

<p>Check if the pod is now running correctly after rescheduling:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>kubectl get pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;
kubectl describe pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li><strong>✅ Success</strong>: If the pod is in the <code class="language-plaintext highlighter-rouge">Running</code> state and no errors appear in the description, the issue is resolved.</li>
  <li><strong>❌ Failure</strong>: If the pod remains in <code class="language-plaintext highlighter-rouge">ContainerCreating</code> or <code class="language-plaintext highlighter-rouge">Pending</code>, or errors like <code class="language-plaintext highlighter-rouge">Multi-Attach</code> or <code class="language-plaintext highlighter-rouge">rbd image is still being used</code> persist, proceed to Step 4.</li>
</ul>

<hr />

<p>Here’s the concise, direct-command version without variables:</p>

<hr />

<h3 id="-step-4-blacklist-stale-rbd-watchers">🧩 <strong>Step 4: Blacklist Stale RBD Watchers</strong></h3>

<h4 id="41-get-the-rbd-image-name"><strong>4.1 Get the RBD Image Name</strong></h4>
<ol>
  <li><strong>Find the PV name from PVC</strong>:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get pvc <span class="nt">-n</span> &lt;namespace&gt; &lt;pvc-name&gt; <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.spec.volumeName}'</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li><strong>Extract the RBD image ID</strong> (replace <code class="language-plaintext highlighter-rouge">&lt;pv-name&gt;</code> with output from above):
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl get pv &lt;pv-name&gt; <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.spec.csi.volumeHandle}'</span> | <span class="nb">cut</span> <span class="nt">-d-</span> <span class="nt">-f6-</span> | xargs <span class="nt">-I</span> <span class="o">{}</span> <span class="nb">echo</span> <span class="s2">"csi-vol-{}"</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<h4 id="42-check-active-watchers"><strong>4.2 Check Active Watchers</strong></h4>
<p>Run this in <strong>one line</strong> (replace <code class="language-plaintext highlighter-rouge">&lt;pv-name&gt;</code>):</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl <span class="nt">-n</span> rook-ceph <span class="nb">exec</span> <span class="nt">-it</span> <span class="si">$(</span>kubectl get pod <span class="nt">-n</span> rook-ceph <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>rook-ceph-tools <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.items[0].metadata.name}'</span><span class="si">)</span> <span class="nt">--</span> rbd status replicapool/<span class="si">$(</span>kubectl get pv &lt;pv-name&gt; <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.spec.csi.volumeHandle}'</span> | <span class="nb">cut</span> <span class="nt">-d-</span> <span class="nt">-f6-</span> | xargs <span class="nt">-I</span> <span class="o">{}</span> <span class="nb">echo</span> <span class="s2">"csi-vol-{}"</span><span class="si">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><em>Expected Output</em>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>Watchers:
    watcher=&lt;REDACTED_IP&gt;:&lt;REDACTED_PORT&gt;/&lt;REDACTED_ID&gt; client.&lt;REDACTED_CLIENT_ID&gt; cookie=&lt;REDACTED_COOKIE&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="43-blacklist-the-stale-watcher"><strong>4.3 Blacklist the Stale Watcher</strong></h4>
<p>Copy the <code class="language-plaintext highlighter-rouge">watcher=IP:PORT/ID</code> from above and run:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl <span class="nt">-n</span> rook-ceph <span class="nb">exec</span> <span class="nt">-it</span> <span class="si">$(</span>kubectl get pod <span class="nt">-n</span> rook-ceph <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>rook-ceph-tools <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.items[0].metadata.name}'</span><span class="si">)</span> <span class="nt">--</span> ceph osd blacklist add &lt;REDACTED_IP&gt;:&lt;REDACTED_PORT&gt;/&lt;REDACTED_ID&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="44-force-pod-restart"><strong>4.4 Force Pod Restart</strong></h4>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>kubectl delete pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<p><strong>Key Notes</strong>:</p>
<ol>
  <li>Replace <code class="language-plaintext highlighter-rouge">&lt;namespace&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;pvc-name&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;pv-name&gt;</code>, and <code class="language-plaintext highlighter-rouge">&lt;pod-name&gt;</code> with your actual values</li>
  <li>For <strong>fish shell</strong>, replace <code class="language-plaintext highlighter-rouge">$(...)</code> with <code class="language-plaintext highlighter-rouge">(...)</code></li>
  <li>The <code class="language-plaintext highlighter-rouge">replicapool</code> name should match your Ceph pool (check StorageClass if unsure)</li>
</ol>

<hr />

<h3 id="-step-5-final-verification">🔎 Step 5: Final Verification</h3>

<p>Verify that the pod is now running correctly:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre>kubectl get pods <span class="nt">-n</span> &lt;namespace&gt;
kubectl describe pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The pod should now be in the <code class="language-plaintext highlighter-rouge">Running</code> state with no errors. If issues persist, consider:</p>
<ul>
  <li>Checking for underlying network issues between nodes and the Ceph cluster.</li>
  <li>Ensuring the Rook Ceph CSI driver and Ceph cluster are running the latest stable versions.</li>
  <li>Reviewing node health and resource availability.</li>
</ul>

<hr />

<h2 id="-summary-flowchart">🧠 Summary Flowchart</h2>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>1. Identify stuck pod → Run `kubectl describe pod` → Note Multi-Attach or MountDevice errors
2. Check VolumeAttachments → Run `kubectl get volumeattachments` → Delete stale attachments
3. Restart pod → Verify with `kubectl describe pod`
4. If still stuck → Find RBD image ID → Check watchers with `rbd status` → Blacklist stale watchers
5. Restart pod → Confirm resolution with `kubectl get pods`
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="-technical-background">🛠 Technical Background</h2>

<h3 id="why-do-these-issues-occur">Why Do These Issues Occur?</h3>

<p>When a pod uses a Rook Ceph RBD-backed PVC:</p>
<ol>
  <li><strong>Kubernetes</strong> creates a <code class="language-plaintext highlighter-rouge">VolumeAttachment</code> object to track which node the volume is mounted on.</li>
  <li><strong>Ceph RBD</strong> registers a “watcher” to monitor clients accessing the volume.</li>
</ol>

<p>If a node crashes, a pod is force-deleted, or a node is improperly drained:</p>
<ul>
  <li>The <code class="language-plaintext highlighter-rouge">VolumeAttachment</code> may remain, incorrectly indicating the volume is still attached to the old node.</li>
  <li>Ceph may retain a watcher for a client that no longer exists, locking the volume.</li>
</ul>

<p>This guide resolves both issues by:</p>
<ul>
  <li>Removing stale <code class="language-plaintext highlighter-rouge">VolumeAttachment</code> objects in Kubernetes.</li>
  <li>Blacklisting stale watchers in Ceph to release the volume.</li>
</ul>

<hr />

<h2 id="️-tips-to-prevent-these-issues">🛡️ Tips to Prevent These Issues</h2>

<ol>
  <li><strong>Avoid Force Deletions</strong>: Allow pods to terminate gracefully to ensure proper cleanup of <code class="language-plaintext highlighter-rouge">VolumeAttachment</code> objects and Ceph watchers.</li>
  <li><strong>Properly Drain Nodes</strong>: Use <code class="language-plaintext highlighter-rouge">kubectl drain &lt;node-name&gt; --ignore-daemonsets</code> before shutting down or rebooting nodes to safely evict pods.</li>
  <li><strong>Keep Rook Ceph Updated</strong>: Newer versions of Rook and the Ceph CSI driver include improved handling of volume attachments and watcher cleanup.</li>
  <li><strong>Monitor Node Health</strong>: Use monitoring tools to detect and address node failures or network issues promptly.</li>
  <li><strong>Enable Ceph Health Checks</strong>: Configure Rook to monitor Ceph cluster health and alert on issues like OSD failures or network disruptions.</li>
</ol>

<hr />

<h2 id="-quick-reference-commands">📜 Quick Reference Commands</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
</pre></td><td class="rouge-code"><pre><span class="c"># Step 1: Identify the problem</span>
kubectl get pods <span class="nt">-n</span> &lt;namespace&gt; | <span class="nb">grep</span> <span class="nt">-i</span> <span class="nt">-E</span> <span class="s1">'ContainerCreating|Pending'</span>
kubectl describe pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;

<span class="c"># Step 2: Check and delete stale VolumeAttachments</span>
kubectl get volumeattachments <span class="nt">-A</span> | <span class="nb">grep</span> &lt;pvc-uid&gt;
kubectl delete volumeattachment &lt;volumeattachment-name&gt;

<span class="c"># Step 3: Verify pod status</span>
kubectl get pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;
kubectl describe pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;

<span class="c"># Step 4: Handle stale RBD watchers</span>
<span class="c"># Find RBD image ID</span>
kubectl get pvc <span class="nt">-n</span> &lt;namespace&gt; &lt;pvc-name&gt; <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.spec.volumeName}'</span>
kubectl get pv &lt;pv-name&gt; <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.spec.csi.volumeHandle}'</span>

<span class="c"># Check for watchers</span>
kubectl <span class="nt">-n</span> rook-ceph <span class="nb">exec</span> <span class="nt">-it</span> <span class="si">$(</span>kubectl get pod <span class="nt">-n</span> rook-ceph <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>rook-ceph-tools <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.items[0].metadata.name}'</span><span class="si">)</span> <span class="nt">--</span> <span class="se">\</span>
  rbd status replicapool/&lt;rbd-image-id&gt;

<span class="c"># Blacklist watcher</span>
kubectl <span class="nt">-n</span> rook-ceph <span class="nb">exec</span> <span class="nt">-it</span> <span class="si">$(</span>kubectl get pod <span class="nt">-n</span> rook-ceph <span class="nt">-l</span> <span class="nv">app</span><span class="o">=</span>rook-ceph-tools <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.items[0].metadata.name}'</span><span class="si">)</span> <span class="nt">--</span> <span class="se">\</span>
  ceph osd blacklist add &lt;watcher-address&gt;

<span class="c"># Step 5: Restart pod</span>
kubectl delete pod <span class="nt">-n</span> &lt;namespace&gt; &lt;pod-name&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="-notes-and-precautions">✅ Notes and Precautions</h2>

<ul>
  <li><strong>Avoid Hasty Deletions</strong>: Do not delete <code class="language-plaintext highlighter-rouge">VolumeAttachment</code> objects unless you’re certain the volume is no longer needed by the referenced node or pod.</li>
  <li><strong>Blacklist Carefully</strong>: Only blacklist watchers after confirming the client is stale (e.g., the pod is deleted, or the node is down).</li>
  <li><strong>Minimize Node Reboots</strong>: The steps above resolve most issues without requiring node reboots, which can be disruptive.</li>
  <li><strong>Check Cluster Health</strong>: If issues persist, verify the health of the Ceph cluster (<code class="language-plaintext highlighter-rouge">kubectl -n rook-ceph get pods</code>) and ensure no OSDs are down.</li>
  <li><strong>Log Errors</strong>: Save error outputs and logs for debugging if the issue recurs or requires escalation.</li>
</ul>

<hr />

<h2 id="-additional-resources">📄 Additional Resources</h2>

<ul>
  <li><strong>Rook Ceph Documentation</strong>: <a href="https://rook.io/docs/rook/latest/">https://rook.io/docs/rook/latest/</a></li>
  <li><strong>Ceph CSI Troubleshooting</strong>: <a href="https://github.com/ceph/ceph-csi">https://github.com/ceph/ceph-csi</a></li>
  <li><strong>Kubernetes Storage SIG</strong>: For advanced debugging, engage with the Kubernetes Storage SIG community.</li>
</ul>

<hr />

<p>Let me know if you’d like this guide in a different format (e.g., Markdown, PDF) or if you want assistance turning it into an automated script (e.g., Ansible role or bash script) for proactive monitoring and healing.</p>]]></content><author><name>Moin Uddin Ahmed</name></author><category term="Kubernetes" /><category term="ceph" /><category term="kubernetes" /><category term="pvc" /><category term="rbd" /><category term="rook" /><summary type="html"><![CDATA[This guide provides a detailed, step-by-step process to resolve stuck Rook Ceph RBD volumes in Kubernetes, particularly when pods encounter errors such as:]]></summary></entry><entry><title type="html">Setting Up Oracle 19c Using Docker on Ubuntu 22.04</title><link href="https://marufmoinuddin.github.io/blog/2025/07/setting-up-oracle-19c-using-docker-on-ubuntu-2204/" rel="alternate" type="text/html" title="Setting Up Oracle 19c Using Docker on Ubuntu 22.04" /><published>2025-07-08T00:00:00+00:00</published><updated>2025-07-08T00:00:00+00:00</updated><id>https://marufmoinuddin.github.io/blog/2025/07/setting-up-oracle-19c-using-docker-on-ubuntu-2204</id><content type="html" xml:base="https://marufmoinuddin.github.io/blog/2025/07/setting-up-oracle-19c-using-docker-on-ubuntu-2204/"><![CDATA[<h1 id="documentation-setting-up-oracle-19c-using-docker-on-ubuntu-2204">Documentation: Setting Up Oracle 19c Using Docker on Ubuntu 22.04</h1>

<p>This documentation provides a detailed step-by-step guide to help you set up Oracle 19c using Docker on an Ubuntu 22.04 system.Whether you are a beginner or an experienced user, this document ensures you can set up and manage the Oracle 19c database in a Docker container without difficulty.</p>

<hr />

<h2 id="table-of-contents">Table of Contents</h2>

<ol>
  <li><a href="#setting-up-oracle-19c-using-docker">Setting Up Oracle 19c Using Docker</a>
    <ul>
      <li><a href="#step-1-install-docker">Step 1: Install Docker</a></li>
      <li><a href="#step-2-pull-the-oracle-19c-docker-image">Step 2: Pull the Oracle 19c Docker Image</a></li>
      <li><a href="#step-3-run-the-oracle-19c-container">Step 3: Run the Oracle 19c Container</a></li>
    </ul>
  </li>
  <li><a href="#access-the-oracle-database-with-sqlplus">Access the Oracle Database with SQL*Plus</a>
    <ul>
      <li><a href="#remote-connection-from-outside-the-container">Remote Connection (from outside the container)</a></li>
      <li><a href="#local-connection-from-inside-the-container">Local Connection (from inside the container)</a></li>
    </ul>
  </li>
  <li><a href="#setting-oracle-environment-variables">Setting Oracle Environment Variables</a>
    <ul>
      <li><a href="#check-oracle-instance-status">Check Oracle Instance Status</a></li>
      <li><a href="#starting-the-oracle-instance">Starting the Oracle Instance</a></li>
    </ul>
  </li>
  <li><a href="#create-a-new-database-user-and-grant-permissions">Create a New Database User and Grant Permissions</a>
    <ul>
      <li><a href="#step-1-set-oracle-script-parameter">Step 1: Set Oracle Script Parameter</a></li>
      <li><a href="#step-2-create-a-new-user">Step 2: Create a New User</a></li>
      <li><a href="#step-3-grant-privileges-to-the-user">Step 3: Grant Privileges to the User</a></li>
      <li><a href="#step-4-set-storage-quota-for-the-user">Step 4: Set Storage Quota for the User</a></li>
    </ul>
  </li>
  <li><a href="#access-the-oracle-database-with-sqlplus-using-containers-ip-address">Access the Oracle Database with SQL*Plus (Using Container’s IP Address)</a>
    <ul>
      <li><a href="#step-1-get-the-containers-ip-address">Step 1: Get the Container’s IP Address</a></li>
      <li><a href="#step-2-remote-connection-using-the-containers-ip-address">Step 2: Remote Connection Using the Container’s IP Address</a></li>
      <li><a href="#bonus-installing-oracle-sql-developer-on-debianubuntu">Bonus: Installing Oracle SQL Developer on Debian/Ubuntu</a></li>
    </ul>
  </li>
  <li><a href="#connect-to-oracle-database-using-oracle-sql-developer-using-containers-ip-address">Connect to Oracle Database Using Oracle SQL Developer (Using Container’s IP Address)</a>
    <ul>
      <li><a href="#step-1-create-a-new-database-connection">Step 1: Create a New Database Connection</a></li>
      <li><a href="#step-2-test-the-connection">Step 2: Test the Connection</a></li>
    </ul>
  </li>
  <li><a href="#additional-note-local-connection-inside-the-container">Additional Note: Local Connection Inside the Container</a></li>
  <li><a href="#conclusion">Conclusion</a></li>
</ol>

<hr />

<h2 id="1-setting-up-oracle-19c-using-docker">1. Setting Up Oracle 19c Using Docker</h2>

<p>Oracle 19c is a powerful database that can be easily deployed using Docker on Ubuntu 22.04. Docker offers a simple and efficient way to run Oracle databases in isolated environments, ensuring faster deployments and minimal configuration. The following steps will guide you through the process of installing Docker, pulling the Oracle 19c image, and running the Oracle container.</p>

<h3 id="step-1-install-docker">Step 1: Install Docker</h3>

<p>Docker is an essential tool for running containerized applications, and it is required to deploy Oracle 19c in a containerized environment. To install Docker, follow these steps:</p>

<ol>
  <li>
    <p><strong>Update the System Package Index:</strong>
Before installing Docker, ensure that your system’s package list is up-to-date by running the following command:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt update
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>
    <p><strong>Install Docker:</strong>
Next, install Docker on your system by running the following command. The <code class="language-plaintext highlighter-rouge">-y</code> flag will automatically approve any prompts during installation:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt <span class="nb">install </span>docker.io <span class="nt">-y</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>
    <p><strong>Enable Docker Service:</strong>
After Docker is installed, enable the Docker service so that it automatically starts when the system boots:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl <span class="nb">enable </span>docker
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>
    <p><strong>Start Docker Service:</strong>
Start Docker manually using this command:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>systemctl start docker
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>
    <p><strong>Verify Docker Installation:</strong>
To confirm that Docker is installed and running correctly, use the following command to check the Docker version:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker <span class="nt">--version</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>

    <p>This will output the version of Docker installed on your system, confirming that the installation was successful.</p>
  </li>
</ol>

<hr />

<h3 id="step-2-pull-the-oracle-19c-docker-image">Step 2: Pull the Oracle 19c Docker Image</h3>

<p>To run Oracle 19c in Docker, you need to pull an Oracle 19c image from a Docker registry. Oracle offers both community-maintained and official images for Oracle 19c.</p>

<ol>
  <li>
    <p><strong>Pull the Community-Maintained Oracle 19c Image:</strong>
For a simpler setup, you can use the community-maintained Oracle 19c image. Run the following command to download it:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker pull doctorkirk/oracle-19c
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>
    <p><strong>Pull the Official Oracle 19c Image:</strong>
For production environments, Oracle recommends using the official Oracle image. You will need to log into Oracle’s container registry to access it:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker pull container-registry.oracle.com/database/enterprise:19.3.0.0
</pre></td></tr></tbody></table></code></pre></div>    </div>

    <p>To pull the official Oracle image, you will need an Oracle account. If you don’t have one, visit Oracle’s website to create an account.</p>
  </li>
</ol>

<hr />

<h3 id="step-3-run-the-oracle-19c-container">Step 3: Run the Oracle 19c Container</h3>

<p>After pulling the Oracle image, you can run it in a Docker container. The following command starts the Oracle 19c container and exposes the default Oracle database ports (1521) and the Enterprise Manager port (5500):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker run <span class="nt">--name</span> oracle_db <span class="nt">-p</span> 1521:1521 <span class="nt">-p</span> 5500:5500 doctorkirk/oracle-19c
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This command starts the Oracle 19c container, which can be accessed remotely through port 1521 (for the database) and port 5500 (for Enterprise Manager).</p>

<ol>
  <li>
    <p><strong>Check the Status of the Running Container:</strong>
After starting the container, check that it is running correctly by listing all Docker containers:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker ps <span class="nt">-a</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>

    <p>You should see the <code class="language-plaintext highlighter-rouge">oracle_db</code> container in the list with a status of “Up.”</p>
  </li>
</ol>

<hr />

<h2 id="2-access-the-oracle-database-with-sqlplus">2. Access the Oracle Database with SQL*Plus</h2>

<p>SQL*Plus is Oracle’s command-line interface for interacting with Oracle databases. There are two main ways to access Oracle running inside a Docker container: remotely from your Ubuntu host machine or locally from within the container.</p>

<h3 id="remote-connection-from-outside-the-container">Remote Connection (from outside the container)</h3>

<p>To connect to Oracle 19c from outside the container (e.g., from your host machine), you can use SQL*Plus with the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker <span class="nb">exec</span> <span class="nt">-it</span> oracle-db sqlplus sys/&lt;REDACTED_PASSWORD&gt;@ORCLCDB as sysdba
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Here:</p>
<ul>
  <li>Replace <code class="language-plaintext highlighter-rouge">&lt;REDACTED_PASSWORD&gt;</code> with the <REDACTED_PASSWORD> you set for the `sys` user when creating the container.</REDACTED_PASSWORD></li>
  <li><code class="language-plaintext highlighter-rouge">ORCLCDB</code> is the default service name for the Oracle database in the Docker container.</li>
</ul>

<h3 id="local-connection-from-inside-the-container">Local Connection (from inside the container)</h3>

<p>If you are inside the Docker container and want to connect to Oracle locally (without needing an IP address), you can use OS-level authentication. First, enter the container:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker <span class="nb">exec</span> <span class="nt">-it</span> oracle_db bash
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Then, use the following command to connect as <code class="language-plaintext highlighter-rouge">sysdba</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>sqlplus / as sysdba
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This method leverages Oracle’s OS authentication mechanism, allowing you to connect to the database without needing a <REDACTED_PASSWORD>.</REDACTED_PASSWORD></p>

<hr />

<h2 id="3-setting-oracle-environment-variables">3. Setting Oracle Environment Variables</h2>

<p>Oracle databases require certain environment variables to be set in order to function properly. These variables are typically set in the container when it is started, but if you encounter any issues, you can manually configure them.</p>

<h3 id="setting-oracle-environment-variables">Setting Oracle Environment Variables</h3>

<p>Set the following Oracle-specific environment variables to ensure smooth operation:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c"># Set the Oracle System Identifier (SID)</span>
<span class="nb">export </span><span class="nv">ORACLE_SID</span><span class="o">=</span>ORCLCDB

<span class="c"># Specify the Oracle home directory</span>
<span class="nb">export </span><span class="nv">ORACLE_HOME</span><span class="o">=</span>/opt/oracle/product/19c/dbhome_1

<span class="c"># Update the PATH variable to include Oracle binaries</span>
<span class="nb">export </span><span class="nv">PATH</span><span class="o">=</span><span class="nv">$ORACLE_HOME</span>/bin:<span class="nv">$PATH</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>These variables are crucial for Oracle to locate necessary binaries and configuration files. To make sure they are set every time you log into the container, add them to the <code class="language-plaintext highlighter-rouge">~/.bashrc</code> file.</p>

<hr />

<h3 id="check-oracle-instance-status">Check Oracle Instance Status</h3>

<p>To verify that your Oracle database is running, use the following query in SQL*Plus:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="n">status</span> <span class="k">FROM</span> <span class="n">v</span><span class="err">$</span><span class="n">instance</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This query will return the status of the Oracle instance. If everything is running smoothly, the status will be <code class="language-plaintext highlighter-rouge">STARTED</code>.</p>

<h3 id="starting-the-oracle-instance">Starting the Oracle Instance</h3>

<p>If the Oracle instance is not running, you can start it by running:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">STARTUP</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>If the instance is already running, this command will display a message indicating that the instance is up.</p>

<p>To exit SQL*Plus, simply type:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="n">EXIT</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<hr />

<h2 id="4-create-a-new-database-user-and-grant-permissions">4. Create a New Database User and Grant Permissions</h2>

<p>For creating a new user and granting them permissions in Oracle, follow these detailed steps.</p>

<h3 id="step-1-set-oracle-script-parameter">Step 1: Set Oracle Script Parameter</h3>

<p>If you’re encountering errors like <code class="language-plaintext highlighter-rouge">ORA-65096: invalid common user or role name</code>, you need to enable the <code class="language-plaintext highlighter-rouge">_ORACLE_SCRIPT</code> parameter. This will allow you to create users in a non-CDB environment.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">ALTER</span> <span class="k">SESSION</span> <span class="k">SET</span> <span class="nv">"_ORACLE_SCRIPT"</span><span class="o">=</span><span class="k">true</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="step-2-create-a-new-user">Step 2: Create a New User</h3>

<p>To create a new user, use the following command:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">CREATE</span> <span class="k">USER</span> <span class="k">admin</span> <span class="n">IDENTIFIED</span> <span class="k">BY</span> <span class="o">&lt;</span><span class="n">REDACTED_PASSWORD</span><span class="o">&gt;</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Replace <code class="language-plaintext highlighter-rouge">admin</code> with the desired username and <code class="language-plaintext highlighter-rouge">&lt;REDACTED_PASSWORD&gt;</code> with the user’s <REDACTED_PASSWORD>.</REDACTED_PASSWORD></p>

<h3 id="step-3-grant-privileges-to-the-user">Step 3: Grant Privileges to the User</h3>

<p>Now, grant the necessary privileges to the newly created user. The following commands will grant the user the ability to create sessions, tables, triggers, sequences, and procedures:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">GRANT</span> <span class="k">CREATE</span> <span class="k">SESSION</span> <span class="k">TO</span> <span class="k">admin</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">CREATE</span> <span class="k">TABLE</span> <span class="k">TO</span> <span class="k">admin</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">CREATE</span> <span class="k">TRIGGER</span> <span class="k">TO</span> <span class="k">admin</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">CREATE</span> <span class="n">SEQUENCE</span> <span class="k">TO</span> <span class="k">admin</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">CREATE</span> <span class="k">PROCEDURE</span> <span class="k">TO</span> <span class="k">admin</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="step-4-set-storage-quota-for-the-user">Step 4: Set Storage Quota for the User</h3>

<p>To manage the space the user can consume in the Oracle database, set a storage quota. For instance, to allocate a 100MB quota to the <code class="language-plaintext highlighter-rouge">admin</code> user, use the following command:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="k">ALTER</span> <span class="k">USER</span> <span class="k">admin</span> <span class="n">QUOTA</span> <span class="mi">100</span><span class="n">M</span> <span class="k">ON</span> <span class="n">USERS</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This will limit the amount of space the <code class="language-plaintext highlighter-rouge">admin</code> user can consume on the <code class="language-plaintext highlighter-rouge">USERS</code> tablespace.</p>

<hr />

<h2 id="5-access-the-oracle-database-with-sqlplus-using-containers-ip-address">5. Access the Oracle Database with SQL*Plus (Using Container’s IP Address)</h2>

<p>You can also access the Oracle database remotely by connecting to the container’s IP address. This method is useful when you need to connect from outside the container, for example, from another machine or a different Docker container.</p>

<h3 id="step-1-get-the-containers-ip-address">Step 1: Get the Container’s IP Address</h3>

<p>To get the IP address of the Oracle container, run the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker inspect oracle_db | <span class="nb">grep</span> <span class="s2">"IPAddress"</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This will return an IP address similar to:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nl">"IPAddress"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&lt;REDACTED_IP&gt;"</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="step-2-remote-connection-using-the-containers-ip-address">Step 2: Remote Connection Using the Container’s IP Address</h3>

<p>Once you have the container’s IP address, use SQL*Plus to connect to the database using the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>sqlplus sys/&lt;REDACTED_PASSWORD&gt;@&lt;REDACTED_IP&gt;:1521/ORCLCDB as sysdba
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Make sure to replace <code class="language-plaintext highlighter-rouge">&lt;REDACTED_PASSWORD&gt;</code> with the <REDACTED_PASSWORD> for the `sys` user and the IP address with the one retrieved in the previous step.</REDACTED_PASSWORD></p>

<hr />

<h2 id="bonus-installing-oracle-sql-developer-on-debianubuntu">Bonus: Installing Oracle SQL Developer on Debian/Ubuntu</h2>

<p>Oracle SQL Developer is a free graphical tool for database development that simplifies database management tasks. This guide will help you install SQL Developer on Debian or Ubuntu Linux systems.</p>

<h2 id="prerequisites">Prerequisites</h2>

<ul>
  <li>Debian or Ubuntu Linux distribution</li>
  <li>Administrative (sudo) privileges</li>
  <li>Internet connection to download required packages</li>
</ul>

<h2 id="installation-steps">Installation Steps</h2>

<h3 id="1-install-required-dependencies">1. Install Required Dependencies</h3>

<p>First, install the necessary dependencies:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>apt <span class="nb">install </span>alien openjdk-17-jdk
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>Note for Debian users</strong>: This may require alien &gt;= 8.95.5. If you’re on Debian and encounter errors, you may need to update the alien package:</p>

  <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre>apt remove <span class="nt">--purge</span> alien
wget <span class="nt">--quiet</span> <span class="nt">-O</span> /tmp/alien.deb http://ftp.de.debian.org/debian/pool/main/a/alien/alien_8.95.6_all.deb
dpkg <span class="nt">-i</span> /tmp/alien.deb
</pre></td></tr></tbody></table></code></pre></div>  </div>
</blockquote>

<h3 id="2-download-oracle-sql-developer">2. Download Oracle SQL Developer</h3>

<p>Download the Linux RPM package from the Oracle website:</p>
<ul>
  <li>Visit <a href="https://www.oracle.com/tools/downloads/sqldev-downloads.html">Oracle SQL Developer Downloads</a></li>
  <li>Select the Linux RPM package (version 19.2 or later recommended)</li>
  <li>You’ll need an Oracle account to download the software</li>
</ul>

<blockquote>
  <p>Note: You may need to install jdk according to the version of SQL Developer you are installing. Just make sure you have the correct version of jdk installed. For example, if you need jdk 17, you can install it using <code class="language-plaintext highlighter-rouge">sudo apt install openjdk-17-jdk</code>.</p>
</blockquote>

<h3 id="3-install-sql-developer">3. Install SQL Developer</h3>

<p>Convert and install the RPM package using alien:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="nb">sudo </span>alien <span class="nt">-i</span> sqldeveloper-<span class="k">*</span>.rpm
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote>
  <p><strong>Note</strong>: This process may take several minutes to complete.</p>
</blockquote>

<p>If the above method fails on Debian with a “dh_usrlocal” error, try this alternative approach:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c"># Extract the RPM without installing</span>
rpm2cpio sqldeveloper-<span class="k">*</span>.rpm | cpio <span class="nt">-idmv</span>
<span class="c"># Then run SQL Developer directly using</span>
opt/sqldeveloper/sqldeveloper.sh
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="4-create-a-desktop-entry-optional">4. Create a Desktop Entry (Optional)</h3>

<p>Create a desktop launcher for easier access:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="nb">echo</span> <span class="s2">"[Desktop Entry]
Type=Application
Name=Oracle SQL Developer
Exec=sqldeveloper
Icon=/opt/sqldeveloper/icon.png
Terminal=false"</span> <span class="o">&gt;&gt;</span> ~/.local/share/applications/sqldeveloper.desktop
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="5-configure-sql-developer">5. Configure SQL Developer</h3>

<h4 id="disable-the-welcome-page-optional">Disable the Welcome Page (Optional)</h4>
<ul>
  <li>If the welcome page appears when you start SQL Developer, scroll to the bottom and uncheck “Show on startup”</li>
  <li>Alternatively, you can disable it through preferences</li>
</ul>

<h4 id="disable-unnecessary-features-optional">Disable Unnecessary Features (Optional)</h4>
<ol>
  <li>Open SQL Developer</li>
  <li>Go to Tools &gt; Features</li>
  <li>Uncheck features you don’t need</li>
  <li>For minimal non-DBA development, keep only:
    <ul>
      <li>Oracle SQL Developer - Schema Browser</li>
      <li>Oracle SQL Developer - Snippet</li>
      <li>Oracle SQL Developer - SSH Support</li>
      <li>Oracle SQL Developer - XML Schema</li>
    </ul>
  </li>
  <li>Click “Apply Changes”</li>
</ol>

<h2 id="troubleshooting">Troubleshooting</h2>

<p>If you encounter issues with installation:</p>

<ul>
  <li>For Debian users experiencing “dh_usrlocal” errors, make sure you have alien version 8.95.5 or higher</li>
  <li>If conversion fails, try the direct extraction method mentioned above</li>
  <li>Verify that you have OpenJDK 11 installed correctly</li>
  <li>Check that the downloaded RPM file is not corrupted</li>
</ul>

<h2 id="starting-sql-developer">Starting SQL Developer</h2>

<p>After installation, you can start SQL Developer by:</p>
<ul>
  <li>Using the desktop launcher if you created one</li>
  <li>Running <code class="language-plaintext highlighter-rouge">sqldeveloper</code> in a terminal</li>
  <li>Or executing <code class="language-plaintext highlighter-rouge">/opt/sqldeveloper/sqldeveloper.sh</code> directly</li>
</ul>

<hr />

<h2 id="6-connect-to-oracle-database-using-oracle-sql-developer-using-containers-ip-address">6. Connect to Oracle Database Using Oracle SQL Developer (Using Container’s IP Address)</h2>

<p>Oracle SQL Developer is a powerful graphical tool for managing Oracle databases. To connect to Oracle 19c running in a Docker container, you can follow these steps.</p>

<h3 id="step-1-create-a-new-database-connection">Step 1: Create a New Database Connection</h3>

<ol>
  <li>Open Oracle SQL Developer.</li>
  <li>Click on the <strong>Connections</strong> tab.</li>
  <li>Click on the <strong>+</strong> icon to create a new connection.</li>
  <li>Enter the connection details:
    <ul>
      <li><strong>Connection Name</strong>: Any name you prefer.</li>
      <li><strong>Username</strong>: <code class="language-plaintext highlighter-rouge">sys</code></li>
      <li><strong>Password</strong>: The <REDACTED_PASSWORD> for the `sys` user.</REDACTED_PASSWORD></li>
      <li><strong>Host</strong>: The IP address of the Oracle container (e.g., <code class="language-plaintext highlighter-rouge">&lt;REDACTED_IP&gt;</code>).</li>
      <li><strong>Port</strong>: <code class="language-plaintext highlighter-rouge">1521</code> (default Oracle port).</li>
      <li><strong>SID</strong>: <code class="language-plaintext highlighter-rouge">ORCLCDB</code> (default SID).</li>
    </ul>
  </li>
  <li>Click <strong>Test</strong> to verify the connection, then click <strong>Save</strong>.</li>
</ol>

<h3 id="step-2-test-the-connection">Step 2: Test the Connection</h3>

<p>Click <strong>Connect</strong> to initiate the connection. If all the details are correct, Oracle SQL Developer should successfully connect to your Oracle 19c instance running in Docker.</p>

<hr />

<h2 id="7-additional-note-local-connection-inside-the-container">7. Additional Note: Local Connection Inside the Container</h2>

<p>If you’re working within the container itself and want to connect to the Oracle database locally, you can skip the network configuration. Use the following SQL*Plus command to connect to the database:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>sqlplus / as sysdba
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This method uses the local Oracle user to authenticate the connection, eliminating the need for a <REDACTED_PASSWORD>.</REDACTED_PASSWORD></p>

<h2 id="conclusion">Conclusion</h2>

<p>By following this comprehensive guide, you should now have Oracle 19c up and running in a Docker container on your Ubuntu 22.04 system. You have learned how to install Docker, pull the Oracle 19c image, run the container, access the database using SQL*Plus, set environment variables, create new users, and connect to the database using Oracle SQL Developer. This setup provides a convenient way to work with Oracle databases in a containerized environment, offering flexibility and ease of management.</p>]]></content><author><name>Moin Uddin Ahmed</name></author><category term="Linux" /><category term="debian" /><category term="docker" /><category term="linux" /><category term="oracle" /><category term="ubuntu" /><summary type="html"><![CDATA[This documentation provides a detailed step-by-step guide to help you set up Oracle 19c using Docker on an Ubuntu 22.04 system.Whether you are a beginner or an experienced user, this document ensures you can set up and…]]></summary></entry></feed>