<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: divyanshu_Kumar</title>
    <description>The latest articles on DEV Community by divyanshu_Kumar (@d1vyanshukumar).</description>
    <link>https://dev.to/d1vyanshukumar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1362930%2F77a03698-994a-49c3-b4f5-b09de881c363.jpeg</url>
      <title>DEV Community: divyanshu_Kumar</title>
      <link>https://dev.to/d1vyanshukumar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/d1vyanshukumar"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>divyanshu_Kumar</dc:creator>
      <pubDate>Wed, 10 Jun 2026 09:01:50 +0000</pubDate>
      <link>https://dev.to/d1vyanshukumar/-98h</link>
      <guid>https://dev.to/d1vyanshukumar/-98h</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ansible-from-scratch-for-my-open-source-project-heres-the-full-breakdown-3eh6" class="crayons-story__hidden-navigation-link"&gt;I Learned Ansible From Scratch for My Open-Source Project — Here's the Full Breakdown&lt;/a&gt;


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

          &lt;a href="/d1vyanshukumar" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1362930%2F77a03698-994a-49c3-b4f5-b09de881c363.jpeg" alt="d1vyanshukumar profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/d1vyanshukumar" class="crayons-story__secondary fw-medium m:hidden"&gt;
              divyanshu_Kumar
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                divyanshu_Kumar
                
              
              &lt;div id="story-author-preview-content-3864120" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/d1vyanshukumar" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1362930%2F77a03698-994a-49c3-b4f5-b09de881c363.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;divyanshu_Kumar&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ansible-from-scratch-for-my-open-source-project-heres-the-full-breakdown-3eh6" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 10&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ansible-from-scratch-for-my-open-source-project-heres-the-full-breakdown-3eh6" id="article-link-3864120"&gt;
          I Learned Ansible From Scratch for My Open-Source Project — Here's the Full Breakdown
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ansible"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ansible&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ansible-from-scratch-for-my-open-source-project-heres-the-full-breakdown-3eh6" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;8&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ansible-from-scratch-for-my-open-source-project-heres-the-full-breakdown-3eh6#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

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

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>I Learned Ansible From Scratch for My Open-Source Project — Here's the Full Breakdown</title>
      <dc:creator>divyanshu_Kumar</dc:creator>
      <pubDate>Wed, 10 Jun 2026 08:59:45 +0000</pubDate>
      <link>https://dev.to/d1vyanshukumar/i-learned-ansible-from-scratch-for-my-open-source-project-heres-the-full-breakdown-3eh6</link>
      <guid>https://dev.to/d1vyanshukumar/i-learned-ansible-from-scratch-for-my-open-source-project-heres-the-full-breakdown-3eh6</guid>
      <description>&lt;p&gt;This is &lt;strong&gt;Part 2&lt;/strong&gt; of my pre-implementation journey — a feature I am contributing to the open-source &lt;a href="https://github.com/debezium/debezium-platform" rel="noopener noreferrer"&gt;Debezium Platform&lt;/a&gt; project. In &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ssh-from-scratch-for-my-open-source-project-heres-everything-step-by-step-2077"&gt;Part 1, I learned SSH from scratch&lt;/a&gt; — generated key pairs, understood &lt;code&gt;~/.ssh/config&lt;/code&gt;, fixed permission errors, and built two Docker containers as fake SSH servers on my MacBook.&lt;/p&gt;

&lt;p&gt;If you haven't read Part 1, here's the short version: I have two Docker containers (&lt;code&gt;db-server-1&lt;/code&gt; and &lt;code&gt;db-server-2&lt;/code&gt;) that accept SSH connections via key-based auth using a config alias. My &lt;code&gt;~/.ssh/config&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; db-server-1
    &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;127&lt;/span&gt;.0.0.1
    &lt;span class="k"&gt;User&lt;/span&gt; deploy
    &lt;span class="k"&gt;Port&lt;/span&gt; &lt;span class="m"&gt;2201&lt;/span&gt;
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/ddd41_practice

&lt;span class="k"&gt;Host&lt;/span&gt; db-server-2
    &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;127&lt;/span&gt;.0.0.1
    &lt;span class="k"&gt;User&lt;/span&gt; deploy
    &lt;span class="k"&gt;Port&lt;/span&gt; &lt;span class="m"&gt;2202&lt;/span&gt;
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/ddd41_practice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the question is: &lt;strong&gt;what do I actually do with those SSH connections?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;Ansible&lt;/strong&gt;.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Why Ansible — And Why Not Just Java + SSH?
&lt;/h2&gt;

&lt;p&gt;This was the first question I had to answer before studying Ansible at all.&lt;/p&gt;

&lt;p&gt;Think about it: I already have SSH access to my remote servers. So why can't I just open those connections from Java and run shell commands? Well, SSH gives you a &lt;strong&gt;tunnel&lt;/strong&gt; — it doesn't give you an &lt;strong&gt;automation engine&lt;/strong&gt;. If I need to provision multiple remote servers from my main machine, SSH alone becomes incredibly tedious. I'd have to manually handle every step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install Docker&lt;/li&gt;
&lt;li&gt;Start the Docker daemon&lt;/li&gt;
&lt;li&gt;Add the SSH user to the &lt;code&gt;docker&lt;/code&gt; group&lt;/li&gt;
&lt;li&gt;Deploy the Host Agent as a &lt;code&gt;systemd&lt;/code&gt; service&lt;/li&gt;
&lt;li&gt;Pre-pull the Debezium Server Docker image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My first instinct as a Java developer was: &lt;em&gt;"Can't I just use JSch or Apache MINA-SSHD and do all of this in Java?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let me show you why that instinct was wrong:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;What you'd have to write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pure Java + SSH library&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OS detection logic, &lt;code&gt;apt&lt;/code&gt; vs &lt;code&gt;yum&lt;/code&gt;/&lt;code&gt;dnf&lt;/code&gt; branching, error handling, retry logic, idempotency checks — for &lt;strong&gt;every single step&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ansible + YAML playbook&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~150 lines of YAML. All of the above is handled by built-in Ansible modules.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ansible handles OS detection (&lt;code&gt;ansible_os_family&lt;/code&gt;), idempotency (modules check current state before acting), error reporting, retries, and parallel execution — &lt;strong&gt;for free&lt;/strong&gt;. The Debezium design document puts it plainly: Java just runs &lt;code&gt;ProcessBuilder&lt;/code&gt; to call &lt;code&gt;ansible-playbook&lt;/code&gt;. Ansible does the heavy lifting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgdvoovussjewdz1i5ib3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgdvoovussjewdz1i5ib3.gif" alt="GIF of someone delegating all work and relaxing" width="480" height="264"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Ansible
&lt;/h2&gt;

&lt;p&gt;Installing Ansible on macOS is a one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ansible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;ansible [core 2.17.x]&lt;/code&gt; or similar. Then install the Docker community collection (needed later for pulling images idempotently):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-galaxy collection &lt;span class="nb"&gt;install &lt;/span&gt;community.docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Mental Model — Four Things You Need to Understand
&lt;/h2&gt;

&lt;p&gt;Before I ran a single Ansible command, I forced myself to understand four core concepts. Without these, you're just copying commands without knowing &lt;strong&gt;why&lt;/strong&gt; they work.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Control Node
&lt;/h3&gt;

&lt;p&gt;Your Mac (or whatever machine you're running Ansible from). This is where Ansible is installed and where you execute &lt;code&gt;ansible-playbook&lt;/code&gt;. Here's the important part: &lt;strong&gt;Ansible does not need to be installed on the remote machines&lt;/strong&gt; — only on your control node.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Managed Node
&lt;/h3&gt;

&lt;p&gt;The remote host where Ansible will make changes. It only needs &lt;strong&gt;Python 3&lt;/strong&gt; and &lt;strong&gt;SSH access&lt;/strong&gt;. Both of our Docker containers qualify.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Inventory
&lt;/h3&gt;

&lt;p&gt;A list of hosts for Ansible to target. This can be a file (like &lt;code&gt;inventory.ini&lt;/code&gt;) or an inline string passed on the command line.&lt;/p&gt;

&lt;p&gt;The design uses &lt;strong&gt;inline ad-hoc inventory&lt;/strong&gt; — a comma-separated string passed directly via the &lt;code&gt;-i&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-playbook host-setup.yml &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"db-server-1,"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that trailing comma after the hostname? &lt;strong&gt;That is not a typo — it is required.&lt;/strong&gt; Without it, Ansible interprets the string as a &lt;strong&gt;filename&lt;/strong&gt; and tries to open a file called &lt;code&gt;db-server-1&lt;/code&gt; on your disk. The comma tells the parser: &lt;em&gt;"This is a comma-separated list of hosts that happens to contain exactly one item."&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why not use an inventory file?&lt;/strong&gt; Because hosts in DDD-41 are dynamic — they come from &lt;code&gt;~/.ssh/config&lt;/code&gt;, not a static file. The Java &lt;code&gt;HostProvisioningService&lt;/code&gt; provisions one host at a time and builds the inventory string programmatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And here's the elegant part: when Ansible sees &lt;code&gt;db-server-1&lt;/code&gt; in the inventory, it doesn't need you to explicitly pass an IP address, port, or private key path. It calls your system's native &lt;strong&gt;OpenSSH&lt;/strong&gt; client under the hood, which automatically reads &lt;code&gt;~/.ssh/config&lt;/code&gt;, resolves the alias, and establishes the connection. Zero extra configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Playbook
&lt;/h3&gt;

&lt;p&gt;A YAML file describing &lt;strong&gt;what to do&lt;/strong&gt;. It contains &lt;strong&gt;plays&lt;/strong&gt;, each play contains &lt;strong&gt;tasks&lt;/strong&gt;, and each task calls a &lt;strong&gt;module&lt;/strong&gt;. Think of it as a nested structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Playbook
└── Play (target: all hosts in inventory)
    ├── Task 1: Bootstrap Python
    ├── Task 2: Install Docker
    ├── Task 3: Start Docker service
    ├── Task 4: Add user to docker group
    ├── Task 5: Deploy Host Agent as systemd service
    └── Task 6: Pre-pull Debezium Server image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How Ansible Reads ~/.ssh/config Automatically
&lt;/h2&gt;

&lt;p&gt;This is the part that genuinely surprised me. I assumed I'd have to pass IP addresses, ports, and key paths directly into the playbook or build some mapping layer in Java.&lt;/p&gt;

&lt;p&gt;Nope.&lt;/p&gt;

&lt;p&gt;When Ansible connects to &lt;code&gt;db-server-1&lt;/code&gt;, it delegates the connection to your system's &lt;strong&gt;OpenSSH&lt;/strong&gt; client. OpenSSH automatically reads &lt;code&gt;~/.ssh/config&lt;/code&gt;. That means Ansible &lt;strong&gt;inherits your entire SSH configuration for free&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ What IP address to connect to&lt;/li&gt;
&lt;li&gt;✅ What port to use&lt;/li&gt;
&lt;li&gt;✅ What username to log in with&lt;/li&gt;
&lt;li&gt;✅ Which private key to authenticate with&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sysadmin maintains &lt;code&gt;~/.ssh/config&lt;/code&gt; — everything else flows from it automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ansible.cfg File — Avoiding Repetitive Flags
&lt;/h2&gt;

&lt;p&gt;Before running any commands, I created a project-level config file so I wouldn't have to repeat flags every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/ddd41-lab/ansible
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/ddd41-lab/ansible/ansible.cfg &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[defaults]
host_key_checking = False
gathering = explicit
timeout = 30

[ssh_connection]
ssh_args = -F ~/.ssh/config -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null
pipelining = True
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what these settings do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;host_key_checking = False&lt;/code&gt;&lt;/strong&gt; — Disables the &lt;em&gt;"Are you sure you want to continue connecting?"&lt;/em&gt; prompt. This is fine for a lab environment. &lt;strong&gt;Never do this in production.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gathering = explicit&lt;/code&gt;&lt;/strong&gt; — Tells Ansible not to auto-gather system facts at playbook start. We do it manually after bootstrapping Python, which matters on fresh hosts that might not have Python installed yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ssh_args = -F ~/.ssh/config&lt;/code&gt;&lt;/strong&gt; — Explicitly tells SSH to use our config file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pipelining = True&lt;/code&gt;&lt;/strong&gt; — Reduces the number of SSH sessions needed per task. Faster execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ansible reads &lt;code&gt;ansible.cfg&lt;/code&gt; from the &lt;strong&gt;current working directory&lt;/strong&gt; when you run a command. So as long as I &lt;code&gt;cd ~/ddd41-lab/ansible&lt;/code&gt; before running anything, this config is automatically active.&lt;/p&gt;




&lt;h2&gt;
  
  
  First Test: Ad-Hoc Ping
&lt;/h2&gt;

&lt;p&gt;An ad-hoc command runs a single Ansible module without a playbook — perfect for quick connectivity checks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/ddd41-lab/ansible
ansible db-server-1 &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"db-server-1,"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Common misconception:&lt;/strong&gt; The Ansible &lt;code&gt;ping&lt;/code&gt; module does &lt;strong&gt;NOT&lt;/strong&gt; send ICMP packets (unlike the &lt;code&gt;ping&lt;/code&gt; command in your terminal). It connects via SSH, runs a tiny Python script on the remote host, and verifies that Python is available and working. A successful &lt;code&gt;ping&lt;/code&gt; means: SSH works &lt;strong&gt;AND&lt;/strong&gt; Python is installed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To test both servers at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible all &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"db-server-1,db-server-2"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4v39kg7r37zpdh65etn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd4v39kg7r37zpdh65etn.png" alt="Ansible ping output showing SUCCESS for both servers" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both servers are alive and responding. Time to write a real playbook.&lt;/p&gt;




&lt;h2&gt;
  
  
  My First Real Playbook — A Connectivity Check
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;cat &amp;gt; ~/ddd41-lab/ansible/01-ping.yml &amp;lt;&amp;lt; 'EOF'&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify connectivity to all lab hosts&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
  &lt;span class="na"&gt;gather_facts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ping the host&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.ping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Print hostname&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hostname&lt;/span&gt;
      &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;result&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Show hostname output&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Remote&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;hostname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;result.stdout&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-playbook 01-ping.yml &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"db-server-1,db-server-2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh80mt40yjkttvgcd5qi8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh80mt40yjkttvgcd5qi8.png" alt="Playbook output showing successful ping and hostname for both servers" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Task Status Colors
&lt;/h3&gt;

&lt;p&gt;Ansible uses colour-coded output so you can read results at a glance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;What It Means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ok&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟢 Green&lt;/td&gt;
&lt;td&gt;Task ran successfully, nothing needed to change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;changed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟡 Yellow&lt;/td&gt;
&lt;td&gt;Task ran and made a modification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skipped&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🔵 Cyan&lt;/td&gt;
&lt;td&gt;Task was skipped (condition not met)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🔴 Red&lt;/td&gt;
&lt;td&gt;Task failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unreachable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🔴 Red&lt;/td&gt;
&lt;td&gt;Could not connect to the host at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  YAML Basics — Because Playbooks Are All YAML
&lt;/h2&gt;

&lt;p&gt;Before writing the full playbook, I needed to make sure my YAML fundamentals were solid. One wrong indentation and the entire playbook breaks. Here are the rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spaces only, never tabs.&lt;/strong&gt; Ansible recommends 2 spaces for indentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lists&lt;/strong&gt; start with &lt;code&gt;-&lt;/code&gt; (dash + space).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dictionaries&lt;/strong&gt; use &lt;code&gt;:&lt;/code&gt; (colon + space).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comments&lt;/strong&gt; start with &lt;code&gt;#&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strings&lt;/strong&gt; with special characters need quotes.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🫠 If you have tabs in a YAML file, Ansible throws the most cryptic error messages you've ever seen. I learned this the hard way when I copy-pasted a snippet from a web page that had invisible tab characters. Spent 20 minutes debugging a syntax error that was literally invisible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpp9ddi8qmowhf862bodm.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpp9ddi8qmowhf862bodm.gif" alt="GIF of someone squinting at code trying to find the bug" width="504" height="322"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the DDD-41 Provisioning Playbook
&lt;/h2&gt;

&lt;p&gt;Now we get to the core of the project.our playbook needs to execute &lt;strong&gt;6 specific provisioning steps&lt;/strong&gt; to transform a clean server into a fully managed Debezium host.&lt;/p&gt;

&lt;p&gt;Fair warning: if you copy the standard textbook templates for these steps, &lt;strong&gt;your pipeline will crash&lt;/strong&gt;. the real-world errors I hit, and how I actually fixed them.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Bootstrap Python (The "Permission Denied" Trap)
&lt;/h3&gt;

&lt;p&gt;Ansible modules are agentless, but they require &lt;strong&gt;Python&lt;/strong&gt; to be present on the remote host to execute tasks. If a server is completely fresh, Python might not be installed yet. The &lt;code&gt;ansible.builtin.raw&lt;/code&gt; module solves this chicken-and-egg problem — it sends raw SSH shell commands directly, bypassing the Python requirement entirely.&lt;/p&gt;

&lt;p&gt;The textbook version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ WILL FAIL: Permission denied&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bootstrap — install Python3 on Debian/Ubuntu&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.raw&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;apt-get update -qq &amp;amp;&amp;amp; apt-get install -y python3 python3-pip&lt;/span&gt;
  &lt;span class="na"&gt;changed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What actually happened:&lt;/strong&gt; The playbook instantly crashed with a wall of red:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;"E: List directory /var/lib/apt/lists/partial is missing. - Acquire (13: Permission denied)"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; When Ansible logs into the server, it uses the standard user from your SSH config — in our case, the &lt;code&gt;deploy&lt;/code&gt; user. A regular user doesn't have permission to install system packages. We need to tell Ansible to escalate privileges using &lt;code&gt;sudo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ✅ CORRECT: Explicitly escalates privileges via sudo&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bootstrap — install Python3 on Debian/Ubuntu&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.raw&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;apt-get update -qq &amp;amp;&amp;amp; apt-get install -y python3 python3-pip&lt;/span&gt;
  &lt;span class="na"&gt;changed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line — &lt;code&gt;become: true&lt;/code&gt; — maps directly to running the command with &lt;code&gt;sudo&lt;/code&gt;. It works seamlessly here because our Docker container's base setup configures the &lt;code&gt;deploy&lt;/code&gt; user with passwordless &lt;code&gt;sudo&lt;/code&gt; access in the &lt;code&gt;/etc/sudoers&lt;/code&gt; file (which we set up in Part 1).&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Gather Facts (The Deprecation Warning)
&lt;/h3&gt;

&lt;p&gt;Once Python is bootstrapped, we can safely collect system information. Ansible handles this through the &lt;code&gt;setup&lt;/code&gt; module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gather facts&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.setup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This populates an internal inventory of the server — CPU architecture, RAM, Linux distribution, OS family, and more. We need this data for the next step where we conditionally install Docker based on the OS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gotcha:&lt;/strong&gt; Older guides and tutorials reference top-level variables like &lt;code&gt;when: ansible_os_family == "Debian"&lt;/code&gt;. Modern versions of Ansible flag this with a bright yellow &lt;strong&gt;Deprecation Warning&lt;/strong&gt;. To future-proof your playbooks, access facts through the formal dictionary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ Old way (triggers deprecation warning)&lt;/span&gt;
&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ansible_os_family == "Debian"&lt;/span&gt;

&lt;span class="c1"&gt;# ✅ Modern way&lt;/span&gt;
&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ansible_facts['os_family'] == "Debian"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 3: Install Docker (The Package Name Mismatch)
&lt;/h3&gt;

&lt;p&gt;The playbook needs to conditionally install Docker based on whether the target is running Debian/Ubuntu or RHEL/CentOS.&lt;/p&gt;

&lt;p&gt;The textbook version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ WILL FAIL: Package not found&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Docker (Debian/Ubuntu)&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker.io&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-compose-plugin&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
    &lt;span class="na"&gt;update_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ansible_facts['os_family'] == "Debian"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What actually happened:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No package matching 'docker-compose-plugin' is available"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; The textbook assumed our servers had Docker's official apt repository pre-configured. Our lab containers use vanilla Ubuntu repositories, where the package is simply called &lt;code&gt;docker-compose&lt;/code&gt; — not &lt;code&gt;docker-compose-plugin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ✅ CORRECT: Uses package names from default Ubuntu repositories&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Docker (Debian/Ubuntu)&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker.io&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-compose&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
    &lt;span class="na"&gt;update_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ansible_facts['os_family'] == "Debian"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📝 &lt;strong&gt;Lesson learned:&lt;/strong&gt; Always verify package names against the actual repositories available on your target host. &lt;code&gt;apt-cache search docker&lt;/code&gt; is your best friend.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Step 4: Start Docker Daemon (The Container Limitation)
&lt;/h3&gt;

&lt;p&gt;Once Docker is installed, we tell the host to start the daemon and enable it on boot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ⚠️ EXPECTED TO FAIL IN LAB: No systemd inside Docker containers&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start and enable Docker service&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;ignore_errors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What actually happened:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;"System has not been booted with systemd as init system (PID 1)."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this is expected:&lt;/strong&gt; Our "servers" are lightweight Docker containers, not real VMs. They don't run &lt;code&gt;systemd&lt;/code&gt; as PID 1 — they lack a full init system. Since our goal here is to validate the Ansible playbook logic and Java &lt;code&gt;ProcessBuilder&lt;/code&gt; orchestration (not to actually run Docker-in-Docker on a laptop), we add &lt;code&gt;ignore_errors: yes&lt;/code&gt;. Ansible logs the failure, shrugs, and moves on to the next task.&lt;/p&gt;

&lt;p&gt;On a real bare-metal server or cloud VM, this task would succeed without issues.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 5: Add User to Docker Group (The Undefined Variable)
&lt;/h3&gt;

&lt;p&gt;To let our &lt;code&gt;deploy&lt;/code&gt; user run Docker commands without &lt;code&gt;sudo&lt;/code&gt;, we add it to the &lt;code&gt;docker&lt;/code&gt; group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ⚠️ WILL FAIL: Variable not defined&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add deploy user to docker group&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
    &lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🛡️ &lt;strong&gt;The &lt;code&gt;append: yes&lt;/code&gt; flag is critical.&lt;/strong&gt; Without it, Ansible's &lt;code&gt;user&lt;/code&gt; module &lt;strong&gt;replaces&lt;/strong&gt; all existing group memberships. With &lt;code&gt;append: yes&lt;/code&gt;, it &lt;strong&gt;adds&lt;/strong&gt; &lt;code&gt;docker&lt;/code&gt; to the user's existing groups without removing anything. This is idempotency in action — if the user is already in the &lt;code&gt;docker&lt;/code&gt; group, Ansible does nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What actually happened:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Error while resolving value for 'name': 'ansible_user' is undefined"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Standard Ansible setups define &lt;code&gt;ansible_user&lt;/code&gt; in static inventory files (like &lt;code&gt;hosts.ini&lt;/code&gt;). Since we use inline ad-hoc inventory (&lt;code&gt;-i "db-server-1,"&lt;/code&gt;), there's no file where this variable is declared. Ansible can't resolve it.&lt;/p&gt;

&lt;p&gt;The solution is to explicitly define it in a &lt;code&gt;vars&lt;/code&gt; block at the top of the playbook:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This ensures the template variable &lt;code&gt;{{ ansible_user }}&lt;/code&gt; resolves correctly everywhere in the playbook.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 6: Deploy the Host Agent as a Systemd Service
&lt;/h3&gt;

&lt;p&gt;The Java orchestrator needs a persistent remote process to communicate with. Since we haven't compiled the real agent yet, the playbook deploys a &lt;strong&gt;mock agent&lt;/strong&gt; — a lightweight shell script running a sleep loop — to validate that our directory structures, file permissions, and service configurations are structurally correct.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create agent directory&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/debezium-agent&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;directory&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0755'&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create mock agent script (for lab testing)&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/debezium-agent/agent.sh&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;#!/bin/bash&lt;/span&gt;
          &lt;span class="s"&gt;echo "Debezium Host Agent starting on port {{ agent_port }}..."&lt;/span&gt;
          &lt;span class="s"&gt;echo "Token: {{ agent_token | default('NO_TOKEN_PROVIDED') }}"&lt;/span&gt;
          &lt;span class="s"&gt;while true; do sleep 3600; done&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0755'&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create systemd service for Host Agent&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/systemd/system/debezium-agent.service&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;[Unit]&lt;/span&gt;
          &lt;span class="s"&gt;Description=Debezium Host Agent&lt;/span&gt;
          &lt;span class="s"&gt;After=network.target docker.service&lt;/span&gt;
          &lt;span class="s"&gt;Requires=docker.service&lt;/span&gt;

          &lt;span class="s"&gt;[Service]&lt;/span&gt;
          &lt;span class="s"&gt;Type=simple&lt;/span&gt;
          &lt;span class="s"&gt;User={{ ansible_user }}&lt;/span&gt;
          &lt;span class="s"&gt;ExecStart=/opt/debezium-agent/agent.sh&lt;/span&gt;
          &lt;span class="s"&gt;Restart=always&lt;/span&gt;
          &lt;span class="s"&gt;RestartSec=5&lt;/span&gt;
          &lt;span class="s"&gt;Environment="AGENT_PORT={{ agent_port }}"&lt;/span&gt;
          &lt;span class="s"&gt;Environment="AGENT_TOKEN={{ agent_token | default('test-token') }}"&lt;/span&gt;

          &lt;span class="s"&gt;[Install]&lt;/span&gt;
          &lt;span class="s"&gt;WantedBy=multi-user.target&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0644'&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reload systemd and start agent (expected to fail in lab)&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.systemd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debezium-agent&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
        &lt;span class="na"&gt;daemon_reload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;ignore_errors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;{{ agent_port }}&lt;/code&gt; and &lt;code&gt;{{ agent_token }}&lt;/code&gt; placeholders use &lt;strong&gt;Jinja2&lt;/strong&gt; templating — Ansible's template engine. These values get injected at runtime through the &lt;code&gt;-e&lt;/code&gt; (extra vars) flag when we run the playbook. And just like Step 4, the final &lt;code&gt;systemd&lt;/code&gt; reload uses &lt;code&gt;ignore_errors: yes&lt;/code&gt; because our Docker containers don't have a real init system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Complete Playbook
&lt;/h2&gt;

&lt;p&gt;Here's the full &lt;code&gt;host-setup.yml&lt;/code&gt; with all six steps assembled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;################################################################################&lt;/span&gt;
&lt;span class="c1"&gt;# DDD-41 Host Provisioning Playbook&lt;/span&gt;
&lt;span class="c1"&gt;# Provisions a bare-metal or VM host to run Debezium Server containers.&lt;/span&gt;
&lt;span class="c1"&gt;# Usage: ansible-playbook host-setup.yml -i "&amp;lt;ssh-alias&amp;gt;,"&lt;/span&gt;
&lt;span class="c1"&gt;################################################################################&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bootstrap and provision Debezium host&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
  &lt;span class="na"&gt;gather_facts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;agent_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8090&lt;/span&gt;
    &lt;span class="na"&gt;agent_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0"&lt;/span&gt;
    &lt;span class="na"&gt;debezium_image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quay.io/debezium/server:latest"&lt;/span&gt;
    &lt;span class="na"&gt;ansible_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deploy"&lt;/span&gt;

  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 1: Bootstrap Python using raw module&lt;/span&gt;
    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bootstrap — install Python3 on Debian/Ubuntu&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.raw&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;apt-get update -qq &amp;amp;&amp;amp; apt-get install -y python3 python3-pip&lt;/span&gt;
      &lt;span class="na"&gt;changed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gather facts&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.setup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 2: Install Docker&lt;/span&gt;
    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Docker (Debian/Ubuntu)&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.apt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker.io&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-compose&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
        &lt;span class="na"&gt;update_cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ansible_facts['os_family'] == "Debian"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Docker (RHEL/CentOS)&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.yum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;present&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ansible_facts['os_family'] == "RedHat"&lt;/span&gt;

    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 3: Start and enable Docker daemon&lt;/span&gt;
    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start and enable Docker service&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;ignore_errors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;

    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 4: Add SSH user to docker group&lt;/span&gt;
    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add deploy user to docker group&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
        &lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 5: Pre-pull Debezium Server image&lt;/span&gt;
    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pre-pull Debezium Server Docker image&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;docker pull {{ debezium_image }}&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_result&lt;/span&gt;
      &lt;span class="na"&gt;changed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;'Pull&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;complete'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pull_result.stdout&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'Downloaded'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pull_result.stdout"&lt;/span&gt;
      &lt;span class="na"&gt;ignore_errors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;

    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 6: Deploy the Host Agent as a systemd service&lt;/span&gt;
    &lt;span class="c1"&gt;############################################################&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create agent directory&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/debezium-agent&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;directory&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0755'&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create mock agent script (for lab testing)&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/debezium-agent/agent.sh&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;#!/bin/bash&lt;/span&gt;
          &lt;span class="s"&gt;echo "Debezium Host Agent starting on port {{ agent_port }}..."&lt;/span&gt;
          &lt;span class="s"&gt;echo "Token: {{ agent_token | default('NO_TOKEN_PROVIDED') }}"&lt;/span&gt;
          &lt;span class="s"&gt;while true; do sleep 3600; done&lt;/span&gt;
        &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0755'&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create systemd service for Host Agent&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.copy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/systemd/system/debezium-agent.service&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;[Unit]&lt;/span&gt;
          &lt;span class="s"&gt;Description=Debezium Host Agent&lt;/span&gt;
          &lt;span class="s"&gt;After=network.target docker.service&lt;/span&gt;
          &lt;span class="s"&gt;Requires=docker.service&lt;/span&gt;

          &lt;span class="s"&gt;[Service]&lt;/span&gt;
          &lt;span class="s"&gt;Type=simple&lt;/span&gt;
          &lt;span class="s"&gt;User={{ ansible_user }}&lt;/span&gt;
          &lt;span class="s"&gt;ExecStart=/opt/debezium-agent/agent.sh&lt;/span&gt;
          &lt;span class="s"&gt;Restart=always&lt;/span&gt;
          &lt;span class="s"&gt;RestartSec=5&lt;/span&gt;
          &lt;span class="s"&gt;Environment="AGENT_PORT={{ agent_port }}"&lt;/span&gt;
          &lt;span class="s"&gt;Environment="AGENT_TOKEN={{ agent_token | default('test-token') }}"&lt;/span&gt;

          &lt;span class="s"&gt;[Install]&lt;/span&gt;
          &lt;span class="s"&gt;WantedBy=multi-user.target&lt;/span&gt;
        &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0644'&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reload systemd and start agent (expected to fail in lab)&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.systemd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;debezium-agent&lt;/span&gt;
        &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
        &lt;span class="na"&gt;daemon_reload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
      &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;ignore_errors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Report provisioning complete&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.debug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;✅ Host {{ inventory_hostname }} provisioned successfully!&lt;/span&gt;
          &lt;span class="s"&gt;Docker: installed and running&lt;/span&gt;
          &lt;span class="s"&gt;Agent: deployed on port {{ agent_port }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Running the Full Playbook
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/ddd41-lab/ansible

ansible-playbook host-setup.yml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"db-server-1,"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"agent_port=8090 agent_token=test-bearer-token-abc123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then... you hold your breath and watch the terminal scroll.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxuvg2xf9pcylz0tcxnu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftxuvg2xf9pcylz0tcxnu.gif" alt="GIF of someone watching code run nervously" width="400" height="275"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading the Output — What Those Errors Actually Mean
&lt;/h2&gt;

&lt;p&gt;When the playbook finishes, you'll see some red text. &lt;strong&gt;Don't panic.&lt;/strong&gt; Let me walk you through each piece.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Docker Pull Error
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;failed to connect to the docker API... no such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this happened:&lt;/strong&gt; Remember Step 3, where we tried to start the Docker daemon but it failed because our Docker containers can't run &lt;code&gt;systemd&lt;/code&gt;? Since the Docker engine isn't running, Step 5 (pulling an image) physically cannot work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's fine:&lt;/strong&gt; Look right below the error message. You'll see:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Our &lt;code&gt;ignore_errors: yes&lt;/code&gt; safety net caught the crash and allowed the playbook to continue. Exactly as designed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Systemd Error
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;System has not been booted with systemd as init system (PID 1).
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this happened:&lt;/strong&gt; Same root cause. Docker containers don't have &lt;code&gt;systemd&lt;/code&gt; running as PID 1. The agent service can't be started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why it's fine:&lt;/strong&gt; Same &lt;code&gt;ignore_errors: yes&lt;/code&gt; — Ansible logs it, prints &lt;code&gt;...ignoring&lt;/code&gt;, and moves on.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Green Checkmark
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"msg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"✅ Host db-server-1 provisioned successfully!&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Docker: installed and running&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Agent: deployed on port 8090&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ansible reached the very end of the playbook. It created all directories, wrote the bash scripts, injected our &lt;code&gt;agent_port: 8090&lt;/code&gt; variable dynamically via Jinja2 templating, and printed the final success message.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Play Recap — Your Report Card
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;PLAY RECAP ****************************************************
db-server-1  : ok=11   changed=4   unreachable=0   failed=0   skipped=1   rescued=0   ignored=3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me decode this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;What It Means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No task permanently failed. The deployment is a success.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ignored&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ansible caught and bypassed 3 expected lab limitations (Docker daemon, image pull, systemd reload).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;changed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Four things were created: agent directory, mock script, service file, and Python was installed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;skipped&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The RHEL/CentOS Docker install was skipped (because our containers run Ubuntu).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The bottom line:&lt;/strong&gt; The logic is flawless. If you ran this exact playbook against a real bare-metal Ubuntu server, the &lt;code&gt;ignored=3&lt;/code&gt; would drop to &lt;code&gt;0&lt;/code&gt;, the red text would vanish, and it would deploy a real Debezium host end-to-end.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Verifying Everything Worked
&lt;/h2&gt;

&lt;p&gt;Trust, but verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Docker installed?&lt;/span&gt;
ssh db-server-1 &lt;span class="s2"&gt;"docker --version"&lt;/span&gt;

&lt;span class="c"&gt;# Agent directory created?&lt;/span&gt;
ssh db-server-1 &lt;span class="s2"&gt;"ls -la /opt/debezium-agent/"&lt;/span&gt;

&lt;span class="c"&gt;# Systemd service file exists?&lt;/span&gt;
ssh db-server-1 &lt;span class="s2"&gt;"cat /etc/systemd/system/debezium-agent.service"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these should return exactly what our playbook configured. The directory is there, the mock script is executable, and the service file has our templated &lt;code&gt;agent_port&lt;/code&gt; and &lt;code&gt;agent_token&lt;/code&gt; values baked in.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Important Concept: Idempotency
&lt;/h2&gt;

&lt;p&gt;This is the &lt;strong&gt;golden rule&lt;/strong&gt; of configuration management and the core reason why the Debezium design document specifies Ansible for the host provisioning engine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent&lt;/strong&gt; means: running an operation multiple times produces the &lt;strong&gt;exact same result&lt;/strong&gt; without unintended side effects. If Docker is already installed, the playbook shouldn't reinstall it. If a directory already exists with the correct permissions, Ansible should leave it untouched.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters for DDD-41
&lt;/h3&gt;

&lt;p&gt;According to Section 3 of the design document, the platform architecture features a &lt;strong&gt;dynamic file watcher&lt;/strong&gt; that monitors &lt;code&gt;~/.ssh/config&lt;/code&gt;. If a sysadmin modifies a host's IP address or changes an SSH alias, the watcher automatically re-triggers the provisioning pipeline.&lt;/p&gt;

&lt;p&gt;Because of this, our playbook &lt;strong&gt;must be completely safe to re-run&lt;/strong&gt; against an active, healthy host without disrupting running services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modules vs. Shell: The Structural Difference
&lt;/h3&gt;

&lt;p&gt;To achieve idempotency, you must favor &lt;strong&gt;native Ansible modules&lt;/strong&gt; over raw shell commands. Here's the exact contrast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ❌ NOT IDEMPOTENT: Runs every time, always reports "changed"&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add user to docker group&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;usermod -aG docker deploy&lt;/span&gt;

&lt;span class="c1"&gt;# ✅ IDEMPOTENT: Checks current state first, only acts if needed&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Add user to docker group&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deploy"&lt;/span&gt;
    &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
    &lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yes&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shell version blindly runs &lt;code&gt;usermod&lt;/code&gt; every time — even if the user is already in the group. It always reports &lt;code&gt;changed&lt;/code&gt;, making it impossible to tell if your playbook actually did anything meaningful.&lt;/p&gt;

&lt;p&gt;The module version &lt;strong&gt;inspects the system state first&lt;/strong&gt;. If &lt;code&gt;deploy&lt;/code&gt; is already in the &lt;code&gt;docker&lt;/code&gt; group, it does nothing and reports &lt;code&gt;ok&lt;/code&gt;. This is the difference between "automation" and "reliable automation."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule is simple: always prefer built-in Ansible modules over &lt;code&gt;shell&lt;/code&gt; or &lt;code&gt;command&lt;/code&gt; tasks.&lt;/strong&gt; Modules are designed to be idempotent out of the box. Shell scripts are blind to existing state unless you manually wrap them with conditional checks.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Happens on the Second Run?
&lt;/h3&gt;

&lt;p&gt;To verify idempotency, I ran the exact same playbook a second time against the same containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;PLAY RECAP ****************************************************
db-server-1  : ok=11   changed=0   unreachable=0   failed=0   skipped=1   rescued=0   ignored=3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that? &lt;strong&gt;&lt;code&gt;changed=0&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what happened:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Structural tasks shifted from yellow to green.&lt;/strong&gt; Creating the agent directory, writing the mock script, and generating the service file all reported &lt;code&gt;ok&lt;/code&gt; instead of &lt;code&gt;changed&lt;/code&gt;. Ansible checked the containers, verified the files matched the playbook specification exactly, and skipped rewriting them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lab workarounds still triggered.&lt;/strong&gt; The Docker daemon, image pull, and systemd reload tasks still hit their container limitations and fell back to &lt;code&gt;ignore_errors: yes&lt;/code&gt;. That's expected and correct.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero unnecessary changes.&lt;/strong&gt; The playbook confirmed the environment, touched nothing that was already correct, and proved that our provisioning pipeline is completely safe for continuous re-execution.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That &lt;code&gt;changed=0&lt;/code&gt; on the second run is the ultimate proof that your playbook is well-structured. It means the pipeline can safely loop through the same host over and over without causing drift or disruption.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🧠 &lt;strong&gt;Think of it this way:&lt;/strong&gt; A good playbook is like a good &lt;code&gt;if&lt;/code&gt; statement — it only does work when the condition demands it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I Took Away From All This
&lt;/h2&gt;

&lt;p&gt;By the end of this phase, I went from viewing Ansible as just another DevOps buzzword to understanding how to design and debug a resilient infrastructure pipeline. Wrangling with configuration errors on my M1 Mac forced me to appreciate the nuance required to build production-ready automation.&lt;/p&gt;

&lt;p&gt;Here are the three architectural insights that clicked for me:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Power of Delegating to OpenSSH
&lt;/h3&gt;

&lt;p&gt;The thing that surprised me most was how cleanly Ansible handles networking. I initially assumed I'd have to pass explicit IP addresses, ports, and private key paths into the playbook or build a complex mapping layer in Java.&lt;/p&gt;

&lt;p&gt;Instead, Ansible completely offloads connection management to the system's native &lt;strong&gt;OpenSSH&lt;/strong&gt; client. Because OpenSSH automatically reads &lt;code&gt;~/.ssh/config&lt;/code&gt;, Ansible inherits that entire configuration for free. This made the DDD-41 host discovery architecture click: a sysadmin maintains one standard SSH config file, the platform watches it for changes, and the automation engine handles everything else.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Idempotency Is Verified, Not Assumed
&lt;/h3&gt;

&lt;p&gt;Running this playbook six or seven times taught me what idempotency actually means in practice. On the first run, my terminal was flooded with yellow &lt;code&gt;changed&lt;/code&gt; statuses as directories were created and packages were installed. On the second run, every structural task shifted to green &lt;code&gt;ok&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Seeing &lt;code&gt;changed=0&lt;/code&gt; in the play recap is the ultimate proof of a well-behaved playbook. It proves that by favoring native modules over blind shell commands, the engine inspects remote state before touching a single file. This guarantee is critical for the file-watcher architecture — if a config change re-triggers provisioning, the pipeline passes through an active, healthy host without breaking anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A Clean Separation of Concerns
&lt;/h3&gt;

&lt;p&gt;The boundary between the Java control plane and the Ansible automation layer is completely decoupled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java's job:&lt;/strong&gt; Manage the high-level lifecycle state machine (&lt;code&gt;PENDING → PROVISIONING → READY/FAILED&lt;/code&gt;), handle asynchronous execution so the application never freezes during provisioning, and feed context variables into the deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ansible's job:&lt;/strong&gt; Manage the concrete reality of the remote host — validate package states, create directories, write service configurations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They don't need to know anything about each other's internals. Java fires off a &lt;code&gt;ProcessBuilder&lt;/code&gt;, passes the runtime flags (&lt;code&gt;-e&lt;/code&gt;), and waits for an exit code. A &lt;code&gt;0&lt;/code&gt; means success; anything else means failure. Clean, simple, decoupled.&lt;/p&gt;




&lt;p&gt;With SSH key authentication (Part 1) and Ansible provisioning playbooks (Part 2) fully tested and running against my local container lab, the automation foundation is rock solid.&lt;/p&gt;




&lt;p&gt;Thank you for reading! If this helped you understand Ansible better (or saved you from the same &lt;code&gt;Permission denied&lt;/code&gt; errors I hit), drop a comment or share your feedback. I'd love to hear how you'd approach this differently.&lt;/p&gt;

&lt;p&gt;Happy automating! 🚀&lt;/p&gt;

</description>
      <category>devops</category>
      <category>opensource</category>
      <category>beginners</category>
      <category>ansible</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>divyanshu_Kumar</dc:creator>
      <pubDate>Tue, 09 Jun 2026 19:41:13 +0000</pubDate>
      <link>https://dev.to/d1vyanshukumar/-509h</link>
      <guid>https://dev.to/d1vyanshukumar/-509h</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ssh-from-scratch-for-my-open-source-project-heres-everything-step-by-step-2077" class="crayons-story__hidden-navigation-link"&gt;How I Finally Understood SSH Before Building a Real Open-Source Feature?&lt;/a&gt;


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

          &lt;a href="/d1vyanshukumar" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1362930%2F77a03698-994a-49c3-b4f5-b09de881c363.jpeg" alt="d1vyanshukumar profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/d1vyanshukumar" class="crayons-story__secondary fw-medium m:hidden"&gt;
              divyanshu_Kumar
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                divyanshu_Kumar
                
              
              &lt;div id="story-author-preview-content-3859836" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/d1vyanshukumar" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1362930%2F77a03698-994a-49c3-b4f5-b09de881c363.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;divyanshu_Kumar&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ssh-from-scratch-for-my-open-source-project-heres-everything-step-by-step-2077" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 9&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ssh-from-scratch-for-my-open-source-project-heres-everything-step-by-step-2077" id="article-link-3859836"&gt;
          How I Finally Understood SSH Before Building a Real Open-Source Feature?
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ssh"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ssh&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/docker"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;docker&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ssh-from-scratch-for-my-open-source-project-heres-everything-step-by-step-2077" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;6&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/d1vyanshukumar/i-learned-ssh-from-scratch-for-my-open-source-project-heres-everything-step-by-step-2077#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              2&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>How I Finally Understood SSH Before Building a Real Open-Source Feature?</title>
      <dc:creator>divyanshu_Kumar</dc:creator>
      <pubDate>Tue, 09 Jun 2026 19:37:55 +0000</pubDate>
      <link>https://dev.to/d1vyanshukumar/i-learned-ssh-from-scratch-for-my-open-source-project-heres-everything-step-by-step-2077</link>
      <guid>https://dev.to/d1vyanshukumar/i-learned-ssh-from-scratch-for-my-open-source-project-heres-everything-step-by-step-2077</guid>
      <description>&lt;p&gt;Hi, I'm a Java developer working on an open-source contribution to the &lt;a href="https://debezium.io/" rel="noopener noreferrer"&gt;Debezium&lt;/a&gt; Platform — a project under the Red Hat/JBoss umbrella. The feature I'm building is called &lt;a href="https://github.com/debezium/debezium-design-documents/pull/45#top" rel="noopener noreferrer"&gt;Host-Based Pipeline Deployment&lt;/a&gt;. The idea is to allow the platform to deploy Debezium Server containers on bare-metal servers and cloud VMs — not just Kubernetes — &lt;em&gt;using SSH and Ansible as the automation backbone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I had never gone deep into how SSH actually works, what the config file does, or why file permissions like &lt;code&gt;0600&lt;/code&gt; and &lt;code&gt;0700&lt;/code&gt; exist. And I definitely had no separate server lying around to test against.&lt;/p&gt;

&lt;p&gt;This post is the story of how I went from zero to fully understanding SSH, entirely on my MacBook M1, using Docker containers as fake servers. No cloud account needed. No separate machine needed. Just one MacBook and some curiosity.&lt;/p&gt;

&lt;p&gt;This will be a great and very beginner-friendly ride, so &lt;code&gt;Developers, Assemble!&lt;/code&gt; 🥷&lt;/p&gt;

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




&lt;h2&gt;
  
  
  What Is SSH, and Why Does It Matter for This Feature?
&lt;/h2&gt;

&lt;p&gt;SSH stands for &lt;em&gt;Secure Shell&lt;/em&gt;. It provides a cryptographically secured environment (the "Secure" part) to access a computer's command-line interface (the "Shell" part) over an untrusted network.&lt;/p&gt;

&lt;p&gt;My feature is all about deploying pipeline containers to remote host servers from a central Conductor service. To do that, we need secure connections over the network so that operations on the remote server happen safely and reliably.&lt;/p&gt;

&lt;p&gt;Here's how the feature works: A sysadmin creates a standard OpenSSH &lt;a href="https://dev.to/eugene-zimin/ssh-config-file-forgotten-gem-1339"&gt;&lt;code&gt;~/.ssh/config&lt;/code&gt;&lt;/a&gt; file listing all the target hosts they want Debezium to deploy to. The Conductor service (a Quarkus Java app) watches that file for changes. When a new host appears, it triggers an Ansible playbook to provision that host — install Docker, deploy the Host Agent (a lightweight HTTP service), pre-pull the Debezium Server Docker image, and so on.&lt;/p&gt;

&lt;p&gt;The critical design decision is: &lt;strong&gt;Java never opens SSH sessions directly.&lt;/strong&gt; No JSch, no Apache MINA-SSHD. Instead, Java calls Ansible via &lt;a href="https://www.baeldung.com/java-lang-processbuilder-api" rel="noopener noreferrer"&gt;&lt;code&gt;ProcessBuilder&lt;/code&gt;&lt;/a&gt;, and Ansible handles all SSH connectivity by reading &lt;code&gt;~/.ssh/config&lt;/code&gt; natively through OpenSSH.&lt;/p&gt;

&lt;p&gt;So if I don't understand SSH — the key pairs, the config file, the permissions — I can't understand why any of the Java code is written the way it is.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Realizing macOS Already Had SSH (and It Was Fine)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-V&lt;/span&gt;
&lt;span class="c"&gt;# OpenSSH_9.9p1, LibreSSL 3.3.6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS ships with OpenSSH pre-installed. Nothing to install. I then checked if the &lt;code&gt;.ssh&lt;/code&gt; directory already existed — it didn't, so I created one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/.ssh
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; ~/.ssh/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: How SSH Actually Works — The Padlock Analogy
&lt;/h2&gt;

&lt;p&gt;I always thought SSH just meant "type a password over an encrypted connection." I was wrong. SSH with key-based authentication works like this:&lt;/p&gt;

&lt;p&gt;Think of a &lt;strong&gt;padlock&lt;/strong&gt;. You have a padlock (your public key) and a key to open it (your private key). You give the padlock to every server you want to connect to — they lock a challenge with it and send it back. Only your key can open it. If you can open it, you prove you own the key — &lt;strong&gt;without ever sending the key to anyone.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YOU (your Mac):
-------------------------------
private key ← stays on your Mac
(~/.ssh/ddd41_practice)

SERVER (remote host):
-------------------------------
public key ← you put this here
(~/.ssh/authorized_keys on server)

What happens at connection time:
  Server → encrypts a challenge using your public key → sends it
  You    → decrypt it with your private key → send proof back
  Server → "Correct! You're in." — no password ever sent.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why losing your private key is such a big deal — anyone who has it can impersonate you to any server that has your public key.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Generating a Project-Specific SSH Key
&lt;/h2&gt;

&lt;p&gt;First, I made sure the &lt;code&gt;.ssh&lt;/code&gt; directory existed with the correct permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/.ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I generated an &lt;a href="https://www.brandonchecketts.com/archives/ssh-ed25519-key-best-practices-for-2025" rel="noopener noreferrer"&gt;ED25519&lt;/a&gt; key. Why ED25519 and not RSA? It's modern, faster, and produces smaller keys — it's the recommended type in 2025+.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"ddd41-practice"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/ddd41_practice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it asked for a passphrase, I pressed Enter twice to skip it. For local development testing, a passphrase gets in the way. In production, you'd always set one.&lt;/p&gt;

&lt;p&gt;This created two files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.ssh/ddd41_practice&lt;/code&gt; — the private key (no extension). &lt;strong&gt;NEVER share this.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.ssh/ddd41_practice.pub&lt;/code&gt; — the public key. Safe to share.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the permissions right away: &lt;code&gt;600&lt;/code&gt; on the private key, &lt;code&gt;644&lt;/code&gt; on the public key. I didn't set those manually — &lt;code&gt;ssh-keygen&lt;/code&gt; set them automatically. That detail matters a lot, and I'll explain why next.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Lives in &lt;code&gt;~/.ssh/&lt;/code&gt; Now
&lt;/h3&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The SSH config file — aliases and settings for each host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;known_hosts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fingerprints of servers you've connected to before&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ddd41_practice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your &lt;strong&gt;private key&lt;/strong&gt; (NEVER share this)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ddd41_practice.pub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your &lt;strong&gt;public key&lt;/strong&gt; (safe to share)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 4: The Permission Rabbit Hole — Why 0600, 0700, 0400 Are Non-Negotiable
&lt;/h2&gt;

&lt;p&gt;This was the part that genuinely surprised me. SSH is &lt;em&gt;paranoid&lt;/em&gt; about file permissions. If your private key is readable by other users on the system, SSH refuses to use it at all — it won't even try.&lt;/p&gt;

&lt;p&gt;Here's the breakdown (&lt;code&gt;r=4, w=2, x=1&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Required Permission&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;What Happens If Wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;~/.ssh/&lt;/code&gt; directory&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;700&lt;/code&gt; (&lt;code&gt;rwx------&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Only you can read, write, or enter&lt;/td&gt;
&lt;td&gt;SSH ignores all config inside it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.ssh/config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;600&lt;/code&gt; (&lt;code&gt;rw-------&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Only you can read and write&lt;/td&gt;
&lt;td&gt;SSH ignores the config file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private key file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;600&lt;/code&gt; or &lt;code&gt;400&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Only you can read (and optionally write)&lt;/td&gt;
&lt;td&gt;SSH refuses: &lt;code&gt;"Permissions are too open"&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public key file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;644&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Others can read (it's meant to be shared)&lt;/td&gt;
&lt;td&gt;Fine either way&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a "best practice suggestion." SSH will &lt;strong&gt;literally refuse&lt;/strong&gt; to use a key that is world-readable. This is a security guarantee baked into the protocol.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: The SSH Config File — The Most Important Part
&lt;/h2&gt;

&lt;p&gt;Without a config file, connecting to a server looks like this every single time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/ddd41_practice &lt;span class="nt"&gt;-p&lt;/span&gt; 2222 &lt;span class="nt"&gt;-l&lt;/span&gt; ubuntu 192.168.1.10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is tedious and error-prone. The SSH config file at &lt;code&gt;~/.ssh/config&lt;/code&gt; lets you write all of that once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ssh"&gt;&lt;code&gt;&lt;span class="k"&gt;Host&lt;/span&gt; db-server-1
    &lt;span class="k"&gt;HostName&lt;/span&gt; &lt;span class="m"&gt;192&lt;/span&gt;.168.1.10
    &lt;span class="k"&gt;User&lt;/span&gt; ubuntu
    &lt;span class="k"&gt;Port&lt;/span&gt; &lt;span class="m"&gt;2222&lt;/span&gt;
    &lt;span class="k"&gt;IdentityFile&lt;/span&gt; ~/.ssh/ddd41_practice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then just type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh db-server-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SSH reads the config, finds the &lt;code&gt;db-server-1&lt;/code&gt; block, and automatically uses the right IP, port, user, and key. This is exactly why the DDD-41 design uses &lt;code&gt;~/.ssh/config&lt;/code&gt; as the source of truth for host discovery — the sysadmin already knows how to write SSH configs. No new UI, no new API, no new format to learn.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Building Fake SSH Servers on My Mac (Docker)
&lt;/h2&gt;

&lt;p&gt;I don't have a separate Linux server. But I have Docker. The solution: build a custom Ubuntu Docker image with an SSH server installed and my public key pre-authorized as a build argument.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Install &lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install Ansible via Homebrew: &lt;code&gt;brew install ansible&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Install the Docker Ansible collection: &lt;code&gt;ansible-galaxy collection install community.docker&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  6.1 — Create a Project Directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/ddd41-lab/docker
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/ddd41-lab/docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.2 — Create the Dockerfile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;cat &amp;gt; ~/ddd41-lab/docker/Dockerfile &amp;lt;&amp;lt; 'EOF'
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ubuntu:22.04&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DEBIAN_FRONTEND=noninteractive&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        openssh-server &lt;span class="se"&gt;\
&lt;/span&gt;        &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        python3 &lt;span class="se"&gt;\
&lt;/span&gt;        python3-pip &lt;span class="se"&gt;\
&lt;/span&gt;        curl &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /bin/bash deploy &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deploy ALL=(ALL) NOPASSWD:ALL"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/sudoers

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/deploy/.ssh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chmod &lt;/span&gt;700 /home/deploy/.ssh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown &lt;/span&gt;deploy:deploy /home/deploy/.ssh

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; SSH_PUB_KEY&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_PUB_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /home/deploy/.ssh/authorized_keys &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chmod &lt;/span&gt;600 /home/deploy/.ssh/authorized_keys &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;chown &lt;/span&gt;deploy:deploy /home/deploy/.ssh/authorized_keys

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; /run/sshd &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/#PubkeyAuthentication yes/PubkeyAuthentication yes/'&lt;/span&gt; /etc/ssh/sshd_config &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/#PasswordAuthentication yes/PasswordAuthentication no/'&lt;/span&gt; /etc/ssh/sshd_config &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/PasswordAuthentication yes/PasswordAuthentication no/'&lt;/span&gt; /etc/ssh/sshd_config &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/#PermitRootLogin prohibit-password/PermitRootLogin no/'&lt;/span&gt; /etc/ssh/sshd_config

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 22&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["/usr/sbin/sshd", "-D"]&lt;/span&gt;
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.3 — Build the Image, Injecting Your Public Key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/ddd41-lab/docker

docker build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;SSH_PUB_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.ssh/ddd41_practice.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; ddd41-fake-server:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What just happened?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker built an Ubuntu image with an OpenSSH server installed&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;deploy&lt;/code&gt; user was created with passwordless sudo&lt;/li&gt;
&lt;li&gt;Your public key was embedded into the image's &lt;code&gt;authorized_keys&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Now, any container from this image will accept connections with your &lt;code&gt;ddd41_practice&lt;/code&gt; private key&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6.4 — Start the Fake Servers
&lt;/h3&gt;

&lt;p&gt;We'll start two containers to simulate &lt;code&gt;db-server-1&lt;/code&gt; and &lt;code&gt;db-server-2&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; db-server-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 2201:22 &lt;span class="se"&gt;\&lt;/span&gt;
  ddd41-fake-server:latest

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; db-server-2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 2202:22 &lt;span class="se"&gt;\&lt;/span&gt;
  ddd41-fake-server:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify they're running:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.5 — Configure the SSH Config File
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.ssh/config &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'

Host db-server-1
    HostName 127.0.0.1
    User deploy
    Port 2201
    IdentityFile ~/.ssh/ddd41_practice
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null

Host db-server-2
    HostName 127.0.0.1
    User deploy
    Port 2202
    IdentityFile ~/.ssh/ddd41_practice
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.ssh/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.6 — Test SSH Connections
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test connecting to db-server-1&lt;/span&gt;
ssh db-server-1 &lt;span class="s2"&gt;"hostname"&lt;/span&gt;

&lt;span class="c"&gt;# Test connecting to db-server-2&lt;/span&gt;
ssh db-server-2 &lt;span class="s2"&gt;"hostname"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Yaiiii, it worked! 🎉 No IP. No port flag. No &lt;code&gt;-i&lt;/code&gt; flag. Just the alias. SSH read the config, found the &lt;code&gt;db-server-1&lt;/code&gt; block, used &lt;code&gt;127.0.0.1:2201&lt;/code&gt;, authenticated with &lt;code&gt;~/.ssh/ddd41_practice&lt;/code&gt;, and connected as &lt;code&gt;deploy&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.7 — The &lt;code&gt;ssh -G&lt;/code&gt; Command: Verify What SSH Actually Resolved
&lt;/h3&gt;

&lt;p&gt;This command is incredibly useful for debugging. It shows you &lt;em&gt;exactly&lt;/em&gt; what SSH resolved for a given host alias — without actually connecting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-G&lt;/span&gt; db-server-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints every resolved setting: the hostname, port, user, identity file, and more. If something isn't working, &lt;strong&gt;always run &lt;code&gt;ssh -G&lt;/code&gt; before blaming anything else.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Took Away From All This
&lt;/h2&gt;

&lt;p&gt;After going through every step above, I finally understood things I had taken for granted for years:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why key-based auth is better than passwords&lt;/strong&gt; — the private key never leaves your machine. There is nothing to intercept in transit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why file permissions are enforced by SSH itself&lt;/strong&gt; — it's not optional or a best-practice suggestion. SSH will literally refuse to use a key that is world-readable. This is a security guarantee baked into the protocol.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;~/.ssh/config&lt;/code&gt; is so powerful&lt;/strong&gt; — and why my project uses it as the source of truth for host discovery. The sysadmin already knows how to write SSH configs. The platform reads it directly — no new UI, no new API, no new format to learn.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;ssh -G&lt;/code&gt; is the most useful debugging command&lt;/strong&gt; — always run it before blaming anything else.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me know in the comments: &lt;strong&gt;what was your Best moment with SSH?&lt;/strong&gt; 💬&lt;/p&gt;




&lt;p&gt;Let's recap what we accomplished:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Generated an SSH key pair (ED25519)&lt;/li&gt;
&lt;li&gt;✅ Built fake SSH servers with Docker (public key pre-authorized)&lt;/li&gt;
&lt;li&gt;✅ Configured &lt;code&gt;~/.ssh/config&lt;/code&gt; with host aliases&lt;/li&gt;
&lt;li&gt;✅ Connected to both servers using just an alias name&lt;/li&gt;
&lt;li&gt;✅ Understood &lt;em&gt;why&lt;/em&gt; permissions like &lt;code&gt;0600&lt;/code&gt; and &lt;code&gt;0700&lt;/code&gt; are non-negotiable&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I'm building this feature as part of GSoC 2026. If you're interested in the full design, check out the &lt;a href="https://github.com/debezium/debezium-design-documents/pull/45#top" rel="noopener noreferrer"&gt;DDD-41 design document&lt;/a&gt;. Feedback and questions are always welcome!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ssh</category>
      <category>opensource</category>
      <category>docker</category>
      <category>beginners</category>
    </item>
    <item>
      <title>GSoC Progress (Weeks 1–4) at MIT App Inventor — Building a Responsive GWT Interface</title>
      <dc:creator>divyanshu_Kumar</dc:creator>
      <pubDate>Sun, 29 Jun 2025 19:04:56 +0000</pubDate>
      <link>https://dev.to/d1vyanshukumar/gsoc-progress-weeks-1-4-at-mit-app-inventor-building-a-responsive-gwt-interface-2fa4</link>
      <guid>https://dev.to/d1vyanshukumar/gsoc-progress-weeks-1-4-at-mit-app-inventor-building-a-responsive-gwt-interface-2fa4</guid>
      <description>&lt;p&gt;So far, GSoC has been an incredibly rewarding journey. Building mobile‑friendly UIs with GWT UiBinder and Java has pushed me to solve real‑world challenges—and every obstacle has taught me something new. In this post, I’ll share my progress through Week 4 and the key lessons I’ve learned along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Week&amp;nbsp;1: Planning &amp;amp; Mentor Review
&lt;/h2&gt;

&lt;p&gt;During the first week, I dove into the existing codebase to understand its structure and began envisioning how the mobile UI components would connect. I sketched out my implementation plan and discussed it in detail with my mentor, outlining the key integration points and data flows. Although this period coincided with my semester exams (so I couldn’t code as much as I’d hoped), I came away with a clear roadmap for building and wiring up the mobile interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 2: Building a Standalone Mobile UI with GWT &amp;amp; UiBinder
&lt;/h2&gt;

&lt;p&gt;This week, I began building a standalone mobile UI module from the ground up instead of retrofitting the existing desktop code with media queries—an approach that often leads to cluttered styles and unintended breakages. By leveraging GWT and UiBinder (the same stack behind MIT App Inventor), we write type‑safe Java that compiles to optimised JavaScript, enjoy robust IDE support and compile‑time checks, and define our layouts declaratively in XML for a clean separation of structure and behaviour. This lets us reuse the desktop’s core business logic without touching its styles, avoid the cross‑browser quirks common in pure JavaScript frameworks, and benefit from GWT‑RPC’s efficient client–server communication and strong unit‑testing tools. In doing so, we achieve a maintainable, scalable codebase with clear separation of concerns—desktop versus mobile—and a mentor‑approved foundation for a robust mobile interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges
&lt;/h2&gt;

&lt;p&gt;Breakpoint-Driven Desktop‑to‑Mobile Interface Swap&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In our GWT app, we define a numeric breakpoint—say, &lt;strong&gt;768px&lt;/strong&gt;—that separates desktop from mobile. On initial load, we call&lt;/p&gt;


&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClientWidth&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;to read the current viewport width. If &lt;code&gt;width &amp;lt; BREAKPOINT&lt;/code&gt;, we inject our &lt;strong&gt;mobile CSS bundle&lt;/strong&gt; (for example, &lt;code&gt;mobileStyles.ensureInjected()&lt;/code&gt;), which swaps in touch‑friendly layouts and hides desktop‑only elements. Otherwise, we keep the default desktop styles.&lt;/p&gt;

&lt;p&gt;To handle user‑driven resizes—when someone drags the browser window or rotates their device—we register a resize handler:&lt;/p&gt;


&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Window&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addResizeHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;newWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getWidth&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BREAKPOINT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mobileStyles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureInjected&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;desktopStyles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureInjected&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This listener means we don’t just detect the breakpoint once; we continuously watch for the window crossing that threshold, dynamically swapping style bundles as needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getClientWidth()&lt;/code&gt;&lt;/strong&gt; gives a one‑time measurement on load, ensuring your UI starts in the right mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;addResizeHandler()&lt;/code&gt;&lt;/strong&gt; lets you react to changes—critical for desktop users who resize or tablets that rotate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS bundles&lt;/strong&gt; in GWT let you package and inject only the rules you need (mobile vs. desktop), keeping your CSS lean and avoiding messy overrides in a single stylesheet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Together, these APIs provide a type‑safe, maintainable way to implement true adaptive behaviour—rather than bolting on media queries to an existing stylesheet, you cleanly separate and inject the appropriate styles at runtime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Week&amp;nbsp;3: Begin TopToolbar and TopPanel Development Following Successful Mobile Interface Binding
&lt;/h2&gt;

&lt;p&gt;This week, I began developing the TopPanel—specifically the TopToolbar—after successfully binding our mobile interface. Inheriting all of the desktop styles initially prevented any toolbar changes from appearing on mobile; instead, the app continued to render the parent‑class TopPanel.&lt;/p&gt;

&lt;p&gt;During troubleshooting, my mentor, Susan, guided me to a second culprit in the build menu configuration: if the &lt;code&gt;buildDropDown&lt;/code&gt; items aren’t explicitly defined, the template silently falls back to the Classic layout. To work around this, she suggested mirroring those menu entries in the mobile menu (and simply hiding the original build menu) so the template can load correctly.&lt;/p&gt;

&lt;p&gt;Digging deeper, we found the underlying XML parsers enforce a strict mapping between custom tags and Java classes. These parsers handle our drop‑down buttons without extra Java or CSS, but they’re not very feature‑rich—labels aren’t even supported yet. Although we could refine them later, ensuring every inherited component tag appears in UiBinder was enough to restore the mobile styling for now.&lt;/p&gt;

&lt;p&gt;Although the popup‑based approach I initially built might still work, Susan and I opted for a single menu built on our existing framework to guarantee stability. Solving these nested inheritance and parser quirks taught me a valuable lesson about GWT’s markup requirements and reinforced the importance of aligning UiBinder XML with component hierarchies. Despite the week’s frustrations, Week&amp;nbsp;3 turned into a highly educational deep dive into GWT’s theming and templating mechanics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Week 4: Implementing Hamburger Menu &amp;amp; PopupPanel via DisclosurePanel
&lt;/h2&gt;

&lt;p&gt;This week’s focus was on replacing our dropdown build menu with a responsive hamburger menu. When tapped, it now opens a PopupPanel containing the full menu and submenus, structured with GWT’s &lt;code&gt;DisclosurePanel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;DisclosurePanel&lt;/code&gt; provides a collapsible header and content area that expands when clicked—perfect for nested submenus. Within our mobile UI, each header acts as a section title (e.g., Build, Settings), and the content panel holds related submenu items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I chose DisclosurePanel + PopupPanel:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PopupPanel&lt;/code&gt; overlays the rest of the UI and supports auto-hide behaviour and backdrop glass.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DisclosurePanel&lt;/code&gt; nested inside the popup lets users expand and collapse sub-categories smoothly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Implementation summary:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tapping the hamburger icon opens a vertical menu panel within a &lt;code&gt;PopupPanel&lt;/code&gt;, positioned just next to the icon.&lt;/li&gt;
&lt;li&gt;Inside the &lt;code&gt;PopupPanel&lt;/code&gt;’s UiBinder template, each menu section is wrapped in a &lt;code&gt;&amp;lt;g:DisclosurePanel&amp;gt;&lt;/code&gt;, allowing GWT to link these sections to corresponding Java event handlers.&lt;/li&gt;
&lt;li&gt;Here’s an example of the UiBinder markup:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ai:DropDownButton&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Settings"&lt;/span&gt;
                   &lt;span class="na"&gt;styleName=&lt;/span&gt;&lt;span class="s"&gt;"ode-TopPanelButton"&lt;/span&gt;
                   &lt;span class="na"&gt;ui:field=&lt;/span&gt;&lt;span class="s"&gt;"settingsDropDown"&lt;/span&gt;
                   &lt;span class="na"&gt;align=&lt;/span&gt;&lt;span class="s"&gt;"left"&lt;/span&gt;
                   &lt;span class="na"&gt;icon=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt;
                   &lt;span class="na"&gt;caption=&lt;/span&gt;&lt;span class="s"&gt;"{messages.menuButton}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ai:DropDownButton&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;g:VerticalPanel&lt;/span&gt; &lt;span class="na"&gt;ui:field=&lt;/span&gt;&lt;span class="s"&gt;"menuContent"&lt;/span&gt; &lt;span class="na"&gt;visible=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="na"&gt;spacing=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Projects Section --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;g:DisclosurePanel&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;g:header&lt;/span&gt; &lt;span class="na"&gt;styleName=&lt;/span&gt;&lt;span class="s"&gt;"mobile-SectionHeader"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Projects&lt;span class="nt"&gt;&amp;lt;/g:header&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;g:VerticalPanel&lt;/span&gt; &lt;span class="na"&gt;styleName=&lt;/span&gt;&lt;span class="s"&gt;"mobile-SectionPanel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;g:Button&lt;/span&gt; &lt;span class="na"&gt;ui:field=&lt;/span&gt;&lt;span class="s"&gt;"myProjectsButton"&lt;/span&gt;
                &lt;span class="na"&gt;text=&lt;/span&gt;&lt;span class="s"&gt;"{messages.projectMenuItem}"&lt;/span&gt;
                &lt;span class="na"&gt;visible=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;g:Button&lt;/span&gt; &lt;span class="na"&gt;ui:field=&lt;/span&gt;&lt;span class="s"&gt;"newButton"&lt;/span&gt;
                &lt;span class="na"&gt;text=&lt;/span&gt;&lt;span class="s"&gt;"{messages.newProjectMenuItem}"&lt;/span&gt;
                &lt;span class="na"&gt;visible=&lt;/span&gt;&lt;span class="s"&gt;"{hasWriteAccess}"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;g:Button&lt;/span&gt; &lt;span class="na"&gt;ui:field=&lt;/span&gt;&lt;span class="s"&gt;"importProjectButton"&lt;/span&gt;
                &lt;span class="na"&gt;text=&lt;/span&gt;&lt;span class="s"&gt;"{messages.importProjectMenuItem}"&lt;/span&gt;
                &lt;span class="na"&gt;visible=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

 &lt;span class="c"&gt;&amp;lt;!-- Other subMenu Items here --&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;/g:VerticalPanel&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/g:DisclosurePanel&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Additional sections here --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/g:VerticalPanel&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;settingsDropDown.addClickHandler(new ClickHandler() {
      @Override
      public void onClick(ClickEvent event) {
        LOG.warning("Hamburger menu clicked");
        menuPopup.setWidget(menuContent);
        menuPopup.center();
        menuContent.setVisible(true);
      }
    });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;What changed and why it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clarified that the vertical menu is shown inside a &lt;code&gt;PopupPanel&lt;/code&gt; when the hamburger is tapped.&lt;/li&gt;
&lt;li&gt;Specified that each section uses &lt;code&gt;DisclosurePanel&lt;/code&gt; to enable collapsible behavior and backend event handling.&lt;/li&gt;
&lt;li&gt;Improved markup readability by aligning attributes and showing how GWT binds UI to Java logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Resources I relied on this week:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.gwtproject.org/javadoc/latest/com/google/gwt/user/client/ui/DisclosurePanel.html" rel="noopener noreferrer"&gt;GWT Javadoc for &lt;code&gt;DisclosurePanel&lt;/code&gt; usage, constructor options, and CSS styling&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Week 4 delivered not just a functional hamburger menu, but also strengthened my understanding of GWT’s composable widgets. It’s been a gratifying step toward a more intuitive mobile experience.&lt;/p&gt;

&lt;p&gt;Here is my Pr Link:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mit-cml/appinventor-sources/pull/3491" rel="noopener noreferrer"&gt;https://github.com/mit-cml/appinventor-sources/pull/3491&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thank you for reading to the end!&lt;/strong&gt; I hope you found this post insightful and full of useful takeaways. I’ll be back soon with Week 5’s update—so stay tuned. Until then, keep coding and keep exploring!&lt;/p&gt;

</description>
      <category>java</category>
      <category>gwt</category>
      <category>opensource</category>
      <category>gsoc</category>
    </item>
  </channel>
</rss>
