<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged ansible</title>
<link>https://coderlegion.com/tag/ansible</link>
<description>Powered by Question2Answer</description>
<item>
<title>⚙️From 4 Hours to 3 Minutes: Automating GCP Ingress Provisioning with Ansible</title>
<link>https://coderlegion.com/32728/from-4-hours-to-3-minutes-automating-gcp-ingress-provisioning-with-ansible</link>
<description>&lt;h4 id=&quot;context&quot;&gt;Context&lt;/h4&gt;
&lt;p&gt;Every time a new client was onboarded, their web application had to be deployed and exposed to the internet.&lt;/p&gt;
&lt;p&gt;As part of the deployment, the infrastructure required several configuration steps across different services before the application was ready to receive traffic.&lt;/p&gt;
&lt;h4 id=&quot;problem&quot;&gt;Problem&lt;/h4&gt;
&lt;p&gt;Although the deployment workflow was always the same, every new client required repeating the exact same operational tasks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating GCP firewall rules&lt;/li&gt;
&lt;li&gt;Retrieving the VM's public IP&lt;/li&gt;
&lt;li&gt;Creating or updating Cloudflare DNS records&lt;/li&gt;
&lt;li&gt;Installing and configuring Nginx&lt;/li&gt;
&lt;li&gt;Generating Let's Encrypt SSL certificates&lt;/li&gt;
&lt;li&gt;Validating the routing and HTTPS configuration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since these tasks were performed manually for every client, the provisioning process typically took around four hours to complete. Besides being time-consuming, it also increased the likelihood of configuration mistakes and inconsistencies.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;solution&quot;&gt;Solution&lt;/h4&gt;
&lt;p&gt;After repeating the same workflow several times, I realized the process was entirely deterministic. The only inputs that changed between deployments were values such as the domain name, backend ports, and service account.&lt;/p&gt;
&lt;p&gt;Instead of spending hours repeating the same sequence of tasks, I automated the entire workflow with Ansible and integrated it into our CI/CD pipeline.&lt;/p&gt;
&lt;h4 id=&quot;how-the-workflow-looks&quot;&gt;How the workflow looks&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;GitLab CI/CD
      │
      ▼
Run Ansible Playbook
      │
      ▼
Create GCP Firewall Rule
      │
      ▼
Retrieve VM Public IP
      │
      ▼
Create / Update Cloudflare DNS Record
      │
      ▼
Install Nginx
      │
      ▼
Generate HTTP Configuration
      │
      ▼
Request Let's Encrypt Certificate
      │
      ▼
Generate HTTPS Configuration
      │
      ▼
Reload Nginx
      │
      ▼
Application Available via HTTPS
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A single CI/CD job orchestrates the entire workflow. Ansible handles the infrastructure configuration, DNS updates, reverse proxy setup, and SSL provisioning, making the deployment consistent and repeatable.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;implementation-highlights&quot;&gt;Implementation Highlights&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Google Cloud Firewall Automation – Creates HTTP/HTTPS firewall rules through the GCP API.&lt;/li&gt;
&lt;li&gt;Automatic Public IP Discovery – Retrieves the VM's external IP from the GCP metadata service instead of requiring manual input.&lt;/li&gt;
&lt;li&gt;Cloudflare DNS Management – Updates DNS records automatically as part of the deployment.&lt;/li&gt;
&lt;li&gt;Dynamic Nginx Configuration – Uses Jinja2 templates to generate HTTP and HTTPS configurations.&lt;/li&gt;
&lt;li&gt;Automated SSL Provisioning – Obtains and configures Let's Encrypt certificates without manual intervention.&lt;/li&gt;
&lt;li&gt;CI/CD Integration – The entire process runs automatically through GitLab CI.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;results&quot;&gt;Results&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Before                                     &lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;~4 hours to provision and configure        &lt;/td&gt;&lt;td&gt;~3 minutes through CI/CD&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Multiple manual steps                      &lt;/td&gt;&lt;td&gt;Fully automated workflow&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Manual DNS, firewall and SSL configuration &lt;/td&gt;&lt;td&gt;Managed by Ansible&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Repetitive onboarding process              &lt;/td&gt;&lt;td&gt;Consistent and repeatable deployment&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Beyond the time savings, every deployment now follows the exact same process.&lt;/p&gt;
&lt;p&gt;Instead of spending hours repeating the same steps, I simply trigger the pipeline and let the automation do the work.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;check-the-code&quot;&gt;Check the code&lt;/h4&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ja-lourenco/ansible-gcp-ingress-proxy&quot;&gt;https://github.com/ja-lourenco/ansible-gcp-ingress-proxy&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32728/from-4-hours-to-3-minutes-automating-gcp-ingress-provisioning-with-ansible</guid>
<pubDate>Tue, 27 Oct 2026 16:36:09 +0000</pubDate>
</item>
<item>
<title>Why win_updates Alone Isn't Enough for Production Windows Patching — My AWX Approach</title>
<link>https://coderlegion.com/30130/why-winupdates-alone-isnt-enough-for-production-windows-patching-my-awx-approach</link>
<description>&lt;p&gt;Most Windows patching guides show you a basic win_updates task and call it done. That works fine in a lab. But when you're managing 30 production servers, you need something that tells you what's going to happen before it happens, handles double reboots gracefully, auto-detects WSUS, and leaves a proper audit trail.&lt;/p&gt;
&lt;p&gt;This is the playbook I actually run at work through AWX — four phases, one server at a time, with a human approval checkpoint before anything gets touched.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pre-check&lt;/strong&gt; — OS build, pending reboots, disk space, WSUS detection, update scan&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Patch&lt;/strong&gt; — two-pass patching with automatic reboot handling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Post-check&lt;/strong&gt; — OS build verification, uptime, installed hotfixes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validate&lt;/strong&gt; — build comparison before vs after, final ping test&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each phase runs as a separate AWX job template using Ansible tags — so you're in full control of what runs and when. We patch one server at a time (&lt;code&gt;serial: 1&lt;/code&gt;) so a failure on one host never cascades to the rest of the fleet.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-architecture-how-it-runs-in-awx&quot;&gt;The Architecture — How It Runs in AWX&lt;/h3&gt;
&lt;p&gt;Before looking at the playbook itself, here's how I've wired this up in AWX:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;AWX Workflow: Windows Patching
│
├── Job Template 1: Pre-Check (tags: precheck)
│ └── [On Success]
├── Job Template 2: Patch (tags: patch)
│ └── [On Success]
├── Job Template 3: Post-Check (tags: postcheck)
│ └── [On Success]
└── Job Template 4: Validate (tags: validate)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each job template runs the same playbook (&lt;code&gt;windows_patching_with_clear_logs.yaml&lt;/code&gt;) but with a different tag. This gives you full visibility in AWX — you can see exactly which phase passed or failed, and stop the workflow at any point before moving to the next phase.&lt;/p&gt;
&lt;p&gt;In our environment, all 30 servers are in the same AWX inventory. The playbook uses &lt;code&gt;serial: 1&lt;/code&gt; which means AWX runs through all four phases on server 1 before moving to server 2. One server at a time, fully controlled.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-playbook-variables&quot;&gt;The Playbook Variables&lt;/h3&gt;
&lt;p&gt;At the top of the playbook I define the key variables that control the patching behaviour:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;vars:
  patch_categories:
    - SecurityUpdates
    - CriticalUpdates
    - UpdateRollups
  windowslogs_dir: &quot;windowslogs/windows-patching&quot;
  patch_passes: 2
  upd_search_log: &quot;C:\\Windows\\Temp\\ans_upd_search.log&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;patch_categories&lt;/code&gt;&lt;/strong&gt; — the three update types I install. I deliberately exclude FeaturePacks and DefinitionUpdates to keep patching focused on security and stability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;windowslogs_dir&lt;/code&gt;&lt;/strong&gt; — where log files get saved on the Ansible controller, not on the Windows servers. Every host gets its own timestamped log file so you have a full audit trail.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;patch_passes&lt;/code&gt;&lt;/strong&gt; — set to 2 because Windows often needs a second pass after a reboot to pick up remaining updates, especially Cumulative Updates and Servicing Stack updates.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;pre-tasks-setting-up-log-files&quot;&gt;Pre-Tasks — Setting Up Log Files&lt;/h3&gt;
&lt;p&gt;Before any tasks run, I set up timestamped log file paths on the controller:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;pre_tasks:
  - name: Build run timestamp on controller (for unique filenames)
    delegate_to: localhost
    run_once: true
    set_fact:
      date_str: &quot;{{ lookup('ansible.builtin.pipe', 'date +%Y%m%d_%H%M%S') }}&quot;
    tags: [always]

  - name: Ensure windowslogs dir exists on controller
    delegate_to: localhost
    run_once: true
    ansible.builtin.file:
      path: &quot;{{ windowslogs_dir }}&quot;
      state: directory
      mode: &quot;0755&quot;
    tags: [always]

  - name: Build per-host log file paths (on controller)
    set_fact:
      precheck_file: &quot;{{ windowslogs_dir }}/{{ inventory_hostname }}_precheck_{{ date_str }}.txt&quot;
      postcheck_file: &quot;{{ windowslogs_dir }}/{{ inventory_hostname }}_postcheck_{{ date_str }}.txt&quot;
      updates_file:  &quot;{{ windowslogs_dir }}/{{ inventory_hostname }}_updates_{{ date_str }}.txt&quot;
    tags: [always]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;tags: [always]&lt;/code&gt; means these tasks run regardless of which tag you pass — log paths are always set up first.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-1-pre-check&quot;&gt;Phase 1 — Pre-Check&lt;/h3&gt;
&lt;p&gt;This is the most important phase. I never patch a server without running pre-checks first.&lt;/p&gt;
&lt;h4 id=&quot;1-get-the-full-os-build-including-ubr&quot;&gt;1. Get the Full OS Build Including UBR&lt;/h4&gt;
&lt;p&gt;The standard OS build number isn't enough. Windows uses an &lt;strong&gt;Update Build Revision (UBR)&lt;/strong&gt; on top of the major build — so two servers with the same &lt;code&gt;OsBuildNumber&lt;/code&gt; might be at different patch levels:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Get OS version/build before patch (full build)
  ansible.windows.win_shell: |
    $ci = Get-ComputerInfo
    $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    $ubr = $cv.UBR
    $major = $ci.OsBuildNumber
    $full = '{0}.{1}' -f $major, $ubr
    [PSCustomObject]@{
      OsVersion     = $ci.OsVersion
      OsBuildNumber = $major
      UBR           = $ubr
      FullBuild     = $full
      Caption       = $ci.OsName
    } | ConvertTo-Json -Compress | Out-String
  register: pre_os
  check_mode: no
  changed_when: false
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I then validate this output — if it comes back as UNKNOWN, the playbook fails immediately:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Validate pre_os output
  ansible.builtin.assert:
    that:
      - pre_os_version     != 'UNKNOWN'
      - pre_os_build_major != 'UNKNOWN'
      - pre_os_build_full  != 'UNKNOWN'
    fail_msg: &quot;Failed to parse OS info on {{ inventory_hostname }}.&quot;
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-check-for-pending-reboots&quot;&gt;2. Check for Pending Reboots&lt;/h4&gt;
&lt;p&gt;If a server already has a pending reboot, patching it is risky. I check two registry keys:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Check pending reboot before patch (PowerShell fallback)
  ansible.windows.win_shell: |
    $paths = @(
      &quot;HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending&quot;,
      &quot;HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired&quot;
    )
    $pending = $false
    foreach ($p in $paths) { if (Test-Path $p) { $pending = $true } }
    if ($pending) { &quot;true&quot; } else { &quot;false&quot; }
  register: pre_reboot_raw
  check_mode: no
  changed_when: false
  tags: [precheck]

- name: Stop if a reboot is already pending
  ansible.builtin.fail:
    msg: &quot;Reboot is already pending on {{ inventory_hostname }}. Reboot first, then patch.&quot;
  when: pre_reboot.reboot_pending | default(false)
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-check-disk-space&quot;&gt;3. Check Disk Space&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Check free space on system drive (bytes)
  ansible.windows.win_shell: |
    $sys = ([System.Environment]::GetEnvironmentVariable('SystemDrive'))
    Get-PSDrive -Name $sys.TrimEnd(':') |
    Select-Object Used,Free | ConvertTo-Json -Compress | Out-String
  register: pre_disk
  check_mode: no
  changed_when: false
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-wsus-auto-detection&quot;&gt;4. WSUS Auto-Detection&lt;/h4&gt;
&lt;p&gt;One of the things I'm most proud of in this playbook. It auto-detects whether WSUS is configured and adjusts accordingly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Detect WSUS policy (is WSUS enforced?)
  ansible.windows.win_shell: |
    $wuKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
    $auKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU'
    $obj = [ordered]@{
      UseWUServer = $null
      WUServer    = $null
      WUStatus    = $null
    }
    if (Test-Path $auKey) {
      $obj.UseWUServer = (Get-ItemProperty $auKey -ErrorAction SilentlyContinue).UseWUServer
    }
    if (Test-Path $wuKey) {
      $p = Get-ItemProperty $wuKey -ErrorAction SilentlyContinue
      $obj.WUServer  = $p.WUServer
      $obj.WUStatus  = $p.WUStatusServer
    }
    $obj | ConvertTo-Json -Compress | Out-String
  register: wsus_raw
  check_mode: no
  changed_when: false
  tags: [precheck]

- name: Decide update server selection (WSUS vs Microsoft Update)
  set_fact:
    update_server_selection: &amp;gt;-
      {{
        'managed_server'
        if (wsus_use_server == 1 and (wsus_url | length &amp;gt; 0))
        else 'windows_update'
      }}
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;5-async-update-scan&quot;&gt;5. Async Update Scan&lt;/h4&gt;
&lt;p&gt;Before touching anything, I do a full scan without downloading or installing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Kick off update search (no download/install)
  ansible.windows.win_updates:
    state: searched
    category_names: &quot;{{ patch_categories }}&quot;
    server_selection: &quot;{{ update_server_selection }}&quot;
    log_path: &quot;{{ upd_search_log }}&quot;
  register: pre_search_async
  async: 7200
  poll: 0
  tags: [precheck]

- name: Wait up to 20 minutes for search to finish
  async_status:
    jid: &quot;{{ pre_search_async.ansible_job_id }}&quot;
  register: pre_search
  until: pre_search.finished
  retries: 40
  delay: 30
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;6-predict-patching-complexity&quot;&gt;6. Predict Patching Complexity&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Predict complexity (long run / multi-reboot heuristic)
  set_fact:
    candidate_updates: &quot;{{ pre_search.updates | default([]) }}&quot;
    will_be_long: &amp;gt;-
      {{ (pre_search.updates | default([])
          | selectattr('title','search','Cumulative Update|Servicing Stack|Feature Update')
          | list | length) &amp;gt; 0 }}
    predicted_passes: &quot;{{ 2 if (pre_search.updates | default([])
          | selectattr('title','search','Cumulative Update|Servicing Stack')
          | list | length) &amp;gt; 0 else 1 }}&quot;
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;7-save-pre-check-log&quot;&gt;7. Save Pre-Check Log&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Save pre-check summary (controller)
  delegate_to: localhost
  ansible.builtin.copy:
    dest: &quot;{{ precheck_file }}&quot;
    mode: &quot;0644&quot;
    content: |
      Host: {{ inventory_hostname }}
      Caption: {{ pre_os_caption }}
      OS Version: {{ pre_os_version }}
      OS Build (major): {{ pre_os_build_major }}
      OS Build (full) : {{ pre_os_build_full }}
      Last Boot: {{ pre_uptime.stdout | trim }}
      Pending Reboot: {{ pre_reboot.reboot_pending | default(false) }}
      Disk (bytes): {{ pre_disk.stdout | trim }}
      Candidate Updates: {{ (candidate_updates | length) if candidate_updates is defined else 0 }}
      Predicted Long/Multi-Reboot: {{ will_be_long | default(false) }}
      Suggested Passes: {{ predicted_passes | default(1) }}
      Updates:
      {% for u in candidate_updates %}
        - {{ u.title if (u is mapping and 'title' in u) else (u|string) }}
      {% endfor %}
  tags: [precheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-2-patch&quot;&gt;Phase 2 — Patch&lt;/h3&gt;
&lt;p&gt;Once pre-checks pass and I've reviewed the log, I launch the patch job template in AWX. Two passes — first installs and reboots, second catches anything remaining:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: PATCH PASS 1 - install updates
  ansible.windows.win_updates:
    category_names: &quot;{{ patch_categories }}&quot;
    server_selection: &quot;{{ update_server_selection }}&quot;
    reboot: yes
    reboot_timeout: 3600
  register: patch_pass1
  tags: [never, patch]

- name: Wait for host to return (pass 1)
  wait_for_connection:
    delay: 20
    timeout: 1200
  when: patch_pass1.reboot_required | default(false)
  tags: [never, patch]

- name: PATCH PASS 2 - install remaining updates
  ansible.windows.win_updates:
    category_names: &quot;{{ patch_categories }}&quot;
    server_selection: &quot;{{ update_server_selection }}&quot;
    reboot: yes
    reboot_timeout: 3600
  register: patch_pass2
  when: patch_passes|int &amp;gt;= 2
  tags: [never, patch]

- name: Wait for host to return (pass 2)
  wait_for_connection:
    delay: 20
    timeout: 1200
  when:
    - patch_passes|int &amp;gt;= 2
    - patch_pass2.reboot_required | default(false)
  tags: [never, patch]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tags: [never, patch]&lt;/code&gt; — the &lt;code&gt;never&lt;/code&gt; means these tasks are skipped unless you explicitly pass &lt;code&gt;--tags patch&lt;/code&gt;. This prevents accidental patching.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-3-post-check&quot;&gt;Phase 3 — Post-Check&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Get OS version/build postchecks (full build)
  ansible.windows.win_shell: |
    $ci = Get-ComputerInfo
    $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    $ubr = $cv.UBR
    $major = $ci.OsBuildNumber
    $full = '{0}.{1}' -f $major, $ubr
    [PSCustomObject]@{
      OsVersion     = $ci.OsVersion
      OsBuildNumber = $major
      UBR           = $ubr
      FullBuild     = $full
    } | ConvertTo-Json -Compress | Out-String
  register: post_os
  tags: [never, postcheck]

- name: Get installed hotfixes postchecks
  ansible.windows.win_shell: &amp;gt;
    Get-HotFix | Sort-Object InstalledOn |
    Select-Object HotFixID, InstalledOn |
    Format-Table -HideTableHeaders | Out-String
  register: hotfixes
  tags: [never, postcheck]

- name: Save post-check summary (controller)
  delegate_to: localhost
  ansible.builtin.copy:
    dest: &quot;{{ windowslogs_dir }}/{{ inventory_hostname }}_postcheck_{{ date_str }}.txt&quot;
    mode: &quot;0644&quot;
    content: |
      Host: {{ inventory_hostname }}
      OS Version (post): {{ post_os_version }}
      OS Build (post, major): {{ post_os_build_major }}
      OS Build (post, full) : {{ post_os_build_full }}
      Last Boot (post): {{ post_uptime.stdout | trim }}
      Installed Updates:
      {{ hotfixes.stdout | trim }}
  tags: [never, postcheck]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-4-validate&quot;&gt;Phase 4 — Validate&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;- name: Summarize patch result
  ansible.builtin.debug:
    msg: |
      Patch Result for {{ inventory_hostname }}:
      - Categories: {{ patch_categories | join(', ') }}
      - Pass1: installed={{ (patch_pass1.updates | default([])) | length }},
               reboot={{ patch_pass1.reboot_required | default(false) }}
      - Pass2: installed={{ (patch_pass2.updates | default([])) | length
               if (patch_pass2 is defined) else 0 }},
               reboot={{ patch_pass2.reboot_required | default(false)
               if (patch_pass2 is defined) else false }}
      - OS Build Before (full): {{ pre_os_build_full | default('N/A') }}
      - OS Build After  (full): {{ post_os_build_full | default('N/A') }}
      - Status: {{
          'PATCH SUCCESSFUL - OS BUILD CHANGED'
          if (pre_os_build_full is defined and post_os_build_full is defined
              and pre_os_build_full != post_os_build_full)
          else 'NO BUILD CHANGE - verify installed hotfixes or CU cadence'
        }}
  tags: [never, validate]

- name: Final ping test
  ansible.windows.win_ping:
  tags: [never, validate]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setting-this-up-in-awx&quot;&gt;Setting This Up in AWX&lt;/h3&gt;
&lt;h4 id=&quot;step-1-create-four-job-templates&quot;&gt;Step 1 — Create Four Job Templates&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Job Template &lt;/th&gt;&lt;th&gt;Playbook &lt;/th&gt;&lt;th&gt;Job Tags&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Windows Pre-Check &lt;/td&gt;&lt;td&gt;windows_patching_with_clear_logs.yaml &lt;/td&gt;&lt;td&gt;precheck&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Windows Patch &lt;/td&gt;&lt;td&gt;windows_patching_with_clear_logs.yaml &lt;/td&gt;&lt;td&gt;patch&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Windows Post-Check &lt;/td&gt;&lt;td&gt;windows_patching_with_clear_logs.yaml &lt;/td&gt;&lt;td&gt;postcheck&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Windows Validate &lt;/td&gt;&lt;td&gt;windows_patching_with_clear_logs.yaml &lt;/td&gt;&lt;td&gt;validate&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;step-2-create-the-workflow&quot;&gt;Step 2 — Create the Workflow&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;AWX UI → &lt;strong&gt;Templates&lt;/strong&gt; → &lt;strong&gt;Add&lt;/strong&gt; → &lt;strong&gt;Add Workflow Template&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;Windows Patching Workflow&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt; → &lt;strong&gt;Workflow Visualiser&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Chain the four job templates together using &lt;strong&gt;On Success&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pre-Check → [On Success] → Patch → [On Success] → Post-Check → [On Success] → Validate&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;step-3-add-a-manual-approval-node&quot;&gt;Step 3 — Add a Manual Approval Node&lt;/h4&gt;
&lt;p&gt;Between Pre-Check and Patch I add a manual &lt;strong&gt;Approval&lt;/strong&gt; node so someone reviews the pre-check logs before patching starts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the Workflow Visualiser → between Pre-Check and Patch&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+&lt;/strong&gt; → select &lt;strong&gt;Approval&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;Review Pre-Check Logs Before Patching&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Timeout: 4 hours&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the human checkpoint before anything touches production.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;errors-i-hit-in-production&quot;&gt;Errors I Hit in Production&lt;/h3&gt;
&lt;h4 id=&quot;error-1-winrm-drops-after-reboot-and-never-comes-back&quot;&gt;Error 1 — WinRM Drops After Reboot and Never Comes Back&lt;/h4&gt;
&lt;p&gt;Fix — increase the &lt;code&gt;wait_for_connection&lt;/code&gt; timeout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;wait_for_connection:
  delay: 20
  timeout: 1800
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;error-2-wsus-returning-no-updates&quot;&gt;Error 2 — WSUS Returning No Updates&lt;/h4&gt;
&lt;p&gt;WSUS hadn't approved the latest Cumulative Update. Fix — override at runtime in AWX by passing as an extra variable:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;update_server_selection: windows_update
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;error-3-ubr-not-changing-after-patching&quot;&gt;Error 3 — UBR Not Changing After Patching&lt;/h4&gt;
&lt;p&gt;Check &lt;code&gt;Get-HotFix&lt;/code&gt; output in the post-check log. If new KBs are listed even though the UBR didn't change, only smaller updates installed — worth investigating before signing off.&lt;/p&gt;
&lt;h4 id=&quot;error-4-async-update-scan-timing-out&quot;&gt;Error 4 — Async Update Scan Timing Out&lt;/h4&gt;
&lt;p&gt;Fix — increase async timeout and retries:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;async: 7200
retries: 80
delay: 30
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-gives-you&quot;&gt;What This Gives You&lt;/h3&gt;
&lt;p&gt;For 30 production Windows servers, this approach gives you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;full audit trail&lt;/strong&gt; — pre and post check logs per server per patching run&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safety gates&lt;/strong&gt; — pending reboot detection, disk space checks and manual approval before patching starts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intelligence&lt;/strong&gt; — WSUS auto-detection means the same playbook works across different network segments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visibility in AWX&lt;/strong&gt; — each phase is a separate node in the workflow so you can see exactly where something failed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confidence&lt;/strong&gt; — build comparison before and after tells you definitively whether the Cumulative Update actually applied&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's Next&lt;/h3&gt;
&lt;p&gt;The next improvement I'm planning is adding email notifications to this workflow — so when patching fails on any of the 30 servers at 2AM, I get an alert immediately rather than finding out in the morning.&lt;/p&gt;
&lt;p&gt;Drop your questions in the comments — happy to help!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;— Sireesha&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30130/why-winupdates-alone-isnt-enough-for-production-windows-patching-my-awx-approach</guid>
<pubDate>Wed, 07 Oct 2026 11:41:31 +0000</pubDate>
</item>
<item>
<title>From Shell Scripts to Scalable Automation: Why Ansible?</title>
<link>https://coderlegion.com/24294/from-shell-scripts-to-scalable-automation-why-ansible</link>
<description>&lt;p&gt;Most of us start our automation journey with 𝗦𝗵𝗲𝗹𝗹 𝗦𝗰𝗿𝗶𝗽𝘁𝗶𝗻𝗴—and that's exactly how it should be.&lt;/p&gt;
&lt;p&gt;It's simple, powerful, and a great way to automate repetitive tasks.&lt;/p&gt;
&lt;p&gt;But as your infrastructure grows, you'll eventually hit its limits.&lt;/p&gt;
&lt;p&gt;🚨 Imagine this...&lt;br&gt;
You need to configure 100 or even 1,000 servers.&lt;br&gt;
Yes, you can SSH into each server and execute a shell script.&lt;br&gt;
But managing automation at this scale becomes challenging.&lt;/p&gt;
&lt;p&gt;⚠️ Challenges with Shell Scripting&lt;br&gt;
❌ No Idempotency — Running the same script multiple times can repeat tasks and cause errors unless you add custom checks.&lt;/p&gt;
&lt;p&gt;❌ Limited Error Handling — You must manually validate exit codes ($?) and handle failures yourself.&lt;/p&gt;
&lt;p&gt;❌ Complex at Scale — Large scripts quickly become difficult to maintain with retries, logging, validations, and conditions.&lt;/p&gt;
&lt;p&gt;❌ Limited Scalability — Shell executes sequentially by default. Parallel execution is possible using &amp;amp; and wait, but managing jobs, failures, and thousands of SSH connections adds complexity.&lt;/p&gt;
&lt;p&gt;❌ No Built-in Inventory — You manage server IPs and SSH logic yourself.&lt;/p&gt;
&lt;p&gt;❌ Distribution-Specific Logic — Different Linux distributions often require different commands.&lt;/p&gt;
&lt;p&gt;✅ So, why Ansible?&lt;/p&gt;
&lt;p&gt;Ansible is an open-source, agentless automation tool that follows a push-based architecture.&lt;/p&gt;
&lt;p&gt;It connects to remote servers over SSH, pushes tasks from the control node, and executes them without installing any agent on the managed nodes.&lt;/p&gt;
&lt;p&gt;Key Advantages&lt;br&gt;
✅ Idempotent by default&lt;/p&gt;
&lt;p&gt;✅ Inventory Management&lt;/p&gt;
&lt;p&gt;✅ Parallel Execution&lt;/p&gt;
&lt;p&gt;✅ Better Error Handling&lt;/p&gt;
&lt;p&gt;✅ Configuration Management&lt;/p&gt;
&lt;p&gt;✅ Rich Modules&lt;/p&gt;
&lt;p&gt;✅ Readable &amp;amp; Scalable YAML Playbooks&lt;/p&gt;
&lt;p&gt;💡 Shell Scripting is still a fundamental skill for every DevOps engineer.&lt;/p&gt;
&lt;p&gt;But as your infrastructure grows, writing more shell code isn't always the answer.&lt;/p&gt;
&lt;p&gt;Sometimes, choosing the right tool is.&lt;/p&gt;
&lt;p&gt;The question isn't whether Shell Scripting is good or bad.&lt;/p&gt;
&lt;p&gt;The real question is: At what point does your automation stop scaling?&lt;/p&gt;
&lt;p&gt;Follow my profile for more practical DevOps content and real-world automation insights. 🚀&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24294/from-shell-scripts-to-scalable-automation-why-ansible</guid>
<pubDate>Sat, 08 Aug 2026 17:39:49 +0000</pubDate>
</item>
<item>
<title>Coolify: The Complete Manual Setup Guide (For When the Auto-Install Script Won't Cut It)</title>
<link>https://coderlegion.com/23307/coolify-the-complete-manual-setup-guide-for-when-the-auto-install-script-wont-cut-it</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=jlUzYm6W-bI&quot;&gt;&lt;img src=&quot;https://img.youtube.com/vi/VIDEO_ID_HERE/0.jpg&quot; alt=&quot;Watch the video&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Coolify's one-line install script is great — until it isn't. Right now it officially supports Ubuntu 20.04, 22.04, and 24.04 LTS. If you're running anything newer (Ubuntu's already on 26.04 LTS), the script won't work and you're left doing it manually.&lt;/p&gt;
&lt;p&gt;This is that manual walkthrough — set up in the order that fits a security-first VPS workflow rather than the order Coolify's own docs use. If you've been following along with the Ansible playbooks from earlier in this series, this picks up right where that left off.&lt;/p&gt;
&lt;h3 id=&quot;minimum-hardware-requirements&quot;&gt;Minimum Hardware Requirements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU:&lt;/strong&gt; 2 cores&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory:&lt;/strong&gt; 2 GB RAM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; 30 GB free&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Coolify can technically run below this, but it's not recommended.&lt;/p&gt;
&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;Before touching Coolify itself, you'll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSH access to your VPS&lt;/li&gt;
&lt;li&gt;CURL installed&lt;/li&gt;
&lt;li&gt;Docker Engine installed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're reconnecting to a server you've rebuilt or re-provisioned, clear the old fingerprint first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -f '/home/your-path/.ssh/known_hosts' -R 'your-vps-ip'
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;installing-ssh&quot;&gt;Installing SSH&lt;/h4&gt;
&lt;p&gt;If you followed the earlier videos in this series, OpenSSH is already installed. If not:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install -y openssh-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Confirm it's running and check which port it's listening on (you should have already changed this from the default 22 — see the VPS security video):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status ssh
sudo ss -tulpn | grep ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;installing-curl&quot;&gt;Installing CURL&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install -y curl
curl --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;ca-certificates&lt;/code&gt; also get installed as part of the &lt;code&gt;apt-update&lt;/code&gt; Ansible playbook below, so this may already be handled.&lt;/p&gt;
&lt;h3 id=&quot;running-the-first-ansible-playbook&quot;&gt;Running the First Ansible Playbook&lt;/h3&gt;
&lt;p&gt;Connect Ansible to the VPS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ANSIBLE_HOST_KEY_CHECKING=FALSE ansible -i ./inventory/hosts vpsDemo -m ping --user root --ask-pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then run the update playbook:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible-playbook ./playbooks/apt-update.yml --user root -e &quot;ansible_port=22&quot; --ask-pass --ask-become-pass -i ./inventory/hosts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you haven't set up the Ansible inventory and playbooks from the earlier videos, do that first — this guide assumes they're already in place.&lt;/p&gt;
&lt;h3 id=&quot;installing-docker-engine&quot;&gt;Installing Docker Engine&lt;/h3&gt;
&lt;p&gt;Remove any conflicting packages first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc | cut -f1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add Docker's official GPG key and repo:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources &amp;lt;&amp;lt;EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release &amp;amp;&amp;amp; echo &quot;${UBUNTU_CODENAME:-$VERSION_CODENAME}&quot;)
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verify it worked:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl status docker
sudo docker run hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;creating-a-non-root-admin-user&quot;&gt;Creating a Non-Root Admin User&lt;/h3&gt;
&lt;p&gt;Coolify's own docs assume you're using the root account. Since root login is disabled as part of the security hardening earlier in this series, we create a dedicated user with passwordless sudo instead.&lt;/p&gt;
&lt;p&gt;Run the second playbook to create that user:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible-playbook ./playbooks/basic-secure.yml --user root -e &quot;ansible_port=22&quot; --ask-pass --ask-become-pass -i ./inventory/hosts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then set up that user's SSH directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /home/your-sudo-user/.ssh
sudo touch /home/your-sudo-user/.ssh/authorized_keys
sudo chmod 700 /home/your-sudo-user/.ssh
sudo chmod 600 /home/your-sudo-user/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;setting-up-coolify-s-directory-structure&quot;&gt;Setting Up Coolify's Directory Structure&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance}
sudo mkdir -p /data/coolify/ssh/{keys,mux}
sudo mkdir -p /data/coolify/proxy/dynamic
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;generating-and-adding-an-ssh-key&quot;&gt;Generating and Adding an SSH Key&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo ssh-keygen -f /data/coolify/ssh/keys/*Emails are not allowed*rnal -t ed25519 -N '' -C your-sudo-user@coolify
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the public key to the authorized_keys file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /data/coolify/ssh/keys/*Emails are not allowed* | sudo tee -a /home/your-sudo-user/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;pulling-coolify-s-configuration-files&quot;&gt;Pulling Coolify's Configuration Files&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo curl -fsSL https://cdn.coollabs.io/coolify/docker-compose.yml -o /data/coolify/source/docker-compose.yml
sudo curl -fsSL https://cdn.coollabs.io/coolify/docker-compose.prod.yml -o /data/coolify/source/docker-compose.prod.yml
sudo curl -fsSL https://cdn.coollabs.io/coolify/.env.production -o /data/coolify/source/.env
sudo curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;generating-secure-environment-values&quot;&gt;Generating Secure Environment Values&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Only run these once, on first install.&lt;/strong&gt; Changing them later can break Coolify. Back them up somewhere safe.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo sed -i &quot;s|APP_ID=.*|APP_ID=$(openssl rand -hex 16)|g&quot; /data/coolify/source/.env
sudo sed -i &quot;s|APP_KEY=.*|APP_KEY=base64:$(openssl rand -base64 32)|g&quot; /data/coolify/source/.env
sudo sed -i &quot;s|DB_PASSWORD=.*|DB_PASSWORD=$(openssl rand -base64 32)|g&quot; /data/coolify/source/.env
sudo sed -i &quot;s|REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -base64 32)|g&quot; /data/coolify/source/.env
sudo sed -i &quot;s|PUSHER_APP_ID=.*|PUSHER_APP_ID=$(openssl rand -hex 32)|g&quot; /data/coolify/source/.env
sudo sed -i &quot;s|PUSHER_APP_KEY=.*|PUSHER_APP_KEY=$(openssl rand -hex 32)|g&quot; /data/coolify/source/.env
sudo sed -i &quot;s|PUSHER_APP_SECRET=.*|PUSHER_APP_SECRET=$(openssl rand -hex 32)|g&quot; /data/coolify/source/.env
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;permissions-and-docker-setup&quot;&gt;Permissions and Docker Setup&lt;/h3&gt;
&lt;p&gt;Set correct ownership and permissions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo chown -R 9999:root /data/coolify
sudo find /data/coolify -type d -exec chmod 755 {} \;
sudo find /data/coolify -type f -exec chmod 644 {} \;
sudo chown -R your-sudo-user:your-sudo-user /home/your-sudo-user/.ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create the Docker network Coolify expects:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo docker network create --attachable coolify
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add your user to the Docker group:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo usermod -aG docker your-sudo-user
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;starting-coolify&quot;&gt;Starting Coolify&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --pull always --remove-orphans --force-recreate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Confirm it's running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo docker ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then visit &lt;code&gt;http://YOUR-SERVER-IP:8000&lt;/code&gt; in your browser.&lt;/p&gt;
&lt;h3 id=&quot;setup-your-ssl-certificates&quot;&gt;Setup your SSL Certificates&lt;/h3&gt;
&lt;p&gt;Coolify's reverse-proxy Traefik does this under the hood automatically.&lt;/p&gt;
&lt;p&gt;In your DNS records add two A records.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A     @     your-vps-ip-address        14400
A     *     your-vps-ip-address        14400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In your Coolify dashboard go to settings in the left side column. In the input box marked URL type your https subdomain for Coolify there.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://coolify.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Traefik will automatically apply a Let's Encrypt certificate to your Coolify subdomain.&lt;/p&gt;
&lt;h3 id=&quot;disable-port-8000&quot;&gt;Disable Port 8000&lt;/h3&gt;
&lt;p&gt;Disable port 8000 on your VPS. If your hosting provider allows you to configure a firewall from your dashboard, disable it from there. To disable port 8000, simply write rules that allow the ports you want and block everything else.&lt;/p&gt;
&lt;h3 id=&quot;wrap-up&quot;&gt;Wrap-Up&lt;/h3&gt;
&lt;p&gt;That's a full manual Coolify install on a hardened, non-root VPS — no automated script required. From here, Coolify handles the rest: connecting your Git repos, setting up applications, and managing deployments.&lt;/p&gt;
&lt;p&gt;If you hit issues with the automated script on a newer Ubuntu release, this manual path should get you unblocked. Questions or corrections welcome in the comments.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23307/coolify-the-complete-manual-setup-guide-for-when-the-auto-install-script-wont-cut-it</guid>
<pubDate>Thu, 23 Jul 2026 05:15:31 +0000</pubDate>
</item>
<item>
<title>I automated my homelab so hard it started naming servers after Tarantino characters</title>
<link>https://coderlegion.com/22912/i-automated-my-homelab-so-hard-it-started-naming-servers-after-tarantino-characters</link>
<description>&lt;hr&gt;
&lt;p&gt;title: I automated my homelab so hard it started naming servers after Tarantino characters&lt;br&gt;
published: true&lt;br&gt;
description: What started as avoiding the Proxmox UI turned into OpenTofu, Ansible, dynamic inventory, and a bunch of bugs I didn't expect.&lt;br&gt;
tags: devops, opentofu, ansible, homelab&lt;/p&gt;
&lt;h3 id=&quot;cover-image-https-raw-githubusercontent-com-ayushverma8-homelab-main-docs-blog-cover-png&quot;&gt;cover_image: &lt;a rel=&quot;nofollow&quot; href=&quot;https://raw.githubusercontent.com/Ayushverma8/homelab/main/docs/blog-cover.png&quot;&gt;https://raw.githubusercontent.com/Ayushverma8/homelab/main/docs/blog-cover.png&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This started because I was lazy.&lt;/p&gt;
&lt;p&gt;Every time I needed a new container on my Proxmox server, I'd open the web UI, click Create CT, pick a template, type a hostname, set the specs, configure networking, hit create, wait, then SSH in and install the same packages I always install. Fifteen minutes of clicking for something I do regularly.&lt;/p&gt;
&lt;p&gt;So one evening I wrote a &lt;code&gt;main.tf&lt;/code&gt; to automate it. That should have been the end of it. It wasn't.&lt;/p&gt;
&lt;h3 id=&quot;the-first-version-was-embarrassing&quot;&gt;The first version was embarrassing&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;provider &quot;proxmox&quot; {
  pm_api_url = &quot;https://192.168.2.250:8006/api2/json&quot;
}

resource &quot;proxmox_lxc&quot; &quot;test_container&quot; {
  target_node  = &quot;proxmox&quot;
  hostname     = &quot;test-lxc&quot;
  cores        = 1
  memory       = 512
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;API URL hardcoded in source. Specs as magic numbers. Want a second container? Copy-paste the whole block. It worked, but anyone looking at this in a code review would have things to say.&lt;/p&gt;
&lt;h3 id=&quot;then-i-kept-going&quot;&gt;Then I kept going&lt;/h3&gt;
&lt;p&gt;I split it into proper files. Moved every value into variables with defaults. Added validation so &lt;code&gt;cores = 0&lt;/code&gt; or &lt;code&gt;disk_size = &quot;big&quot;&lt;/code&gt; would fail at plan time instead of blowing up on the Proxmox API with some cryptic error.&lt;/p&gt;
&lt;p&gt;Then I switched to a map with &lt;code&gt;for_each&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;lxc_containers = {
  testbox = {}
  docker  = { cores = 4, memory = 4096, disk_size = &quot;64G&quot; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;testbox = {}&lt;/code&gt; gives you a container with sane defaults. Override only what's different. Adding a container is one line, not a resource block.&lt;/p&gt;
&lt;h3 id=&quot;the-tarantino-thing&quot;&gt;The Tarantino thing&lt;/h3&gt;
&lt;p&gt;I needed hostnames. Didn't want &lt;code&gt;container-1&lt;/code&gt;, &lt;code&gt;container-2&lt;/code&gt;. That's boring and you can never remember which is which.&lt;/p&gt;
&lt;p&gt;So I threw 30 Tarantino characters into a list and used &lt;code&gt;random_shuffle&lt;/code&gt; to pick one per container, plus a &lt;code&gt;random_id&lt;/code&gt; hex suffix to avoid collisions. Now when I &lt;code&gt;tofu apply&lt;/code&gt;, my container might come back as &lt;code&gt;beatrix-a3f1b2&lt;/code&gt; or &lt;code&gt;django-7c9e04&lt;/code&gt;. My Proxmox dashboard looks ridiculous and I love it.&lt;/p&gt;
&lt;h3 id=&quot;ssh-keys-without-managing-ssh-keys&quot;&gt;SSH keys without managing SSH keys&lt;/h3&gt;
&lt;p&gt;Instead of copying keys around, the config pulls them from GitHub:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;data &quot;http&quot; &quot;github_keys&quot; {
  url = &quot;https://github.com/${var.github_username}.keys&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every apply fetches the latest keys. Add a new key to GitHub, run the pipeline, done. No &lt;code&gt;ssh-copy-id&lt;/code&gt;, no Ansible vault for public keys.&lt;/p&gt;
&lt;h3 id=&quot;where-it-got-hard-ansible-dhcp&quot;&gt;Where it got hard: Ansible + DHCP&lt;/h3&gt;
&lt;p&gt;After creating containers, I wanted Ansible to configure them. Install packages, set timezone, run updates. But the containers use DHCP, so at plan time there's no IP. The output just says &lt;code&gt;ip = &quot;dhcp&quot;&lt;/code&gt;. Not helpful.&lt;/p&gt;
&lt;p&gt;I wrote a script that calls the Proxmox API after boot to get the actual IP:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ip=$(curl -sk &quot;${PM_API_URL}/nodes/${node}/lxc/${vmid}/interfaces&quot; \
  -H &quot;Authorization: PVEAPIToken=...&quot; \
  | jq -r '.data[] | select(.name==&quot;eth0&quot;) | .[&quot;ip-addresses&quot;][]
    | select(.[&quot;ip-address-type&quot;]==&quot;inet&quot;) | .[&quot;ip-address&quot;]')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Generates an Ansible inventory on the fly. No hardcoded IPs.&lt;/p&gt;
&lt;p&gt;Sounds clean, right? It wasn't.&lt;/p&gt;
&lt;h3 id=&quot;everything-that-went-wrong&quot;&gt;Everything that went wrong&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Wrong field name.&lt;/strong&gt; The script was producing &lt;code&gt;ansible_host: null&lt;/code&gt; for every container. Ansible tried to resolve the hostname, which obviously didn't exist in DNS, and died. I spent 15 minutes curling the Proxmox API manually before I noticed: the field is &lt;code&gt;ip-address&lt;/code&gt;, not &lt;code&gt;inet-addr&lt;/code&gt;. Every example I'd seen online used &lt;code&gt;inet-addr&lt;/code&gt;. One jq filter change and it worked.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Race condition I didn't think about.&lt;/strong&gt; The Makefile ran &lt;code&gt;tofu apply&lt;/code&gt; then immediately generated inventory. But the container had literally just booted. DHCP hadn't assigned an IP yet. The inventory came back empty, Ansible ran against zero hosts, and the whole thing &quot;succeeded&quot; with nothing actually provisioned. No errors. No warnings. Just silence.&lt;/p&gt;
&lt;p&gt;I added retry logic. The script now polls the API every 5 seconds, up to a minute, waiting for all containers to have IPs before generating inventory. Not elegant, but it works every time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lifecycle rules that created a deadlock.&lt;/strong&gt; I wanted to protect important containers from accidental &lt;code&gt;tofu destroy&lt;/code&gt;. OpenTofu has &lt;code&gt;prevent_destroy&lt;/code&gt; for this, but it has to be a literal &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; in the code. You can't pass a variable. So I ended up with two resource blocks: &lt;code&gt;proxmox_lxc.protected&lt;/code&gt; and &lt;code&gt;proxmox_lxc.unprotected&lt;/code&gt;, and a flag in tfvars to control which pool a container landed in.&lt;/p&gt;
&lt;p&gt;This worked until I tried to flip the flag. Changing a container from protected to unprotected meant OpenTofu wanted to destroy the protected one and recreate it in the unprotected block. But &lt;code&gt;prevent_destroy&lt;/code&gt; blocked the destroy. Complete deadlock. The only fix was &lt;code&gt;tofu state mv&lt;/code&gt; to manually move resources between blocks.&lt;/p&gt;
&lt;p&gt;I spent an hour on this before deciding it was absurd for a homelab. Deleted the whole thing. One resource block, no flags, no state gymnastics.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Cloudflare Tunnel rabbit hole.&lt;/strong&gt; I thought it'd be cool to SSH into my containers from anywhere. Set up Cloudflare Tunnel, created DNS records, installed &lt;code&gt;cloudflared&lt;/code&gt; on a container. Everything looked right. Then: &lt;code&gt;remote error: tls: handshake failure&lt;/code&gt;. Spent another hour debugging tunnel configs, ingress rules, Access policies.&lt;/p&gt;
&lt;p&gt;Then I asked myself when I last needed to SSH into my homelab from outside my house. The answer was never. Deleted the entire Cloudflare integration. Over-engineering is a real disease.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CI that passed locally and failed in Actions.&lt;/strong&gt; &lt;code&gt;ansible-lint&lt;/code&gt; was green on my machine because I had &lt;code&gt;community.general&lt;/code&gt; installed from an old project. The GitHub Actions runner didn't have it. Pipeline went red. Then lint complained my variables didn't have a role prefix. Then it complained about a missing newline at the end of a file. Three separate commits to fix linting issues. Should have run it locally first.&lt;/p&gt;
&lt;h3 id=&quot;where-it-ended-up&quot;&gt;Where it ended up&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;make up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the whole thing. Creates containers, waits for IPs, provisions with Ansible, prints this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;============================================
Infrastructure up and provisioned
============================================

SSH into your containers:
  ssh root@192.168.2.85
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;make down&lt;/code&gt; tears it all down. &lt;code&gt;make up&lt;/code&gt; again and you get a fresh set. Different Tarantino name, same packages, same config. Ansible is idempotent: first run &lt;code&gt;changed=4&lt;/code&gt;, second run &lt;code&gt;changed=0&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-skip-next-time&quot;&gt;What I'd skip next time&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;prevent_destroy&lt;/code&gt; experiment. Not worth the state complexity for a homelab.&lt;/p&gt;
&lt;p&gt;Cloudflare Tunnel. Cool idea, but I didn't need it. Building for a problem you don't have is how features become baggage.&lt;/p&gt;
&lt;p&gt;And I'd run the linter locally before pushing. Not after CI tells me what I already should have caught.&lt;/p&gt;
&lt;h3 id=&quot;the-repo&quot;&gt;The repo&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ayushverma8/homelab&quot;&gt;github.com/ayushverma8/homelab&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Clone it, fill in your Proxmox credentials, &lt;code&gt;make up&lt;/code&gt;. Let me know what Tarantino character you get.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22912/i-automated-my-homelab-so-hard-it-started-naming-servers-after-tarantino-characters</guid>
<pubDate>Sat, 18 Jul 2026 00:47:17 +0000</pubDate>
</item>
<item>
<title>The Ansible Playbook that will Harden Your VPS</title>
<link>https://coderlegion.com/22679/the-ansible-playbook-that-will-harden-your-vps</link>
<description>&lt;h2 id=&quot;ansible-playbooks-automate-vps-hardening-user-creation-amp-removal&quot;&gt;Ansible Playbooks — Automate VPS Hardening, User Creation &amp;amp; Removal&lt;/h2&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=vl8IW8F1mxA&quot;&gt;Link to youtube Video on these playbooks&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;In this post I'm sharing 3 Ansible playbooks I use to manage my VPS servers. I won't go into great detail on tasks here — if you're new to Ansible check out my previous post and video first.&lt;/p&gt;
&lt;p&gt;The three playbooks covered today:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;basic-secure.yml&lt;/code&gt; — automate VPS hardening&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add-vps-user.yml&lt;/code&gt; — semi-automated user creation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remove-vps-user.yml&lt;/code&gt; — completely remove a user and their privileges&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;📚 &lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.ansible.com&quot;&gt;Ansible Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.ansible.com/ansible/latest/collections/index.html&quot;&gt;Ansible Collection Index&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-basics&quot;&gt;The Basics&lt;/h3&gt;
&lt;p&gt;A task is broken up into four parts:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Part &lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;A plain-text description of what the task does&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Collection&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;The Ansible content bundle the module belongs to&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Module&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;The tool that executes the action&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Parameters&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;The specific options passed to the module&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;playbook-1-basic-secure-yml&quot;&gt;Playbook 1 — basic-secure.yml&lt;/h3&gt;
&lt;p&gt;This playbook fully hardens a fresh Ubuntu VPS in a single command. It prompts you for a custom admin username, generates a random 16-character password, configures UFW, installs Fail2Ban, and moves SSH to port 2222.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
- name: Harden Ubuntu VPS Security Configuration
  hosts: vpsDemo
  gather_facts: true
  vars_prompt:
    - name: 'custom_admin_user'
      prompt: 'Enter the custom username for your main administrator account'
      private: false
  tasks:
    # 1. GENERATE RANDOM PASSWORD
    - name: Generate random password
      set_fact:
        new_admin_password: &quot;{{ lookup('password', '/dev/null chars=ascii_letters,digits,hexdigits length=16') }}&quot;

    # 2. CREATE SUDO USER
    - name: Ensure the custom admin user exists
      ansible.builtin.user:
        name: '{{ custom_admin_user }}'
        password: &quot;{{ new_admin_password | password_hash('sha512') }}&quot;
        shell: /bin/bash
        state: present
        groups: sudo
        append: true

    - name: Allow the admin user to use sudo without a password prompt
      ansible.builtin.copy:
        content: &quot;{{ custom_admin_user }} ALL=(ALL) NOPASSWD:ALL\n&quot;
        dest: '/etc/sudoers.d/{{ custom_admin_user }}'
        mode: '0440'
        validate: /usr/sbin/visudo -cf %s

    # 3. CONFIGURE UFW FIREWALL
    - name: Reset UFW to default settings
      community.general.ufw:
        state: reset

    - name: Set UFW default policies to deny incoming
      community.general.ufw:
        policy: deny
        direction: incoming

    - name: Open Port 80 (HTTP)
      community.general.ufw:
        rule: allow
        port: '80'
        proto: tcp

    - name: Open Port 443 (HTTPS)
      community.general.ufw:
        rule: allow
        port: '443'
        proto: tcp

    - name: Open Custom SSH Port 2222
      community.general.ufw:
        rule: allow
        port: '2222'
        proto: tcp

    - name: Enable UFW Firewall
      community.general.ufw:
        state: enabled

    # 4. INSTALL FAIL2BAN
    - name: Install Fail2Ban
      ansible.builtin.apt:
        name: fail2ban
        state: present
        update_cache: true

    - name: Ensure Fail2Ban is running and enabled on boot
      ansible.builtin.service:
        name: fail2ban
        state: started
        enabled: true

    # 5. HARDEN SSH
    - name: Configure SSH to use custom port 2222
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?Port\s'
        line: 'Port 2222'
        state: present

    - name: Disable Root SSH Login
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^#?PermitRootLogin\s'
        line: 'PermitRootLogin no'
        state: present

    # 6. FIX SYSTEMD SSH SOCKET
    - name: Create systemd override directory for SSH socket
      ansible.builtin.file:
        path: /etc/systemd/system/ssh.socket.d
        state: directory
        mode: '0755'

    - name: Write dual-stack IPv4/IPv6 socket configuration
      ansible.builtin.copy:
        dest: /etc/systemd/system/ssh.socket.d/listen.conf
        mode: '0644'
        content: |
          [Socket]
          ListenStream=
          ListenStream=0.0.0.0:2222
          ListenStream=[::]:2222

    - name: Reload systemd daemon
      ansible.builtin.systemd_service:
        daemon_reload: true

    - name: Restart SSH socket
      ansible.builtin.service:
        name: ssh.socket
        state: restarted

    - name: Restart SSH service
      ansible.builtin.service:
        name: ssh
        state: restarted

    # 7. DISPLAY CREDENTIALS
    - name: Display your new credentials (SAVE THESE IMMEDIATELY)
      ansible.builtin.debug:
        msg:
          - '========================================================'
          - 'NEW SUDO USERNAME: {{ custom_admin_user }}'
          - 'NEW PASSWORD: {{ new_admin_password }}'
          - 'CUSTOM SSH PORT: 2222'
          - '========================================================'
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Save the displayed credentials immediately&lt;/strong&gt; — the generated password is only shown once.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;playbook-2-add-vps-user-yml&quot;&gt;Playbook 2 — add-vps-user.yml&lt;/h3&gt;
&lt;p&gt;Creates a new sudo user with a randomly generated secure password and prints the credentials to your screen.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
- name: Universal Semi-Automated User Creation Script
  hosts: vpsDemo
  gather_facts: false
  become: true
  vars_prompt:
    - name: 'custom_admin_user'
      prompt: 'Enter the custom username for this new administrator account'
      private: false
  tasks:
    - name: Generate random secure password
      set_fact:
        new_random_password: &quot;{{ lookup('password', '/dev/null chars=ascii_letters,digits length=16') }}&quot;

    - name: Ensure the new user account exists
      ansible.builtin.user:
        name: '{{ custom_admin_user }}'
        password: &quot;{{ new_random_password | password_hash('sha512') }}&quot;
        shell: /bin/bash
        state: present
        groups: sudo
        append: true

    - name: Allow the new user to use sudo without a password prompt
      ansible.builtin.copy:
        content: &quot;{{ custom_admin_user }} ALL=(ALL) NOPASSWD:ALL\n&quot;
        dest: '/etc/sudoers.d/{{ custom_admin_user }}'
        mode: '0440'
        validate: /usr/sbin/visudo -cf %s

    - name: Display New User Credentials
      ansible.builtin.debug:
        msg:
          - '========================================================'
          - 'NEW ADMINISTRATIVE ACCOUNT CREATED SUCCESSFULLY!'
          - 'USERNAME: {{ custom_admin_user }}'
          - 'PASSWORD: {{ new_random_password }}'
          - '========================================================'
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;playbook-3-remove-vps-user-yml&quot;&gt;Playbook 3 — remove-vps-user.yml&lt;/h3&gt;
&lt;p&gt;Completely purges a user account, their home directory, mail spool, and sudo privileges from the server.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
- name: Universal User and Privilege Removal Script
  hosts: all
  gather_facts: false
  become: true
  vars_prompt:
    - name: 'user_to_delete'
      prompt: 'Enter the exact username you want to COMPLETELY delete'
      private: false
  tasks:
    - name: Delete the user's custom sudoers configuration file
      ansible.builtin.file:
        path: '/etc/sudoers.d/{{ user_to_delete }}'
        state: absent

    - name: Remove the user account and purge their files
      ansible.builtin.user:
        name: '{{ user_to_delete }}'
        state: absent
        remove: true   # deletes home directory and mail spool
        force: true    # kills any active processes owned by the user
        ignore_errors: true

    - name: Display Removal Confirmation
      ansible.builtin.debug:
        msg:
          - '========================================================'
          - &quot;SUCCESS: Account '{{ user_to_delete }}' and their sudo privileges&quot;
          - 'have been completely purged from the server.'
          - '========================================================'
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;I hope these playbooks are useful — grab them, adapt them to your environment and save yourself hours of repetitive manual work. I'll be sharing more playbooks as I build them out.&lt;/p&gt;
&lt;p&gt;Blessings. 🙏&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Looking for developer templates built on TanStack, Directus, and Tailwind CSS?&lt;/em&gt;&lt;br&gt;
&lt;em&gt;🛒 &lt;a rel=&quot;nofollow&quot; href=&quot;https://northernrangedigital.lemonsqueezy.com/&quot;&gt;northernrangedigital.lemonsqueezy.com&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;🌐 &lt;a rel=&quot;nofollow&quot; href=&quot;https://northernrangedigital.com&quot;&gt;northernrangedigital.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22679/the-ansible-playbook-that-will-harden-your-vps</guid>
<pubDate>Tue, 14 Jul 2026 21:06:17 +0000</pubDate>
</item>
<item>
<title>Ansible Installation and Configuration On Ubuntu</title>
<link>https://coderlegion.com/22079/ansible-installation-and-configuration-on-ubuntu</link>
<description>&lt;h2 id=&quot;introduction-1&quot;&gt;Introduction&lt;/h2&gt;
&lt;h4 id=&quot;what-is-ansible-&quot;&gt;What is Ansible?&lt;/h4&gt;
&lt;p&gt;Ansible is an automation language that can describe any IT environment, whether home-lab or large-scale infrastructure. It is easy to learn, beautiful code that reads like clear documentation.&lt;/p&gt;
&lt;p&gt;There are many repetitive tasks that we may perform on a server, a VM, or a VPS. The tasks can range from setting up SSH keys to disabling the root user to firewall configs. If you manage multiple servers daily and you have to do the same configuration over and over, Ansible can automate the process, allowing you to increase your productivity.&lt;/p&gt;
&lt;p&gt;It requires installing Ansible on the Control Node and just having Python 3 installed on the Managed Node.&lt;/p&gt;
&lt;h4 id=&quot;what-is-the-control-node-&quot;&gt;What is the Control Node?&lt;/h4&gt;
&lt;p&gt;It's simply the system that Ansible is installed on that controls the remote machines.&lt;/p&gt;
&lt;h4 id=&quot;what-is-the-managed-node-&quot;&gt;What is the Managed Node?&lt;/h4&gt;
&lt;p&gt;A remote system, or host, that Ansible controls. Ansible is agent-less, so you don’t need to install Ansible on the managed node; you simply need Python 3 to be installed.&lt;/p&gt;
&lt;h3 id=&quot;install-ansible-on-ubuntu-control-node-local-machine-running-zorinos-&quot;&gt;Install Ansible on Ubuntu (Control Node—local machine. Running ZorinOS).&lt;/h3&gt;
&lt;p&gt;your-command-prompt:~$ sudo apt update&lt;/p&gt;
&lt;p&gt;your-command-prompt:~$ sudo apt install software-properties-common&lt;/p&gt;
&lt;p&gt;your-command-prompt:~$ sudo add-apt-repository --yes --update ppa:ansible/ansible&lt;/p&gt;
&lt;p&gt;your-command-prompt:~$ sudo apt install ansible&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;  Ensure that Python 3 is installed on your remote server. At this time of writing, Ubuntu 26.04 LTS has Python 3.14.4 installed by default. This is enough to get your remote server running with Ansible.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;create-an-inventory-folder-with-a-host-file-&quot;&gt;Create an inventory folder with a host file.&lt;/h3&gt;
&lt;p&gt;This file contains a list of the remote machines, VMs, or VPSs that you want to control using Ansible.&lt;/p&gt;
&lt;p&gt;For this step you can use a code editor; I will be using Vs Code.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create a folder called Ansible and open it in vs code&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In that folder, create another folder and name it inventory&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inside the inventory folder, create a file named hosts with no extension&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8019618342096067034&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;hosts file&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Start with an attribute; give it whatever name you want.&lt;br&gt;
[servers]&lt;/p&gt;
&lt;p&gt;Then list the servers that you want to manage using their DNS names or their IP addresses.&lt;br&gt;
[servers]&lt;br&gt;
vpsServer ansible_host=10.10.100.45&lt;br&gt;
work-ToRule&lt;br&gt;
10.10.45.62&lt;/p&gt;
&lt;p&gt;We can give our hosts an alias by passing in their IP address to any name we wish to give it. In the list above, I have given the VPS with IP address 10.10.100.45 an alias of vpsServer. (IP address, just an example)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1561479094875298780&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;create-a-folder-named-playbook&quot;&gt;Create a folder named Playbook&lt;/h3&gt;
&lt;p&gt;In this folder we will create the actual yml files that we will use to automate the different processes we want to run on our remote servers. It can be an update, firewall config or install a package.&lt;/p&gt;
&lt;p&gt;Our first playbook will update the Ubuntu server and set the right timezone. I will name it apt-update.yml.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=10832088875540847717&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;examine-the-yml-file-&quot;&gt;Examine the YML file.&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;- hosts: &quot;*&quot;&lt;/strong&gt; — this means all remote hosts will be updated. A single host can be selected by either its alias, DNA name, or IP address.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;become: &lt;/strong&gt;  true—gives Ansible the administrative authority to act as a sudo user.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;serial: 1 &lt;/strong&gt; —In Ansible, serial: 1 tells the automation engine to manage your servers one by one rather than updating all of them at the same time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tasks &lt;/strong&gt;—In Ansible, the tasks section is structured as a list of individual actions. Each task is a YAML block that breaks down into four main parts: the Identity, the Module, the Arguments, and Directives.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- name: &lt;/strong&gt; This is a plain-text description of what the task does and always starts with a dash or hyphen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; The Collection (community.general): &lt;/strong&gt;An Ansible Collection is a standardized packaging format used to bundle and distribute Ansible content, including playbooks, roles, modules, and plugins.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; The Module (timezone): &lt;/strong&gt; This is the specialized tool or command line plugin you want to use. It tells Ansible how to execute the action on the server. Instead of writing raw terminal commands, you use abstract modules like apt (for packages), copy (for files), user (for accounts), or service (for daemons).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Parameters (name: America/Port_of_Spain): &lt;/strong&gt; These are the specific settings, options, or instructions you pass into the module. It defines the exact parameters of your action. The name: parameter is a mandatory argument required by the community.general.timezone module. It specifies the exact geographical timezone identifier you want to apply to the target Linux operating system.&lt;/p&gt;
&lt;p&gt;If you're new to Ansible, you can find a collection of the modules and arguments available @ &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.ansible.com/projects/ansible/latest/collections/index.html&quot;&gt;https://docs.ansible.com/projects/ansible/latest/collections/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4683817883742389333&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;run-playbook&quot;&gt;Run Playbook&lt;/h3&gt;
&lt;p&gt;Connect to the remote host in your terminal.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;your-command-prompt:~$ ANSIBLE_HOST_KEY_CHECKING=FALSE ansible -i ./inventory/hosts vpsServer -m ping --user root --ask-pass&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ANSIBLE_HOST_KEY_CHECKING=FALSE&lt;/strong&gt;&lt;br&gt;
This is a temporary environment variable that changes how Ansible handles security signatures. It tells Ansible to skip looking inside your local ~/.ssh/known_hosts file. It allows you to connect to a fresh or reinstalled VPS instantly without throwing the strict “Host key verification failed” fingerprint error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ansible&lt;/strong&gt;&lt;br&gt;
This invokes the ad-hoc command line engine (used for running single, quick tasks instead of running an entire playbook file)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-i ./inventory/hosts&lt;/strong&gt;&lt;br&gt;
The -i flag stands for Inventory. It tells Ansible exactly where your infrastructure map file is saved. In this case, you are instructing it to bypass any default system paths and read the specific file named hosts located inside your ./inventory/ folder&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;vpsServer&lt;/strong&gt;&lt;br&gt;
This is the specific destination target. Ansible looks into your inventory to find a server or group named exactly vpsServer to run the task against.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-m ping&lt;/strong&gt;&lt;br&gt;
The module flag. It tells Ansible to execute the built-in ping software module. This is a diagnostic module that logs into the server, checks if Python is available, and returns a successful green&quot;pong&quot;if the link is healthy.&lt;/p&gt;
&lt;p&gt;Once you get a green ‘pong’ you can run the next command.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;your-command-prompt:~$ ansible-playbook ./playbooks/apt-update.yml --user root -e “ansible_port=22&quot; --ask-pass --ask-become-pass -i ./inventory/hosts&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ansible-playbook&lt;/strong&gt;&lt;br&gt;
This invokes Ansible's playbook execution engine. Unlike the simple ansible command (which runs just one quick ad-hoc task), ansible-playbook is designed to open an automation script file and run a whole sequence of structured tasks in order.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;./playbooks/apt-update.yml&lt;/strong&gt;&lt;br&gt;
This is the relative file path to the exact automation script you want to run. : You are telling Ansible to open the file named apt-update.yml inside your playbooks folder and execute whatever configuration steps (like package updates or installations) are written inside it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;--user root&lt;/strong&gt;&lt;br&gt;
This asks Ansible to establish the initial SSH network connection using the root account, completely ignoring any other default usernames written inside your configuration files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;-e “ansible_port=22”&lt;/strong&gt;&lt;br&gt;
The -e flag stands for Extra Variables. This dynamically injects a temporary variable into the execution. Here, you are explicitly forcing Ansible to connect over standard Port 22, overriding any custom ports (like 2222) saved in your host files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;--ask-pass&lt;/strong&gt;&lt;br&gt;
Tells Ansible that you are using password authentication instead of an SSH key token. It forces the terminal to prompt you for the root SSH login password when you launch the command.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;--ask-become-pass&lt;/strong&gt;&lt;br&gt;
This tells Ansible to prompt you for the privilege escalation (sudo) password.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note: &lt;/strong&gt; Since you are logging in directly as the absolute system master (root),--ask-become-pass is actually redundant here because root already has absolute power and never needs to escalate further.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;-i ./inventory/hosts&lt;/strong&gt;&lt;br&gt;
The -i flag maps your Inventory. It tells the playbook engine exactly where to look to find your list of servers. You are directing it to read the specific hosts file located inside your ./inventory/ folder.&lt;/p&gt;
&lt;h3 id=&quot;summary-of-the-execution-flow&quot;&gt;Summary of the Execution Flow&lt;/h3&gt;
&lt;p&gt;When you execute this command from your Control Node terminal, Ansible handles it in this sequence:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It opens your./inventory/hostsfile to look up the IP address of the target server.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It stops and asks you to type the SSH password (due to –ask-pass).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It stops and asks you to type the Sudo password (due to –ask-become-pass).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It bypasses port variables, connects directly to Port 22 as root, opens up your apt-update.yml playbook, and begins executing its tasks.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;I want to give a shout-out to Aldo @&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/aldo_cve&quot;&gt;https://dev.to/aldo_cve&lt;/a&gt;. He recommended Ansible in a previous post I published, and I looked it up.&lt;/p&gt;
&lt;p&gt;I found Ansible to be a great addition to my workflow in setting up my backend. I hope you guys enjoyed this walk through. You can look forward to more posts where I publish some of the playbooks that I find useful.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22079/ansible-installation-and-configuration-on-ubuntu</guid>
<pubDate>Wed, 08 Jul 2026 00:14:52 +0000</pubDate>
</item>
<item>
<title>From Zero to Infra: Building a Production-Ready Setup Using Our API &amp;amp;amp;amp;amp; Ansible</title>
<link>https://coderlegion.com/7307/from-zero-to-infra-building-a-production-ready-setup-using-our-api-amp-amp-amp-amp-ansible</link>
<description>&lt;p&gt;&lt;strong&gt;The Story Begins&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One common pattern we’ve observed at Nine is that many of our customers run web applications — think content management systems, web shops, dashboards, or custom APIs. These apps often rely on a tried-and-true combination of infrastructure services: a database to persist data, a cache layer for performance, and a server to run the application logic.&lt;/p&gt;
&lt;p&gt;With that in mind, the setup we chose here reflects that real-world use case. We’re not just launching a VM or showing off an API (ok, maybe a little). But we’re also simulating the foundational components of what you’d find in production:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A PostgreSQL database to store orders, users, and product data&lt;/li&gt;
&lt;li&gt;A Redis key-value store to cache hot data and reduce load times&lt;/li&gt;
&lt;li&gt;A CloudServer VM to run the actual application (e.g. web server,&lt;br&gt;
backend API, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is to show how these can be provisioned and managed easily, using our API and tools, in a way that mirrors your own workload and challenges.&lt;/p&gt;
&lt;p&gt;As the Head of Engineering at &lt;a rel=&quot;nofollow&quot; href=&quot;http://nine.ch&quot;&gt;Nine&lt;/a&gt;, I’ve witnessed our platform evolve, providing developers with powerful tools to automate infrastructure. One of the most potent tools is our public API, giving you the ability to provision and orchestrate your infrastructure programmatically.&lt;/p&gt;
&lt;p&gt;This blog post is not just a tutorial; it’s a story about turning a blank slate into a working, production-like environment. We will build and validate the provisioning of the following infrastructure components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A PostgreSQL database for persistent storage&lt;/li&gt;
&lt;li&gt;A Redis key-value store for high-speed caching&lt;/li&gt;
&lt;li&gt;A CloudServer VM that could host any application you choose&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each step is written not just to instruct but to explain, giving context to every decision and command. This post assumes you’re a developer or DevOps engineer who may not yet be familiar with Nine’s API, Ansible, or our CLI tool &lt;code&gt;nctl&lt;/code&gt;. We’ll guide you from first principles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why Ansible?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before we get into the technical weeds, it’s worth discussing the tool we’ll use to glue everything together: Ansible.&lt;/p&gt;
&lt;p&gt;Ansible is an automation engine used for configuration management, application deployment, and general infrastructure orchestration. What makes it so popular?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Agentless: Unlike some other tools, you don’t need to install anything on your remote servers. Ansible connects over SSH.&lt;/li&gt;
&lt;li&gt;Human-readable YAML syntax: This makes it easy to learn and collaborate on with others. &lt;/li&gt;
&lt;li&gt;Dynamic control flow: You can incorporate    conditions, variables, loops, and even execute raw shell commands or API calls.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We chose Ansible because it’s great for scenarios like ours: where we want to orchestrate not just the creation of infrastructure, but also follow-up steps like configuration and validation.&lt;/p&gt;
&lt;p&gt;You may have heard of OpenTofu or Terraform – these tools excel at declaratively managing infrastructure states. In fact, since we offer a public API, Nine is also fully compatible with Terraform. You can find example configurations in our &lt;a rel=&quot;nofollow&quot; href=&quot;http://github.com/ninech/terraform-examples&quot;&gt;Terraform examples repository&lt;/a&gt;. Ansible, in contrast, is more procedural and event-driven, which suits this use case well where we need more step-by-step control and conditional logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Setting Up Your Toolkit (macOS)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Our journey begins on a MacBook. We’ll install all the tools we need using &lt;a rel=&quot;nofollow&quot; href=&quot;https://brew.sh/&quot;&gt;Homebrew&lt;/a&gt;, the de facto package manager for macOS.&lt;/p&gt;
&lt;p&gt;If you’re on Linux, you can follow the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.ansible.com/projects/ansible/latest/installation_guide/intro_installation.html&quot;&gt;Ansible installation guide&lt;/a&gt; for Linux and install &lt;code&gt;nctl&lt;/code&gt; manually from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ninech/nctl/releases&quot;&gt;our GitHub releases&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For Windows users, we recommend using &lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/windows/wsl/&quot;&gt;WSL2 (Windows Subsystem for Linux)&lt;/a&gt; and then following the Linux instructions above. This allows you to use the same Unix-like tooling and commands without leaving Windows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Install Homebrew&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you don’t have Homebrew yet, install it with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/bin/bash -c &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Homebrew lets us easily install other dependencies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Install Ansible&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew install ansible
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ansible is our primary tool to automate the provisioning, configuration, and validation of infrastructure resources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Install Nine’s CLI tool: &lt;code&gt;nctl&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;brew tap ninech/nine
brew install nctl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;nctl&lt;/code&gt; is Nine’s command-line client. It wraps around our public API and simplifies tasks like creating a VM or database, retrieving secrets, and managing access control. We’ll use it heavily.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authenticate with Nine’s API&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To interact with our API, you’ll need a Bearer token. This token grants authenticated access and is required for both Ansible and &lt;code&gt;nctl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can create a service account and retrieve your token either through Cockpit:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cockpit.nine.ch/en/customer/api%5C_service%5C_accounts/&quot;&gt;https://cockpit.nine.ch/en/customer/api_service_accounts/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Or via the CLI:&lt;/p&gt;
&lt;p&gt;Login with your account&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nctl auth login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We create an apiserviceaccount named &lt;code&gt;cicd&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nctl create apiserviceaccount cicd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And we retrieve the token for the apiserviceaccount named &lt;code&gt;cicd&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nctl get apiserviceaccounts cicd --print-token
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you have your token, export it as an environment variable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export NINE_API_TOKEN=&quot;&amp;lt;your-token&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And don’t forget to export your SSH public key too. We’ll need it to access the VM later:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export SSH_PUBLIC_KEY=&quot;$(cat ~/.ssh/id_ed25519.pub)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;You should always treat your API token like a password. Don’t hardcode it in files.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Project Structure&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To keep things tidy, let’s create a project directory and define our reusable configuration.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir nine-infra &amp;amp;&amp;amp; cd nine-infra
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Group Variables&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ansible allows us to keep variables in one place using group vars. Create the folder with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir group_vars
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s what &lt;code&gt;group_vars/all.yml&lt;/code&gt; could look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# group_vars/all.yml
postgres_name: example-postgres
postgres_version: 15
redis_name: example-redis
vm_name: example-vm
location: nine-cz41
machine_type: nine-small-1
os: ubuntu24.04
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes our playbooks more readable and makes it easier to reuse or update settings. You can of course adjust the values here — such as the OS version, machine type, or service versions — depending on your needs. To discover which values are supported, you can use the documentation we provide for our API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloud VM machine types and locations&lt;/li&gt;
&lt;li&gt;PostgreSQL supported versions and types&lt;/li&gt;
&lt;li&gt;KeyValueStore specs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Provision PostgreSQL&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To provision a managed PostgreSQL instance, we’ll use &lt;code&gt;nctl&lt;/code&gt; within an Ansible playbook. This ensures the database is consistently created with our desired configuration.&lt;/p&gt;
&lt;p&gt;Create the following file and save it as &lt;code&gt;provision_postgres.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# provision_postgres.yml
- name: Provision PostgreSQL
  hosts: localhost
  gather_facts: false
 
  vars_files:
    - group_vars/all.yml
 
  tasks:
    - name: Check if PostgreSQL already exists
      ansible.builtin.shell: &quot;nctl get postgres {{ postgres_name }}&quot;
      changed_when: false
      failed_when: false
      register: pg_exists
 
    - name: Create PostgreSQL
      ansible.builtin.command: &amp;gt;
        nctl create postgres {{ postgres_name }}
        --postgres-version {{ postgres_version }}
        --machine-type nine-db-xs
        --location {{ location }}
        --allowed-cidrs 0.0.0.0/0
        --wait
      changed_when: &quot;pg_result.rc == 0&quot;
      ignore_errors: true
      when: &quot;pg_exists.rc != 0&quot;
      register: pg_result
 
    - name: Get PostgreSQL connection string
      ansible.builtin.command: &quot;nctl get postgres {{ postgres_name }} --print-connection-string&quot;
      register: pg_conn_str
      changed_when: &quot;pg_conn_str.rc != 0&quot;
 
    - name: Set Postgres connection string fact
      ansible.builtin.set_fact:
        postgres_conn_string: &quot;{{ pg_conn_str.stdout }}/postgres&quot;
 
    - name: Debug Postgres connection string
      ansible.builtin.debug:
        var: postgres_conn_string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible-playbook provision_postgres.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be aware: This step may take some time to complete.&lt;/p&gt;
&lt;p&gt;First up is our database. A PostgreSQL instance gives us a durable, reliable data store. It’s a common backend choice for modern apps.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;nctl&lt;/code&gt; to create the instance, and then immediately retrieve its connection string so we can use it later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validation:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use the connection string to confirm the instance is reachable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;psql &amp;lt;postgres_connection_string&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the connection succeeds, you’re ready to move on. Keep in mind that the creation of a DB can take some time since we always provision a dedicated database. Soon we will offer Shared DBs as well which will speed up that scenario by a lot. Also please be aware that we – for simplicity reasons of this blog post – did allow 0.0.0.0/0 to access all our services. I do recommend you to not set up your infrastructure in this way. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Provision Redis&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Redis will act as our key-value store. Just like PostgreSQL, we’ll use an Ansible playbook to automate its provisioning.&lt;/p&gt;
&lt;p&gt;Create the following file as &lt;code&gt;provision_redis.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# provision_redis.yml
- name: Provision Redis
  hosts: localhost
  gather_facts: false
 
  vars_files:
    - group_vars/all.yml
 
  tasks:
    - name: Check if Redis already exists
      ansible.builtin.shell: &quot;nctl get keyvaluestore {{ redis_name }}&quot;
      changed_when: false
      failed_when: false
      register: redis_exists
 
    - name: Create Redis
      ansible.builtin.command: &amp;gt;
        nctl create keyvaluestore {{ redis_name }}
        --memory-size 1Gi
        --location {{ location }}
        --allowed-cidrs 0.0.0.0/0
        --wait
      changed_when: &quot;redis_result.rc == 0&quot;
      ignore_errors: true
      when: &quot;redis_exists.rc != 0&quot;
      register: redis_result
 
    - name: Get Redis connection info
      ansible.builtin.command: nctl get keyvaluestore {{ redis_name }} -o yaml
      register: redis_info
      changed_when: &quot;redis_info.rc != 0&quot;
 
    - name: Parse Redis connection info
      ansible.builtin.set_fact:
        redis_host: &quot;{{ redis_info.stdout | from_yaml | json_query('status.atProvider.connection.address') }}&quot;
        redis_password: &quot;{{ redis_info.stdout | from_yaml | json_query('status.atProvider.connection.password') }}&quot;
 
    - name: Debug Redis details
      ansible.builtin.debug:
        msg: &quot;Redis host: {{ redis_host }}, password: {{ redis_password }}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible-playbook provision_redis.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be aware: This step may take some time to complete.&lt;/p&gt;
&lt;p&gt;Next, we spin up a Redis key-value store. Redis is often used for caching, session storage, rate limiting, or pub/sub. It’s fast and memory-based.&lt;/p&gt;
&lt;p&gt;We again use &lt;code&gt;nctl&lt;/code&gt; to create it, then extract the address and password from the YAML output.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validation:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Try connecting to Redis using the CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis-cli -h &amp;lt;redis_host&amp;gt; -a &amp;lt;redis_password&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the response is &lt;code&gt;PONG&lt;/code&gt;, you’re good to go.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Provision Cloud VM&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With the database services in place, let’s bring up the virtual machine that will act as our compute node. This VM can later be configured to host applications or scripts.&lt;/p&gt;
&lt;p&gt;Save the following playbook as &lt;code&gt;provision_vm.yml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# provision_vm.yml
- name: Provision Cloud VM
  hosts: localhost
  gather_facts: false
 
  vars_files:
    - group_vars/all.yml
 
  tasks:
    - name: Check if VM already exists
      ansible.builtin.shell: &quot;nctl get cloudvirtualmachine {{ vm_name }}&quot;
      changed_when: false
      failed_when: false
      register: vm_exists
 
    - name: Create VM using nctl (wait until ready)
      ansible.builtin.command: &amp;gt;
        nctl create cloudvirtualmachine {{ vm_name }}
        --hostname {{ vm_name }}
        --location {{ location }}
        --machine-type {{ machine_type }}
        --os {{ os }}
        --public-keys &quot;{{ lookup('env', 'SSH_PUBLIC_KEY') }}&quot;
        --wait
      changed_when: &quot;vm_create_output.rc == 0&quot;
      ignore_errors: true
      when: &quot;vm_exists.rc != 0&quot;
      register: vm_create_output
 
    - name: Show VM creation output
      ansible.builtin.debug:
        var: vm_create_output.stdout_lines
      when: &quot;vm_exists.rc != 0 and vm_create_output.rc == 0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then apply it with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible-playbook provision_vm.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Be aware: This step may take some time to complete.&lt;/p&gt;
&lt;p&gt;Now let’s bring up the virtual machine. This will be our compute layer — where your app (or anything else) can run.&lt;/p&gt;
&lt;p&gt;We’ll use &lt;code&gt;nctl&lt;/code&gt; again, this time to provision a VM with your preferred OS image and SSH key.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validation:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Log in via SSH:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh root@&amp;lt;vm_ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This confirms that the server is alive, reachable, and ready for configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Putting it all together&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Wrapping Up&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We’ve now laid the groundwork for a complete cloud-native deployment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A secure, managed PostgreSQL instance&lt;/li&gt;
&lt;li&gt;A fast Redis service ready for caching or queues&lt;/li&gt;
&lt;li&gt;A fully provisioned and reachable virtual machine&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All provisioned step-by-step via Ansible and Nine’s CLI and API — no manual clicking, no guessing.&lt;/p&gt;
&lt;p&gt;From here, you can begin configuring your app, deploying code, and scaling out your architecture – all documented in code and easily accessible.&lt;/p&gt;
&lt;p&gt;Written by Tim Grethler, Head of Engineering at Nine Internet Solutions AG.&lt;/p&gt;
</description>
<category>Cloud Computing</category>
<guid isPermaLink="true">https://coderlegion.com/7307/from-zero-to-infra-building-a-production-ready-setup-using-our-api-amp-amp-amp-amp-ansible</guid>
<pubDate>Thu, 13 Nov 2025 09:47:35 +0000</pubDate>
</item>
<item>
<title>Exploring Ansible Adhoc Commands For Beginners</title>
<link>https://coderlegion.com/1121/exploring-ansible-adhoc-commands-for-beginners</link>
<description>&lt;p&gt;Ansible is a powerful automation tool that simplifies IT tasks such as configuration management, application deployment, and task automation. One of its essential features is the ability to run ad hoc commands. In this blog post, we'll delve into Ansible ad hoc commands, their syntax, and various use cases.&lt;/p&gt;
&lt;h2 id=&quot;what-are-ansible-adhoc-commands-&quot;&gt;What Are Ansible Adhoc Commands?&lt;/h2&gt;
&lt;p&gt;Ansible ad hoc commands are single-line commands used to perform quick tasks without writing a full Ansible playbook. They are convenient for executing simple operations on remote hosts immediately. This makes them ideal for tasks like checking system information, managing files, and installing packages.&lt;/p&gt;
&lt;h2 id=&quot;syntax-of-ansible-adhoc-commands&quot;&gt;Syntax of Ansible Adhoc Commands&lt;/h2&gt;
&lt;p&gt;The basic syntax of an Ansible ad hoc command is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible [host-pattern] -m [module] -a &quot;[module options]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;where, &lt;/p&gt;
&lt;p&gt;ansible: The command to run Ansible.&lt;/p&gt;
&lt;p&gt;[host-pattern]: Specifies the target hosts or groups of hosts.&lt;/p&gt;
&lt;p&gt;-m [module]: Specifies the Ansible module to use.&lt;/p&gt;
&lt;p&gt;-a &quot;[module options]&quot;: Specifies the arguments or options for the module.&lt;/p&gt;
&lt;p&gt;Example:&lt;br&gt;
To ping all hosts in the inventory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible all -m ping
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;common-use-cases-of-ansible-adhoc-commands&quot;&gt;Common Use Cases of Ansible Adhoc Commands&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Checking Connectivity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To check the connectivity between the Ansible control node and the remote hosts, you can use the ping module:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible all -m ping
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will return a &quot;pong&quot; response from each reachable host.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gathering System Information&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The setup module can be used to gather detailed system information from the remote hosts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible all -m setup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command returns a JSON output containing various facts about the target systems.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Managing Files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To create a directory on remote hosts using the file module:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible all -m file -a &quot;path=/tmp/test_dir state=directory&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command creates a directory named test_dir in the /tmp directory on all hosts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Installing Packages&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The yum module (for RHEL-based systems) can be used to install packages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible all -m yum -a &quot;name=httpd state=present&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command installs the httpd package on all hosts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Running Commands&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To execute a shell command on remote hosts, you can use the command module:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible all -m command -a &quot;uname -a&quot;
ansible all -m shell -a &quot;uname -a&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command retrieves the kernel version and system information from all hosts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Copying Files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To copy a file from the Ansible control node to the remote hosts, you can use the copy module:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible all -m copy -a &quot;src=/path/to/source dest=/path/to/destination&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command copies the specified file to the target destination on all hosts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fetch Files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To transfer files from the remote hosts to the Ansible control node, you can use fetch module:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible all -m fetch -a &quot;src=/path/to/source dest=/path/to/destination flat=yes&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will get files from remote machine to local machine.&lt;/p&gt;
&lt;div class=&quot;div-green&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Use Ansible ad hoc commands for quick, one-time tasks instead of writing full playbooks. This is especially useful for troubleshooting, system checks, and immediate configuration changes.&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;div-red&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Caution:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Be cautious when running ad hoc commands on multiple hosts, as changes take effect immediately without confirmation. Always test commands on a single host or in a controlled environment before executing them on a large scale.&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;While ad hoc commands are powerful, they do not maintain state. For long-term configuration management, use Ansible playbooks to ensure consistency across your infrastructure.&lt;/span&gt;
&lt;/div&gt;
&lt;h2 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Ansible ad hoc commands are a powerful tool for performing quick, on-the-fly tasks. Their simplicity and flexibility make them an essential part of any Ansible user's toolkit. Whether you're checking connectivity, gathering system information, managing files, or installing packages, ad hoc commands provide a straightforward way to get the job done. Start experimenting with them today to see how they can streamline your IT operations!&lt;/p&gt;
</description>
<category>Automation</category>
<guid isPermaLink="true">https://coderlegion.com/1121/exploring-ansible-adhoc-commands-for-beginners</guid>
<pubDate>Sat, 15 Feb 2025 08:16:00 +0000</pubDate>
</item>
<item>
<title>Ansible in Automation, Configuration management and Devops</title>
<link>https://coderlegion.com/840/ansible-in-automation-configuration-management-and-devops</link>
<description>&lt;h2 id=&quot;what-is-ansible--1&quot;&gt;What is Ansible?&lt;/h2&gt;
&lt;p&gt;In the fast-paced world of software development, automation and efficient workflows are key to success. Enter Ansible, an open-source tool that has transformed the way we approach automation, configuration management, continuous deployment, and overall DevOps practices.&lt;/p&gt;
&lt;p&gt;Ansible is an IT automation tool that allows you to manage systems, deploy software, and orchestrate more advanced IT tasks. Its simplicity and powerful features have made it a favorite among developers and operations teams.&lt;/p&gt;
&lt;h2 id=&quot;how-ansible-works&quot;&gt;How Ansible Works&lt;/h2&gt;
&lt;p&gt;Unlike other automation tools that require agents to be installed on managed systems, Ansible is agentless. It connects to remote machines using SSH (Secure Shell) and executes tasks using predefined modules. These tasks are written in YAML-based playbooks, making it easy to define and manage automation workflows.&lt;/p&gt;
&lt;p&gt;Ansible operates from a control node, which sends commands to managed nodes, ensuring that configurations and deployments are consistent across all systems.&lt;/p&gt;
&lt;h2 id=&quot;the-benefits-of-ansible&quot;&gt;The Benefits of Ansible&lt;/h2&gt;
&lt;p&gt; &lt;strong&gt;- Simple Automation:&lt;/strong&gt; Ansible uses a straightforward YAML syntax, making&lt;br&gt;
   it easy to write automation scripts, also known as playbooks. This&lt;br&gt;
   simplicity reduces the learning curve and allows teams to get started&lt;br&gt;
   quickly.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;- Efficient Configuration Management With Ansible:&lt;/strong&gt; You can ensure that &lt;br&gt;
   your systems are configured consistently. By defining the desired  &lt;br&gt;
   state of your infrastructure in playbooks, Ansible automatically   &lt;br&gt;
   adjusts any discrepancies, maintaining a uniform environment.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;- Continuous Deployment:&lt;/strong&gt; Ansible streamlines the deployment process,&lt;br&gt;
   allowing you to automate the entire lifecycle of your &lt;br&gt;
   applications.From code integration to testing and deployment, Ansible&lt;br&gt;
   ensures that updates are delivered smoothly and reliably.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;- Enhanced DevOps Practices:&lt;/strong&gt; Ansible fosters collaboration between&lt;br&gt;
   development and operations teams. By using a single tool for both &lt;br&gt;
   automation and configuration management, it bridges the gap and &lt;br&gt;
   improves communication, leading to faster and more efficient &lt;br&gt;
   workflows.&lt;/p&gt;
&lt;h2 id=&quot;real-world-applications&quot;&gt;Real-World Applications&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;- Infrastructure as Code (IaC):&lt;/strong&gt; Ansible allows you to define and manage your infrastructure as code. This means you can version-control your infrastructure alongside your application code, making it easier to reproduce and manage environments.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- Application Deployment:&lt;/strong&gt; Deploying applications manually can be time-consuming and error-prone. Ansible automates the deployment process, ensuring that every deployment is consistent and reducing the risk of human error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- Configuration Management:&lt;/strong&gt; Maintaining consistent configurations across multiple servers is challenging. Ansible simplifies this by allowing you to define configurations once and apply them across all your systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;- Continuous Integration and Continuous Deployment (CI/CD):&lt;/strong&gt; Ansible integrates seamlessly with CI/CD pipelines, automating the build, test, and deployment processes. This ensures that new features and bug fixes are delivered to users quickly and reliably.&lt;/p&gt;
&lt;h2 id=&quot;getting-started-with-ansible&quot;&gt;Getting Started with Ansible&lt;/h2&gt;
&lt;p&gt;To install Ansible on a Linux system, use the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install ansible -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Red Hat-based systems:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo yum install ansible -y
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;writing-your-first-ansible-playbook&quot;&gt;Writing Your First Ansible Playbook&lt;/h3&gt;
&lt;p&gt;Here's an example of a simple Ansible playbook that installs Apache on a remote server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Install Apache Server
  hosts: web_servers
  become: yes
  tasks:
    - name: Install Apache
      apt:
        name: apache2
        state: present
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;run-the-playbook-using-&quot;&gt;Run the playbook using:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ansible-playbook apache.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;div-green&quot;&gt; &lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt; &lt;span class=&quot;alert-body&quot;&gt;If you’re managing multiple environments (e.g., development, staging, production), consider using Ansible Vault to encrypt sensitive information like API keys, database passwords, and SSH credentials within your playbooks.&lt;/span&gt; &lt;/div&gt;
&lt;div class=&quot;div-red&quot;&gt; &lt;span class=&quot;alert-header&quot;&gt;Caution:&lt;/span&gt; &lt;span class=&quot;alert-body&quot;&gt;Be cautious when running Ansible as root (using become: yes). If a playbook contains misconfigured tasks, it may cause unintended changes or even break critical system components. Always test your playbooks in a staging environment before deploying them in production.&lt;/span&gt; &lt;/div&gt;
&lt;div class=&quot;div-blue&quot;&gt; &lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt; &lt;span class=&quot;alert-body&quot;&gt;Ansible follows an idempotent approach, meaning that running the same playbook multiple times will not cause unnecessary changes. It only applies modifications if the system state differs from the defined configuration.&lt;/span&gt; &lt;/div&gt;
&lt;h2 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Ansible has revolutionized the way we approach automation, configuration management, continuous deployment, and DevOps practices. Its simplicity, efficiency, and power make it an invaluable tool for any development and operations team. By adopting Ansible, you can streamline your workflows, improve collaboration, and deliver software more efficiently.&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/840/ansible-in-automation-configuration-management-and-devops</guid>
<pubDate>Mon, 03 Feb 2025 08:42:07 +0000</pubDate>
</item>
<item>
<title>Set up a streams for Apache Kafka cluster with Ansible</title>
<link>https://coderlegion.com/587/set-up-a-streams-for-apache-kafka-cluster-with-ansible</link>
<description>&lt;p&gt;Based on the project Apache Kafka, streams for Apache Kafka is a powerful, innovative distributed event streaming platform. Using a fully distributed architecture, a streams for Apache Kafka cluster requires you to setup and deploy numerous services, ensure they are all properly configured and, even more importantly, that each component is functional and can communicate with each other. &lt;/p&gt;
&lt;p&gt;For all these reasons, applying an automation solution such as Red Hat Ansible Automation Platform to deploy and manage streams for Apache Kafka makes a lot of sense, especially as Ansible now has a collection dedicated for the product.&lt;/p&gt;
&lt;h2 id=&quot;overview&quot;&gt;Overview&lt;/h2&gt;
&lt;p&gt;As mentioned, a typical implementation of streams for Apache Kafka includes several components, each deployed on numerous target systems. Depending on the organization of the servers, some of those components may actually be collocated.&lt;/p&gt;
&lt;p&gt;At the core, however, there are two main parts: Zookeepers instances, often abbreviated zk, in charge of the orchestration of the cluster, and the brokers, responsible of processing the request.&lt;/p&gt;
&lt;p&gt;Beyond those foundational elements, a streams for Apache Kafka deployment can include other services, such as Kafka Connect, to help integrate with numerous systems. We'll first tackle the deployment of the cluster itself before discussing this extra service.&lt;/p&gt;
&lt;p&gt;Throughout this article, we will document step-by-step how to deploy each of those components using Ansible. To be thorough and easy to follow, however, we are going to first describe the setup of the Ansible controller itself to ensure the reader has all the information necessary to properly reproduce this demonstration.&lt;/p&gt;
&lt;h2 id=&quot;preparing-the-ansible-controller&quot;&gt;Preparing the Ansible controller&lt;/h2&gt;
&lt;h3 id=&quot;install-ansible&quot;&gt;Install Ansible&lt;/h3&gt;
&lt;p&gt;In Ansible lexicon, the system executing the automation tool itself is named the &quot;controller&quot;. The machines handled by the controller are designated under the term &quot;targets&quot;.&lt;/p&gt;
&lt;p&gt;To set up a controller on Red Hat Enterprise Linux 9 (RHEL 9), the very first step is to enable the Ansible Automation Platform subscription on the system:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# subscription-manager repos --enable=ansible-automation-platform-2.4-for-rhel-9-x86_64-rpms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then, Ansible can be installed simply by using dnf:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# dnf install -y ansible-core
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;configure-ansible-to-use-ansible-automation-hub&quot;&gt;Configure Ansible to use Ansible automation hub&lt;/h3&gt;
&lt;p&gt;As mentioned above, to smoothly set up and manage our streams for Apache Kafka cluster, we are going to leverage an Ansible collection dedicated to the product. This extension for the automation engine is available on the Ansible automation hub. This means that, to be able to connect to this repository of content, Ansible needs to be configured to utilize it (instead or on top of Ansible Galaxy). This configuration lives in the ansible.cfg configuration file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[defaults]
...
[inventory]
...
[galaxy]
server_list = automation_hub, galaxy
[galaxy_server.galaxy]
url=https://galaxy.ansible.com/
[galaxy_server.automation_hub]
url=https://cloud.redhat.com/api/automation-hub/
auth_url=https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token
token=&amp;lt;insert-your-token-here&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Please refer to this documentation on how to configure Ansible to Automation Hub and then execute the following command to install the Red Hat provided collection for streams for Apache Kafka.&lt;/p&gt;
&lt;p&gt;Once you have properly configured Ansible to utilize automation hub, you can simply run the following command to install the Ansible collection for streams for Apache Kafka:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ansible-galaxy collection install redhat.amq_streams
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Execute this command as the same system user used to run the playbook later on. Indeed, another user, on the same machine, won't have access to the installed collection.&lt;/p&gt;
&lt;h3 id=&quot;prepare-targets&quot;&gt;Prepare targets&lt;/h3&gt;
&lt;p&gt;For any systems managed by Ansible, SSHD must be running and a SSH pubkey, associated to root user needs to have been deployed to ensure that Ansible can connect to it. For RHEL 9 system, the required subscription for streams for Apache Kafka (and RHEL) must be enabled. Please refer to this documentation to learn the necessary steps to achieve this.&lt;/p&gt;
&lt;p&gt;An inventory of the targets needs to be provided to Ansible. The simplest way to achieve this is using a plain inventory file, such as this one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[all]

[brokers]
broker*.example.com

[zookeepers]
zk*.example.com

[topic_writer]
broker1.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For testing purpose, you can use localhost, which also removes the need to set up SSHD (as there will be no SSH connection between Ansible and localhost):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[all]
localhost ansible_connection=local

[brokers]
localhost ansible_connection=local

[zookeepers]
localhost ansible_connection=local

[topic_writer]
localhost ansible_connection=local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each of the components of the streams for Apache Kafka cluster used different ports, so they can be all deployed on one single system. Obviously, this is not recommended for production. It allows you, however, to run the playbooks provided in this article with only one machine (physical, virtual, or containerized).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Execute twice every playbook in this demonstration to verify that they are indeed idempotent (meaning, the second time does not perform any change).&lt;/p&gt;
&lt;h2 id=&quot;automating-the-deployment-of-the-zookeepers-instances&quot;&gt;Automating the deployment of the Zookeepers instances&lt;/h2&gt;
&lt;p&gt;Now that we have a working Ansible controller and an assortment of target machines ready to be provisioned, we can start the setup of our cluster. The first step will be, obviously, to deploy the zk instances. Indeed, they are being orchestrating the cluster, and having them already running before the brokers make sense.&lt;/p&gt;
&lt;p&gt;Thanks to the Ansible collection for streams for Apache Kafka, the playbook to set up the zk nodes is reduced to the bare minimum:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
- name: &quot;Ansible Playbook to install a Zookeeper ensemble with auth&quot;
  hosts: zookeepers
  gather_facts: yes
  vars_files:
    - vars.yml
  collections:
    - redhat.amq_streams
  roles:
    - role: redhat.amq_streams.amq_streams_zookeeper

  post_tasks:
    - name: &quot;Validate that Zookeeper deployment is functional.&quot;
      ansible.builtin.include_role:
        name: redhat.amq_streams.amq_streams_zookeeper
        tasks_from: validate.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the host's value matches the name of the inventory's group associated with the machines responsible for running Zookeeper.&lt;/p&gt;
&lt;p&gt;In essence, the playbook above just use the role redhat.amq_streams.amq_streams_zookeeper provided by the collection. In the post_tasks: segment, it utilizes as well that validate.yml, a handy playbook also supplied by the collection, to check that the zk instances deployed are indeed functional.&lt;/p&gt;
&lt;p&gt;The specific configuration of the Zookeepers node is provided by the variables in the vars.yml:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
amq_streams_common_download_dir: &quot;/opt&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In our demonstration here, we chose to deploy the binaries and files of streams for Apache Kafka in the /opt folder, on the target system. As this particular configuration will also be used for the broker (see below), it has been placed into a separate vars.yml file. This way, this configuration can be shared with the playbook managing the broker instance, as we'll cover in the next section.&lt;/p&gt;
&lt;p&gt;You can now run the playbooks to deploy zk on the targets belonging to the 'zookeepers' group:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ansible-playbook -i inventory -e @service_account.yml zookeepers.yml 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; KRaft can be employed as an alternative to Zookeepers for orchestration, however the Ansible collection for streams for Apache Kafka does not yet supports deployment using KRaft.&lt;/p&gt;
&lt;h2 id=&quot;automating-the-deployment-of-the-brokers&quot;&gt;Automating the deployment of the brokers&lt;/h2&gt;
&lt;p&gt;Now that the zk nodes are ready to orchestrate our cluster, we can look into the deployment of the broker’s nodes. Here again, the Ansible collection for streams for Apache Kafka is doing the heavy lifting:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
- name: &quot;Ansible Playbook to install Kafka Brokers with authentication&quot;
  hosts: brokers
  vars_files:
    - vars.yml
  vars:
    amq_streams_replication_factor: &quot;{{ groups['brokers'] | length }}&quot;
    amq_streams_broker_offsets_topic_replication_factor: &quot;{{ amq_streams_replication_factor }}&quot;
    amq_streams_broker_transaction_state_log_replication_factor: &quot;{{ amq_streams_replication_factor }}&quot;
    amq_streams_broker_transaction_state_log_min_isr: &quot;{{ amq_streams_replication_factor }}&quot;
  collections:
    - redhat.amq_streams
  roles:
    - name: amq_streams_broker
  post_tasks:
    - name: &quot;Validate that Broker deployment is functional.&quot;
      ansible.builtin.include_role:
        name: amq_streams_broker
        tasks_from: validate.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similar to the playbook above, we use the role provided by the streams for Apache Kafka collection in order to deploy the broker. As mentioned in the previous section, we also include the vars.yml to utilize the configuration shared between the zk instances and brokers.&lt;/p&gt;
&lt;p&gt;A few variables were also added in order to configure the broker instances. In the example above, we used the number of instances to determine the value of the replication factor.&lt;/p&gt;
&lt;p&gt;We can now run this playbook in order to set up our cluster:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ansible-playbook -i inventory -e @service_account.yml brokers.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;creating-topics-in-the-cluster&quot;&gt;Creating topics in the cluster&lt;/h2&gt;
&lt;p&gt;With our zk and brokers nodes completely provisioned, our cluster is now ready. For applications to be able to consume or publish events (or messages), topics must be created.&lt;/p&gt;
&lt;p&gt;Here again, these changes to the setup are fully automated by a third playbook, also leveraging the capabilities of the Ansible collection for streams for Apache Kafka:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
- name: &quot;Ansible Playbook to ensure topics are created in Red Hat AMQ Streams cluster&quot;
  hosts: topic_writer
  gather_facts: no
  vars_files:
    - vars.yml
  vars:
    amq_streams_broker_topic_to_create_name: &quot;myTopic&quot;
    amq_streams_broker_topics:
      - name: &quot;{{ amq_streams_broker_topic_to_create_name }}&quot;
        partitions: 1
        replication_factor: &quot;{{ groups['brokers'] | length }}&quot;
  collections:
    - middleware_automation.amq_streams

  tasks:
    - name: &quot;Create topic: {{ amq_streams_broker_topic_to_create_name }}&quot;
      ansible.builtin.include_role:
        name: amq_streams_broker
        tasks_from: topic/create.yml
      loop: &quot;{{ amq_streams_broker_topics }}&quot;
      loop_control:
        loop_var: topic
      vars:
        topic_name: &quot;{{ topic.name }}&quot;
        topic_partitions: &quot;{{ topic.partitions }}&quot;
        topic_replication_factor: &quot;{{ topic.replication_factor }}&quot;

  - name: &quot;Describe topic: {{ amq_streams_broker_topic_to_create_name }}&quot;
    ansible.builtin.include_role:
      name: amq_streams_broker
      tasks_from: topic/describe.yml
    loop: &quot;{{ amq_streams_broker_topics }}&quot;
    loop_control:
      loop_var: topic
    vars:
      topic_name: &quot;{{ topic.name }}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The target host for this playbook is all the members of the group topic_writer. As shown on the manifest above, this group only contains one of the brokers. Indeed, to create the topic for the entire cluster, any broker can be utilized and there is no need to repeat the operation on all the nodes.&lt;/p&gt;
&lt;p&gt;Here again, the vars.yml is used in this playbook to give access the shared variables between all of them. In our demonstration, the folder sheltering the streams for Apache Kafka deployment on the target is the only common parameters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; With a real life use case, there would be the most likely more common configuration between the components, however, for the sake of simplicity, our example only have this parameter shared.&lt;/p&gt;
&lt;p&gt;While we only create one topic, we used a list and the associated for loop to demonstrate how this playbook can be used to ensure the availability of as many topics as needed.&lt;/p&gt;
&lt;p&gt;Let's run this playbook to create our topic inside the cluster. We don't need to pass our service account credentials anymore, as this playbook mustn't perform any product installation and thus does not require to access the Red Hat Customer portal to retrieve the necessary zips:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ansible-playbook -i inventory topics.yml -e amq_streams_common_download_dir=/opt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that we need to specify where streams for Apache Kafka is located on the target system, because this playbook has not performed the installation; thus it does not know where the streams for Apache Kafka binaries and config files lives. If the topics deployment were part of the same playbook as the setup, this would not be necessary.&lt;/p&gt;
&lt;p&gt;Also note that this playbook, like the previous one, is idempotent: if we run it again, no change occurs on the target system as the required topics are already created inside the cluster:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ansible-playbook -i inventory topics.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;automating-the-deployment-of-kafka-connect&quot;&gt;Automating the deployment of Kafka Connect&lt;/h2&gt;
&lt;p&gt;With the cluster now fully functional, we can study an additional component of the rich ecosystem of Red Hat AMQ: Kafka Connect. This supplemental service will allow the cluster to interact with numerous external systems, typically a datasource of some kind, like traditional RDMS or in-memory cache. In essence, it helps data's conversion from a remote service into messages that can be processed by the cluster. Or, the other way around, transforming messages from the cluster into records persisted or manipulated by an external system.&lt;/p&gt;
&lt;p&gt;Here again, setting up this service on the target system is made extremely easy by the Ansible collection for streams for Apache Kafka:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
- name: &quot;Ansible Playbook to install a Kafka Connect ensemble with auth&quot;
  hosts: connect
  gather_facts: yes
  vars_files:
    - vars.yml
  collections:
    - redhat.amq_streams
  tasks:
    - name: &quot;Ensure Kafka Connect is running&quot;
      ansible.builtin.include_role:
        name: redhat.amq_streams.amq_streams_connect
  vars:
    connectors:
      - { name: &quot;file&quot;, path: &quot;connectors/file.yml&quot; }
      
  post_tasks:
    - name: &quot;Validate that Kafka Connect deployment is functional.&quot;
      ansible.builtin.include_role:
        name: redhat.amq_streams.amq_streams_connect
        tasks_from: validate.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The sole configuration specific to Kafka Connect is the file connector. It is needed only because Kafka Connect can be started without any connectors. In a real-life use case, it's not an issue as, obviously, such a component would not be deployed with a set of predefined connectors matching the requirement of the applications utilizing the cluster. In our demonstration, we add this connector only for the installation to be successful:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ansible-playbook -i inventory connect.yml -e @service_account.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In our demonstration we install each component on the same host, which means that the Red Hat service authentication data could be omitted in the command line above, as Kafka is already available on the target. However, in a real-life scenario, Kafka Connect will most likely run on a different system and thus require those identification information to set up the service.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;Thanks to both Ansible and its collection for streams for Apache Kafka, we have deployed a fully operational and ready to be used cluster, with only a set of simple playbooks. This setup works on one machine, but could be effortlessly utilized for hundreds of systems. The only change required is to modify the inventory accordingly.&lt;/p&gt;
&lt;p&gt;From now on, managing the cluster is also made easier by Ansible. New topics can handily be added to the configuration and deployed. Advanced features, such as authentication between components, can be implemented leveraging functionalities of the streams for Apache Kafka collection. Last but not the least, Ansible can help automate the update of the cluster. It's a difficult subject that needs to be handled according to the usage made of the system, so there is no &quot;out of the box&quot; solution for this, however the collection and the primitives of Ansible, provides a lot assistance to manage such critical operation.&lt;/p&gt;
</description>
<category>Automation</category>
<guid isPermaLink="true">https://coderlegion.com/587/set-up-a-streams-for-apache-kafka-cluster-with-ansible</guid>
<pubDate>Wed, 11 Dec 2024 20:21:43 +0000</pubDate>
</item>
</channel>
</rss>