<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sireesharaju Kamparaju</title>
    <description>The latest articles on DEV Community by Sireesharaju Kamparaju (@sirisharaju_kamparaju_c9c).</description>
    <link>https://dev.to/sirisharaju_kamparaju_c9c</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1674314%2F0313cf53-8219-49d9-a754-269c60fddf45.png</url>
      <title>DEV Community: Sireesharaju Kamparaju</title>
      <link>https://dev.to/sirisharaju_kamparaju_c9c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sirisharaju_kamparaju_c9c"/>
    <language>en</language>
    <item>
      <title>Setting Up Email Notifications in AWX via SMTP and Postfix — My DevOps Journey By Sireesha</title>
      <dc:creator>Sireesharaju Kamparaju</dc:creator>
      <pubDate>Wed, 17 Jun 2026 14:30:23 +0000</pubDate>
      <link>https://dev.to/sirisharaju_kamparaju_c9c/setting-up-email-notifications-in-awx-via-smtp-and-postfix-my-devops-journeyby-sireesha-5463</link>
      <guid>https://dev.to/sirisharaju_kamparaju_c9c/setting-up-email-notifications-in-awx-via-smtp-and-postfix-my-devops-journeyby-sireesha-5463</guid>
      <description>&lt;p&gt;After writing about Ansible Vault, I wanted to close the loop on something I'd been putting off — actually knowing when my patching workflow fails instead of finding out the next morning. So in my homelab I set up a Postfix mail server to act as my SMTP relay and connected it to AWX for email notifications.&lt;br&gt;
In this blog I'll walk through exactly how I did it — setting up Postfix, configuring the AWX notification template, and getting the firewall side sorted with help from our network team since that's not something I manage directly.&lt;br&gt;
In this blog I'll cover:&lt;/p&gt;

&lt;p&gt;Setting up Postfix as my SMTP relay&lt;br&gt;
Creating the email notification template in AWX&lt;br&gt;
Getting the firewall opened for SMTP traffic&lt;br&gt;
Attaching the notification to my patching workflow&lt;br&gt;
Testing it end to end&lt;/p&gt;

&lt;p&gt;Let's get into it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I Choose with Postfix&lt;/strong&gt;&lt;br&gt;
I wanted something self-hosted rather than relying on Gmail or an external SMTP provider for my homelab. Postfix is the standard mail transfer agent on Linux and it's lightweight enough to run on a small VM just for relaying AWX notifications. It also gave me a proper feel for how a real internal SMTP relay works — which is exactly what most companies use in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Installing Postfix&lt;/strong&gt;&lt;br&gt;
I set up a small Ubuntu VM dedicated to acting as my mail relay and installed Postfix on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; postfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During installation it asks you to choose a configuration type. I selected:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;And set the System mail name to something like:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2 — Configuring Postfix as a Relay&lt;/strong&gt;&lt;br&gt;
I edited the main Postfix config file to allow my AWX server to relay through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vi /etc/postfix/main.cf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key settings I made sure were set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;myhostname&lt;/span&gt; = &lt;span class="n"&gt;mail&lt;/span&gt;.&lt;span class="n"&gt;homelab&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt;
&lt;span class="n"&gt;mydestination&lt;/span&gt; = $&lt;span class="n"&gt;myhostname&lt;/span&gt;, &lt;span class="n"&gt;localhost&lt;/span&gt;.&lt;span class="n"&gt;localdomain&lt;/span&gt;, &lt;span class="n"&gt;localhost&lt;/span&gt;
&lt;span class="n"&gt;mynetworks&lt;/span&gt; = &lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;/&lt;span class="m"&gt;8&lt;/span&gt; [::&lt;span class="n"&gt;ffff&lt;/span&gt;:&lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;]/&lt;span class="m"&gt;104&lt;/span&gt; [::&lt;span class="m"&gt;1&lt;/span&gt;]/&lt;span class="m"&gt;128&lt;/span&gt; &amp;lt;&lt;span class="n"&gt;ansible&lt;/span&gt;-&lt;span class="n"&gt;IP&lt;/span&gt;&amp;gt;/&lt;span class="m"&gt;24&lt;/span&gt;
&lt;span class="n"&gt;inet_interfaces&lt;/span&gt; = &lt;span class="n"&gt;all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important line is mynetworks — I added my homelab subnet /24 so Postfix trusts and relays mail coming from any of my homelab servers, including AWX, without needing authentication.&lt;br&gt;
Restart Postfix to apply the changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart postfix
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;postfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3 — Testing Postfix Locally First&lt;/strong&gt;&lt;br&gt;
Before even touching AWX, I wanted to confirm Postfix itself was working. I installed mailutils to send a quick test email from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; mailutils
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Test email body"&lt;/span&gt; | mail &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Postfix Test"&lt;/span&gt; your@email.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the mail log to see what happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/mail.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see status=sent in the log, Postfix is relaying correctly and you're ready to connect AWX to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Getting the Firewall Opened&lt;/strong&gt;&lt;br&gt;
This is the part I couldn't do myself — our network team manages the firewall between network segments in our homelab/internal setup. I raised a request with them to allow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWX server subnet → Postfix server (10.215.77.X) → Port 25 TCP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They confirmed it was opened on their side. If you're managing your own firewall on the Postfix VM itself, the equivalent UFW rule would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 10.215.77.0/24 to any port 25 proto tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And on the AWX side, if there's an outbound rule needed on the Kubernetes worker nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow out to 10.215.77.X port 25 proto tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tip: Always test raw port connectivity before testing inside AWX. Saves you from chasing the wrong problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-zv&lt;/span&gt; 10.215.77.X 25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that connects, you're clear to move on to the AWX side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — Creating the Notification Template in AWX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWX UI → Administration → Notifications&lt;br&gt;
Click Add&lt;/p&gt;

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

&lt;p&gt;Click Save&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6 — Attaching the Notification to My Patching Workflow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWX UI → Templates → open Patching Workflow → Notifications tab&lt;br&gt;
Under Failure, toggle on Patching Workflow Email Alerts&lt;br&gt;
Click Save&lt;/p&gt;

&lt;p&gt;I only enabled it for Failure — I don't need an email every time the workflow succeeds, only when something goes wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7 — Testing the Notification&lt;/strong&gt;&lt;br&gt;
AWX has a built-in test button so I didn't need to wait for an actual failure to check it worked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go back to Administration → Notifications&lt;/li&gt;
&lt;li&gt;Open Patching Workflow Email Alerts&lt;/li&gt;
&lt;li&gt;Click Test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I checked my inbox and the test email came through within a few seconds. I also double checked the Postfix mail log on the relay server at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/mail.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeing the AWX server's IP show up in the log with status=sent confirmed the whole chain was working end to end — AWX → Postfix → my inbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors I Hit Along the Way&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 1 — Connection Timed Out from AWX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SMTPConnectError: Connection to &amp;lt;ansoble-IP&amp;gt;timed out&lt;/code&gt;&lt;br&gt;
This was the firewall not being opened yet. Once the network team confirmed the rule was in place between the AWX subnet and the Postfix server, this cleared immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2 — Relay Access Denied&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SMTPRecipientsRefused: 554 5.7.1 Relay access denied&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This happened because I forgot to add my homelab subnet to mynetworks in Postfix's main.cf. Postfix was rejecting the AWX server because it didn't recognise it as a trusted network. Fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vi /etc/postfix/main.cf
&lt;span class="c"&gt;# add your subnet to mynetworks&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart postfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Error 3 — Mail Stuck in the Queue&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I noticed test emails weren't arriving and checked the Postfix queue:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;There were a few stuck messages. This was because my Postfix VM didn't have a working DNS resolver set up properly, so it couldn't resolve where to deliver outbound mail. Since this was all internal traffic destined for a local mailbox, I made sure the destination mail server was reachable and resolvable, then flushed the queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;postqueue &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Error 4 — Postfix Service Not Starting After Config Change&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;postfix/postfix-script: fatal: the Postfix mail system is not running&lt;/code&gt;&lt;br&gt;
A typo in main.cf caused this. Always check the config syntax before restarting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;postfix check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pointed me straight to the line with the typo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I Learned from This&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test each layer separately.&lt;/strong&gt; Postfix locally first, then raw firewall connectivity, then AWX notification test, then the actual workflow trigger. Trying to debug all of it at once just leads to confusion about where the actual problem is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;mynetworks is the equivalent of an allow-list for SMTP relaying.&lt;/strong&gt; If your AWX server's subnet isn't in there, Postfix will reject it outright with a relay denied error — no amount of AWX configuration will fix that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firewall changes between teams take coordination.&lt;/strong&gt; Since I don't manage the network firewall myself, I had to raise the request and wait for confirmation. Good reminder that in real environments, automation setups often depend on more than just your own server configs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Postfix mail log is your best friend.&lt;/strong&gt; Whenever something didn't work as expected, tail -f /var/log/mail.log told me exactly what was happening — accepted, deferred, bounced or sent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's Next&lt;/strong&gt;&lt;br&gt;
Now that I have working email alerts on failure, the next thing I want to try is adding a Slack notification alongside email — so I get alerted in both places when the patching workflow fails at 2AM on a Sunday.&lt;/p&gt;

&lt;p&gt;Drop your questions in the comments — happy to help if you're setting up something similar in your own homelab!&lt;br&gt;
— Sireesha&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Ansible Vault — Managing Secrets Securely in Ansible and AWX — My DevOps Journey By Sireesha</title>
      <dc:creator>Sireesharaju Kamparaju</dc:creator>
      <pubDate>Thu, 04 Jun 2026 15:59:38 +0000</pubDate>
      <link>https://dev.to/sirisharaju_kamparaju_c9c/ansible-vault-managing-secrets-securely-in-ansible-and-awx-my-devops-journeyby-sireesha-5aeb</link>
      <guid>https://dev.to/sirisharaju_kamparaju_c9c/ansible-vault-managing-secrets-securely-in-ansible-and-awx-my-devops-journeyby-sireesha-5aeb</guid>
      <description>&lt;p&gt;One thing I kept putting off when I started with Ansible was properly securing my secrets. Passwords, API keys, tokens — they were just sitting in plain text inside my vars files. I knew it was wrong but it worked and I kept moving forward.&lt;br&gt;
Then I started thinking about what happens when this goes into GitLab. Anyone with access to the repo can see every password. That's when I properly sat down and learned Ansible Vault — and honestly I wish I'd done it from day one.&lt;br&gt;
In this blog I'll walk through how I use Ansible Vault from the command line and then how I handle it properly inside AWX so scheduled jobs and workflow templates can still run without someone manually typing a password every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Ansible Vault?&lt;/strong&gt;&lt;br&gt;
Ansible Vault is a built-in feature that lets you encrypt sensitive data — passwords, keys, tokens — so they can be safely stored in your playbooks and pushed to GitLab without exposing anything.&lt;br&gt;
The beauty of it is that it works right inside your existing YAML files. Your playbook structure doesn't change — Vault just encrypts the values that need protecting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I Was Doing Before — The Wrong Way&lt;/strong&gt;&lt;br&gt;
This is what my vars file looked like before Vault:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vars/main.yml&lt;/span&gt;
&lt;span class="na"&gt;ubuntu_sudo_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MyPassword123&lt;/span&gt;
&lt;span class="na"&gt;db_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SuperSecret456&lt;/span&gt;
&lt;span class="na"&gt;api_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;abcd1234efgh5678&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pushed straight to GitLab. Anyone with repo access could read every single one of those. Not good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encrypting a Single Value with Ansible Vault&lt;/strong&gt;&lt;br&gt;
The first thing I learned was how to encrypt just a single variable value — not the entire file. This is cleaner because the rest of the vars file stays readable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-vault encrypt_string &lt;span class="s1"&gt;'MyPassword123'&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s1"&gt;'ubuntu_sudo_password'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It asks you to create a vault password — this is the master password you'll use to decrypt later. Enter it twice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New Vault password:
Confirm New Vault password:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ubuntu_sudo_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!vault&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;$ANSIBLE_VAULT;1.1;AES256&lt;/span&gt;
          &lt;span class="s"&gt;66386439653236336462626566653337386235396138623934363161623364663834623437333132&lt;/span&gt;
          &lt;span class="s"&gt;3865663966343437326235373961316435623365663866610a663834623437333132386566396634&lt;/span&gt;
          &lt;span class="s"&gt;34373236353337386235396138623934363161623364663834623437333132386566396634343732&lt;/span&gt;
          &lt;span class="s"&gt;3635333738623539613862393436316162336466383462343733313238656639363434373236353337&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy this entire block and paste it into your vars file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vars/main.yml&lt;/span&gt;
&lt;span class="na"&gt;ubuntu_sudo_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!vault&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;$ANSIBLE_VAULT;1.1;AES256&lt;/span&gt;
          &lt;span class="s"&gt;66386439653236336462626566653337386235396138623934363161623364663834623437333132&lt;/span&gt;
          &lt;span class="s"&gt;...&lt;/span&gt;
&lt;span class="na"&gt;db_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!vault&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;$ANSIBLE_VAULT;1.1;AES256&lt;/span&gt;
          &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now this file is safe to push to GitLab. Anyone who opens it sees encrypted gibberish — not the actual passwords.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Encrypting an Entire Vars File&lt;/strong&gt;&lt;br&gt;
If you have a lot of secrets, encrypting the whole file is quicker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-vault encrypt vars/secrets.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To edit it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-vault edit vars/secrets.yml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To decrypt it permanently (be careful with this one): (I didn't try this)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-vault decrypt vars/secrets.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Running Playbooks with Vault from Command Line&lt;/strong&gt;&lt;br&gt;
This is how I was running playbooks before I set up AWX properly. Every time I ran a playbook that used vaulted variables, I added --ask-vault-pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-playbook patch.yml &lt;span class="nt"&gt;-i&lt;/span&gt; inventory/hosts.yml &lt;span class="nt"&gt;--ask-vault-pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prompts to enter vault password:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Enter vault password and the playbook runs, decrypting the secrets on the fly. Nothing is ever stored in plain text on the server.&lt;br&gt;
This works perfectly from the command line. But the moment you move to AWX — scheduled jobs, workflow templates, CI/CD — you can't have AWX stopping and waiting for someone to type a password. That's where the AWX Vault credential comes in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem with --ask-vault-pass in AWX&lt;/strong&gt;&lt;br&gt;
Imagine you've set up your patching workflow to run every Sunday night at 2AM. AWX kicks off the job automatically — but your playbook uses vaulted passwords. AWX has no way to ask you for the vault password at 2AM.&lt;br&gt;
The job just fails.&lt;br&gt;
That's exactly the problem I hit. I had my refresh.yml scheduled for every Monday at 5AM and it kept failing because it couldn't decrypt the vault variables. I was confused at first — it ran fine from the command line. Then I realised AWX needs the vault password stored as a credential so it can decrypt automatically at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solving It in AWX — Adding a Vault Credential&lt;/strong&gt;&lt;br&gt;
AWX has a built-in credential type specifically for Ansible Vault. Here's how I set it up:&lt;/p&gt;

&lt;p&gt;AWX UI → Credentials → Add -&amp;gt;Click Save&lt;/p&gt;

&lt;p&gt;The vault password is stored encrypted inside AWX — no one can read it back out. It's just referenced by name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attaching the Vault Credential to a Job Template&lt;/strong&gt;&lt;br&gt;
Now I attach this credential to any job template that uses vaulted variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWX UI → Templates → open your job template → Edit&lt;/li&gt;
&lt;li&gt;In the Credentials field — click the search icon&lt;/li&gt;
&lt;li&gt;You'll see your existing machine credential already there&lt;/li&gt;
&lt;li&gt;Search for ansible-vault-cred and select it as well&lt;/li&gt;
&lt;li&gt;A job template can have multiple credentials — one for machine access, one for vault&lt;/li&gt;
&lt;li&gt;Click Save&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Now when AWX runs this job template&lt;/strong&gt; — whether manually, on a schedule, or as part of a workflow — it automatically decrypts the vault variables using the stored credential. No one needs to type anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt; — Patching Workflow with Vault&lt;br&gt;
Let me show you exactly how this works end to end with my patching workflow.&lt;br&gt;
My pre_patch_check.yml playbook connects to servers using credentials stored in a vaulted vars file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# vars/secrets.yml (encrypted with ansible-vault)&lt;/span&gt;
&lt;span class="na"&gt;ubuntu_sudo_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!vault&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;$ANSIBLE_VAULT;1.1;AES256&lt;/span&gt;
          &lt;span class="s"&gt;...&lt;/span&gt;
&lt;span class="na"&gt;db_connection_string&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!vault&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;$ANSIBLE_VAULT;1.1;AES256&lt;/span&gt;
          &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Myplaybook&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pre-patch checks&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
  &lt;span class="na"&gt;vars_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vars/secrets.yml&lt;/span&gt;
  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check application status&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inventory_hostname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/health"&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;api_token&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
      &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;health_check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From command line I run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-playbook pre_patch_check.yml &lt;span class="nt"&gt;-i&lt;/span&gt; inventory/hosts.yml &lt;span class="nt"&gt;--ask-vault-pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In AWX — the job template has both machine-cred and ansible-vault-cred attached. When the patching workflow kicks off at 2AM on Sunday, AWX decrypts the secrets automatically and the whole workflow runs without anyone being awake to babysit it.&lt;br&gt;
That's proper production automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotating Vault Password&lt;/strong&gt;&lt;br&gt;
Every now and then you should change your vault master password — especially if someone who knew it leaves the team. Here's how to rekey all your encrypted files at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-vault rekey vars/secrets.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It asks for the old password, then the new one. After rekeying, update the vault credential in AWX with the new password too — otherwise your scheduled jobs will start failing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors I Hit Along the Way&lt;br&gt;
Error 1 — Decryption Failed&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR! Decryption failed (no vault secrets would unlock any vault file/string)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix — I had forgotten to attach the ansible-vault-cred to the job template. Once I added it, the scheduled job ran perfectly.&lt;br&gt;
&lt;strong&gt;Error 3 — Committed Vault Password File to GitLab&lt;/strong&gt;&lt;br&gt;
Early on I accidentally committed my .vault_pass file to GitLab. As soon as I realised:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove from tracking&lt;/span&gt;
git &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--cached&lt;/span&gt; .vault_pass

&lt;span class="c"&gt;# Add to gitignore&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".vault_pass"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore

git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"removed vault password file from tracking"&lt;/span&gt;
git push origin dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And immediately changed my vault password using ansible-vault rekey. Lesson learned — always add .vault_pass to .gitignore before creating it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Golden Rules for Ansible Vault&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After going through all of this, here's what I now follow without exception:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never store plain text passwords in vars files — encrypt everything sensitive from day one&lt;/li&gt;
&lt;li&gt;Always add .vault_pass to .gitignore before creating the file&lt;/li&gt;
&lt;li&gt;Use AWX Vault credentials for any job that runs on a schedule or in a workflow&lt;/li&gt;
&lt;li&gt;Rekey when team members leave — treat it like changing a shared password&lt;/li&gt;
&lt;li&gt;Test vault decryption after any rekey before your next scheduled job runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What's Next&lt;/strong&gt;&lt;br&gt;
Now that secrets are properly managed, the next blog I'm planning is AWX Notifications — setting up Slack and email alerts so when a patching workflow fails at 2AM, I know about it first thing in the morning without having to log into AWX to check.&lt;/p&gt;

&lt;p&gt;Drop your questions in the comments — happy to help!&lt;br&gt;
— Sireesha&lt;/p&gt;

</description>
      <category>awx</category>
      <category>ansible</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>AWX Workflow Templates &amp; Schedules — My DevOps Journey</title>
      <dc:creator>Sireesharaju Kamparaju</dc:creator>
      <pubDate>Thu, 04 Jun 2026 11:24:36 +0000</pubDate>
      <link>https://dev.to/sirisharaju_kamparaju_c9c/awx-workflow-templates-schedules-my-devops-journey-o68</link>
      <guid>https://dev.to/sirisharaju_kamparaju_c9c/awx-workflow-templates-schedules-my-devops-journey-o68</guid>
      <description>&lt;p&gt;If you've been following my blog series, you know I've already set up AWX, synced GitLab projects, and run my first playbook. The next piece I wanted to explore was how AWX handles more complex automation tasks.&lt;/p&gt;

&lt;p&gt;In this post, I'll cover two features I've started using more and more: Workflow Templates and Schedules.&lt;/p&gt;

&lt;p&gt;Here's what I'll walk through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What Workflow Templates are and when they're useful&lt;/li&gt;
&lt;li&gt;How I built a patching workflow by chaining multiple job templates together&lt;/li&gt;
&lt;li&gt;How I configured a schedule to run a playbook automatically every Monday at 5 AM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow Templates
&lt;/h2&gt;

&lt;p&gt;When I first started using AWX, I mostly worked with standard job templates. For simple tasks, they did exactly what I needed — run a playbook against an inventory and return the results.&lt;/p&gt;

&lt;p&gt;As I started building more realistic automation tasks, I realised most jobs weren't a single playbook run. A typical maintenance task might involve patching servers, rebooting them, running validation checks, and then notifying the team once everything is complete.&lt;/p&gt;

&lt;p&gt;I could run each of those steps manually, but it quickly became tedious and left plenty of room for mistakes.&lt;/p&gt;

&lt;p&gt;That's where Workflow Templates came in. They allow you to link multiple job templates together and define what should happen when a step succeeds or fails. Instead of managing each stage yourself, AWX handles the flow for you.&lt;/p&gt;

&lt;p&gt;For me, this was the point where AWX started feeling less like a tool for launching playbooks and more like a platform for automating operational processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Real-World Use Case – Patching Workflow
&lt;/h2&gt;

&lt;p&gt;I didn't fully appreciate Workflow Templates until I started building a server patching process.&lt;/p&gt;

&lt;p&gt;Originally, I was launching individual job templates one after another: run pre-checks, patch the server, reboot it, verify services, and finally send a notification. It worked, but it meant I had to monitor every stage and decide manually what to do if something failed.&lt;/p&gt;

&lt;p&gt;Moving that process into a Workflow Template made things much easier. AWX could handle the sequence automatically and stop the workflow if a critical step failed.&lt;/p&gt;

&lt;p&gt;The workflow looked like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Stop Application → Pre-Patch Check → Patch Servers → Reboot → Post-Patch Check → Start Application&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each step is a separate playbook, chained together in AWX as a workflow. If any step fails, the workflow stops right there — the next step doesn't run. That's critical. You don't want servers rebooting if the pre-patch check failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Create the Individual Job Templates First&lt;/strong&gt;&lt;br&gt;
Before building the workflow, I made sure each playbook was already in GitLab and each had its own job template in AWX. Here's what I set up:&lt;/p&gt;

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

&lt;p&gt;Each of these was already tested individually before I built the workflow. That's important — don't try to debug a workflow and individual playbooks at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Create the Workflow Template&lt;/strong&gt;&lt;br&gt;
AWX UI → Templates → Add → Add Workflow Template&lt;/p&gt;

&lt;p&gt;Click Save&lt;br&gt;
This opens the Workflow Visualiser — a drag and drop canvas where you build the flow visually. This is where it gets satisfying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Build the Workflow in the Visualiser&lt;/strong&gt;&lt;br&gt;
In the Workflow Visualiser:&lt;/p&gt;

&lt;p&gt;Click Start → a window pops up asking you to select a node&lt;br&gt;
Select Stop Application job template → click Save&lt;br&gt;
Hover over the Stop Application node → click the + button&lt;br&gt;
Select On Success → select Pre-Patch Check → click Save&lt;br&gt;
Repeat for each step — always connecting On Success to the next playbook&lt;br&gt;
Here's the full chain I built:&lt;/p&gt;

&lt;p&gt;Start&lt;br&gt;
  └── Stop Application&lt;br&gt;
        └── [On Success] Pre-Patch Check&lt;br&gt;
              └── [On Success] Patch Servers&lt;br&gt;
                    └── [On Success] Reboot Servers&lt;br&gt;
                          └── [On Success] Post-Patch Check&lt;br&gt;
                                └── [On Success] Start Application&lt;/p&gt;

&lt;p&gt;The key here is On Success at every step. If patching fails, AWX stops the workflow right there — servers don't reboot, application doesn't try to start on a broken system. That's exactly the safety net you need in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Click Save when the workflow is built
Step 4 — Launch the Workflow
Go to Templates → find Patching Workflow
Click Launch 🚀
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWX shows you the workflow running in real time — each node lights up green as it completes or red if it fails. You can click into any node to see the full playbook output for that step.&lt;/p&gt;

&lt;p&gt;Watching a 6-step patching workflow run end to end without touching anything is one of those moments that makes all the setup worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWX Schedules — Automating Recurring Jobs&lt;/strong&gt;&lt;br&gt;
Now the second feature — Schedules. Once you have job templates set up, you can tell AWX to run them automatically at any time or frequency without anyone manually clicking Launch.&lt;/p&gt;

&lt;p&gt;My use case was simple but important — I have a refresh.yml playbook that needs to run every Monday at 5AM. Before schedules I had to remember to run it manually. That's not automation — that's just a reminder. Schedules fixed that completely.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Setting Up the Monday 5AM Schedule&lt;br&gt;
AWX UI → Templates → open the job template you want to schedule&lt;br&gt;
Click the Schedules tab at the top&lt;br&gt;
Click Add&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
Field                     Value&lt;br&gt;
Name                      Weekly Data Refresh&lt;br&gt;
Start Date            Set today's date or next Monday&lt;br&gt;
Start Time            05:00 AM&lt;br&gt;
Time Zone             Select your timezone&lt;br&gt;
Repeat Frequency      Week&lt;br&gt;
Run On  Monday&lt;br&gt;
Click Save&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
That's it. AWX will now automatically run refresh.yml every Monday at 5AM without anyone touching anything. You can verify it's set up correctly by checking the Next Run field — it shows you exactly when the next execution is scheduled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managing Schedules&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few things I find useful when managing schedules in AWX:&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Enable/Disable without deleting *&lt;/em&gt;— there's a toggle on each schedule. If I don't want the refresh to run during a maintenance window, I just disable it and re-enable after. No need to delete and recreate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple schedules on one template&lt;/strong&gt; — you can add more than one schedule to the same job template. For example I could run refresh.yml every Monday at 5AM AND every first day of the month at midnight if needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check the Activity Stream&lt;/strong&gt; — after a scheduled job runs, go to Activity Stream in AWX to confirm it ran and whether it succeeded or failed. This is your audit trail for scheduled jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors I Hit Along the Way&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Error 1 — Workflow Node Not Connecting&lt;/strong&gt;&lt;br&gt;
When building the workflow, I kept clicking the + button but the connection line wasn't appearing between nodes.&lt;/p&gt;

&lt;p&gt;Fix — make sure you hover directly over the node until you see the + appear, then click it. If you click too fast it doesn't register. Slow down and wait for the hover state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2 — Wrong Timezone on Schedule&lt;/strong&gt;&lt;br&gt;
My schedule was set up but running at the wrong time. The job was triggering at 5AM UTC instead of my local time.&lt;/p&gt;

&lt;p&gt;Fix — when creating the schedule, always explicitly set the Time Zone field to your local timezone. Don't assume it defaults to your local time — it defaults to UTC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 3 — Workflow Skipping Failure Path&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a node failed, the workflow was completing instead of stopping.&lt;/p&gt;

&lt;p&gt;Fix — I had accidentally connected some nodes with On Either instead of On Success. Go back into the Workflow Visualiser, click each connection line and check it says Run on Success. Change any that are set to On Either or Always.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually changed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before workflows, patching was basically manual SSH work stitched together step by step. It worked, but it was fragile and very dependent on the person running it.&lt;/p&gt;

&lt;p&gt;Now it’s just one workflow execution.&lt;/p&gt;

&lt;p&gt;Same with schedules — anything recurring used to rely on memory or reminders. Now it just runs.&lt;/p&gt;

&lt;p&gt;Nothing revolutionary individually, but together it changes how you approach automation. You start thinking in systems instead of tasks.&lt;/p&gt;

&lt;p&gt;This is what proper production automation looks like — reliable, auditable, and hands-off.&lt;/p&gt;

&lt;p&gt;Drop your questions in the comments — happy to help!&lt;/p&gt;

&lt;p&gt;— Sireesha&lt;/p&gt;

</description>
      <category>awx</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Running Your First Playbook from Ansible AWX — My DevOps Journey By Sireesha</title>
      <dc:creator>Sireesharaju Kamparaju</dc:creator>
      <pubDate>Tue, 02 Jun 2026 09:58:23 +0000</pubDate>
      <link>https://dev.to/sirisharaju_kamparaju_c9c/running-your-first-playbook-from-ansible-awx-my-devops-journeyby-sireesha-2ppl</link>
      <guid>https://dev.to/sirisharaju_kamparaju_c9c/running-your-first-playbook-from-ansible-awx-my-devops-journeyby-sireesha-2ppl</guid>
      <description>&lt;p&gt;In my previous blogs, I covered how to deploy AWX on Kubernetes using Helm and how to sync GitLab projects into AWX. Now that AWX is up and running, the next thing I wanted to do was actually run a playbook from the AWX UI — and that involves setting up a few things first.&lt;br&gt;
In this blog I'll walk through everything you need to configure inside AWX before you can launch your first job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard overview&lt;/li&gt;
&lt;li&gt;Adding an Organisation&lt;/li&gt;
&lt;li&gt;Adding Users&lt;/li&gt;
&lt;li&gt;Adding Inventory and Hosts&lt;/li&gt;
&lt;li&gt;Adding Credentials&lt;/li&gt;
&lt;li&gt;Adding a Project&lt;/li&gt;
&lt;li&gt;Creating a Job Template and launching it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's go step by step.&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;The AWX Dashboard&lt;/strong&gt;&lt;br&gt;
When you first log in with your admin credentials, you land on the AWX dashboard. It gives you a nice overview of everything happening on your server at a glance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Number of hosts that have successfully run playbooks&lt;/li&gt;
&lt;li&gt;Number of hosts that failed&lt;/li&gt;
&lt;li&gt;Total number of inventories&lt;/li&gt;
&lt;li&gt;Number of projects and their sync status&lt;/li&gt;
&lt;li&gt;A graph showing playbook runs over time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the left side you'll see four main sections — Resources, Access, Administration and Settings. Everything we need is in here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Add an Organisation&lt;/strong&gt;&lt;br&gt;
The first thing I set up was an organisation. In AWX, almost everything sits under an organisation — users, credentials, projects, inventories.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On the left pane go to Access → Organisations&lt;/li&gt;
&lt;li&gt;Click Add&lt;/li&gt;
&lt;li&gt;Enter a Name and Description&lt;/li&gt;
&lt;li&gt;Click Save&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Simple enough — but don't skip this, everything else depends on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Add a User&lt;/strong&gt;&lt;br&gt;
Next I created a user under the organisation. AWX has three user types and it's worth understanding the difference before you set one up:&lt;br&gt;
&lt;strong&gt;Normal User&lt;/strong&gt; — can create, use and update templates. Good for engineers who need to run playbooks but shouldn't have full admin access.&lt;br&gt;
&lt;strong&gt;System Auditor&lt;/strong&gt; — can view inventory, templates and job status but cannot create or modify anything. Great for someone who just needs visibility.&lt;br&gt;
&lt;strong&gt;System Administrator&lt;/strong&gt; — has full privileges, same as the default admin account. Use this carefully.&lt;br&gt;
To add a user:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Left pane → Access → Users&lt;/li&gt;
&lt;li&gt;Click Add&lt;/li&gt;
&lt;li&gt;Enter First Name, Last Name, Email, Username and Password&lt;/li&gt;
&lt;li&gt;Select the Organisation you just created&lt;/li&gt;
&lt;li&gt;Choose the User Type based on the access you want to give&lt;/li&gt;
&lt;li&gt;Click Save&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Step 3 — Add an Inventory&lt;/strong&gt;&lt;br&gt;
An inventory in AWX is basically your list of hosts — the machines you want to run playbooks against. You can organise them into groups like development, testing, and production.&lt;br&gt;
To add an inventory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Left pane → Resources → Inventories&lt;/li&gt;
&lt;li&gt;Click Add → Add Inventory&lt;/li&gt;
&lt;li&gt;Enter a Name and select your Organisation&lt;/li&gt;
&lt;li&gt;Click Save&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Adding Hosts to the Inventory&lt;br&gt;
Once the inventory is saved, add your hosts to it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the inventory you just created&lt;/li&gt;
&lt;li&gt;Click the Hosts tab&lt;/li&gt;
&lt;li&gt;Click Add Host&lt;/li&gt;
&lt;li&gt;Enter the Hostname — this can be an IP address or a URL. For example, my host IP was 192.168.237.145&lt;/li&gt;
&lt;li&gt;Add a Description if you want&lt;/li&gt;
&lt;li&gt;Click Save&lt;/li&gt;
&lt;/ul&gt;

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

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

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

&lt;p&gt;You can add as many hosts as you need. I added two hosts to my inventory. You can also temporarily remove a host from the inventory using the on/off toggle on the right side of each host — handy when you want to skip a machine without deleting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Add Credentials&lt;/strong&gt;&lt;br&gt;
AWX stores credentials separately from everything else — which is actually a really clean way to manage it. You create a credential once and reuse it across multiple templates.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Left pane → Resources → Credentials&lt;/li&gt;
&lt;li&gt;Click Add&lt;/li&gt;
&lt;li&gt;Enter a Name and Description&lt;/li&gt;
&lt;li&gt;Select your Organisation&lt;/li&gt;
&lt;li&gt;Select Credential Type → choose Machine (this is similar to SSH login)&lt;/li&gt;
&lt;li&gt;Enter the Username and Password of the remote machine&lt;/li&gt;
&lt;li&gt;If your playbook needs to run as root, fill in the Privilege Escalation Username and Password as well&lt;/li&gt;
&lt;li&gt;Click Save&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If You're Pulling Playbooks from a Git Repo&lt;br&gt;
If your playbooks are coming from GitHub or GitLab, you'll need a second credential for source control:&lt;/p&gt;

&lt;p&gt;Follow the same steps above but select GitHub Personal Access Token as the credential type&lt;br&gt;
Paste your token and save&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To get a GitHub personal access token:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Log into GitHub → Settings → Developer Settings → Personal Access Tokens → Generate new token&lt;br&gt;
Copy it and paste it into AWX — you won't see it again after leaving that page!&lt;/p&gt;

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

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

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

&lt;p&gt;&lt;strong&gt;Step 5 — Add a Project&lt;/strong&gt;&lt;br&gt;
A project in AWX is where you point AWX to your playbooks — either from a Git repo or a directory on the AWX server itself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Left pane → Resources → Projects&lt;/li&gt;
&lt;li&gt;Click Add&lt;/li&gt;
&lt;li&gt;Enter a Name and Description&lt;/li&gt;
&lt;li&gt;Select your Organisation&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select SCM Type:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose Git if your playbooks are in GitHub or GitLab&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose Manual if the playbooks are already sitting on the AWX server in a local directory&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enter the SCM URL — this is your GitHub/GitLab repo URL&lt;br&gt;
Under SCM Update Options select what makes sense for your setup — I ticked Update Revision on Launch so AWX always pulls the latest version&lt;br&gt;
Click Save&lt;/p&gt;

&lt;p&gt;Once saved, hit the Sync button. Wait for the status to turn green — that means AWX has successfully pulled your playbooks from the repo.&lt;/p&gt;

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

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

&lt;p&gt;Step 6 — Create a Job Template and Launch It&lt;br&gt;
This is the exciting part — this is where you actually wire everything together and run your playbook.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Left pane → Resources → Templates&lt;/li&gt;
&lt;li&gt;Click Add → Add Job Template&lt;/li&gt;
&lt;li&gt;Enter Name, Job type check or run , inventory, project, crendetails&lt;/li&gt;
&lt;li&gt;Click Save&lt;/li&gt;
&lt;li&gt;Click Launch 🚀&lt;/li&gt;
&lt;/ul&gt;

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

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

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

&lt;p&gt;AWX will start running the job and you can watch the output in real time right from the UI. Every task, every host, every result — all visible on screen. No more tailing log files!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the Output Looks Like&lt;/strong&gt;&lt;br&gt;
Once the job runs, AWX shows you a full breakdown:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which tasks ran on which hosts&lt;/strong&gt;&lt;br&gt;
Green for ok, yellow for changed, red for failed&lt;br&gt;
Total summary at the end showing how many tasks succeeded or failed per host&lt;/p&gt;

&lt;p&gt;If something fails, you can click into the task and see the exact error — no guessing needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I Noticed Along the Way&lt;/strong&gt;&lt;br&gt;
The credential separation is really useful. Because credentials are stored separately, I can update a password in one place and every template using that credential picks it up automatically. No hunting through playbooks.&lt;br&gt;
&lt;strong&gt;The on/off host toggle is underrated&lt;/strong&gt;. Being able to disable a host in the inventory without deleting it saved me a few times when I wanted to test on just one machine.&lt;br&gt;
&lt;strong&gt;Always sync your project before creating a template&lt;/strong&gt;. If you create a template before syncing the project, the playbook dropdown will be empty. Sync first, then create the template — saves confusion.&lt;br&gt;
&lt;strong&gt;Check mode is your friend&lt;/strong&gt; Before running anything on production hosts, I always create the template with Job Type: Check first. It does a dry run and shows what would change without actually changing anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's Next&lt;/strong&gt;&lt;br&gt;
Now that I can run playbooks from AWX, the next things I want to cover are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workflows — chaining multiple playbooks together in AWX&lt;/li&gt;
&lt;li&gt;Schedules — running playbooks automatically at set times&lt;/li&gt;
&lt;li&gt;Notifications — getting alerts when jobs succeed or fail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're setting up AWX for the first time and getting stuck anywhere in these steps, drop it in the comments — happy to help!&lt;br&gt;
— Sireesha&lt;/p&gt;

</description>
      <category>automation</category>
      <category>beginners</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Syncing GitLab Projects to AWX — My DevOps Journey</title>
      <dc:creator>Sireesharaju Kamparaju</dc:creator>
      <pubDate>Tue, 19 May 2026 13:24:55 +0000</pubDate>
      <link>https://dev.to/sirisharaju_kamparaju_c9c/syncing-gitlab-projects-to-awx-my-devops-journeyby-sireesha-24b8</link>
      <guid>https://dev.to/sirisharaju_kamparaju_c9c/syncing-gitlab-projects-to-awx-my-devops-journeyby-sireesha-24b8</guid>
      <description>&lt;p&gt;If you've been following my blog series, you already know I've set up Ansible AWX on Kubernetes using Helm. Now the next logical step for me was — how do I actually get my playbooks into AWX without manually uploading them every time?&lt;br&gt;
The answer is GitLab. In this blog I'll walk through how I installed GitLab on a VM, pushed my Ansible playbooks to it, and then synced that repo into AWX as a project — in two ways:&lt;/p&gt;

&lt;p&gt;Using an SSH key (SCM Private Key) — this is what I actually use in production&lt;br&gt;
Using a Personal Access Token over HTTP — good for quick lab setups&lt;/p&gt;

&lt;p&gt;Let's get into it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What We're Building Here&lt;/strong&gt;&lt;br&gt;
The flow looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ansible Server → Push Playbooks → GitLab (self-hosted VM) → Sync → AWX Project &amp;amp; Templates&lt;br&gt;
Once this is set up, every time I update a playbook and push to GitLab, AWX syncs and picks up the latest version automatically. That's the real power of this setup.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Part 1 — Installing GitLab on a VM&lt;/strong&gt;&lt;br&gt;
I installed GitLab on a separate Ubuntu 22.04 VM. Here's exactly what I ran:&lt;br&gt;
Step 1 — Install Dependencies&lt;br&gt;
      &lt;code&gt;sudo apt update&lt;/code&gt;&lt;br&gt;
      &lt;code&gt;sudo apt install -y curl openssh-server ca-certificates tzdata perl&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Step 2 — Add GitLab Repository and Install&lt;/strong&gt;&lt;br&gt;
     &lt;code&gt;curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.deb.sh | sudo bash&lt;/code&gt;&lt;br&gt;
     &lt;code&gt;sudo apt install gitlab-ce&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Step 3 — Configure GitLab&lt;/strong&gt;&lt;br&gt;
     &lt;code&gt;sudo vi /etc/gitlab/gitlab.rb&lt;/code&gt;&lt;br&gt;
Update the external URL to your VM IP:&lt;br&gt;
rubyexternal_url '&lt;a href="http://10.*.*.**" rel="noopener noreferrer"&gt;http://10.*.*.**&lt;/a&gt;'&lt;br&gt;
Then reconfigure:&lt;br&gt;
      &lt;code&gt;sudo gitlab-ctl reconfigure&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Step 4 — Get the Initial Root Password&lt;/strong&gt;&lt;br&gt;
       &lt;code&gt;sudo cat /etc/gitlab/initial_root_password&lt;/code&gt;&lt;br&gt;
Open &lt;a href="http://10.*.*.*" rel="noopener noreferrer"&gt;http://10.*.*.*&lt;/a&gt; in your browser → log in with root and the password above.&lt;/p&gt;

&lt;p&gt;Tip: Change your root password immediately after first login — User Settings → Password.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2. Creating a repo and pushing playbooks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inside GitLab, I created a new blank project called:&lt;/p&gt;

&lt;p&gt;ansibleawx&lt;/p&gt;

&lt;p&gt;Set it to private and moved on.&lt;/p&gt;

&lt;p&gt;On my Ansible server, I configured Git:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git config --global user.name "Sireesha"&lt;/code&gt;&lt;br&gt;
&lt;code&gt;git config --global user.email "your@email.com"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then inside my playbook directory:&lt;br&gt;
&lt;/p&gt;

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

git init
git remote add origin http://ip/ansibleawx.git
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial commit - adding ansible playbooks"&lt;/span&gt;
git branch &lt;span class="nt"&gt;-M&lt;/span&gt; patches
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin patches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’m using a patches branch here because that’s what I decided to track in AWX later.&lt;/p&gt;

&lt;p&gt;That part matters more than it looks — branch mismatches will bite you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part3. Connecting AWX to GitLab using SSH (production way)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the setup I actually use properly. It’s more secure and avoids token expiry issues.&lt;/p&gt;

&lt;p&gt;Generate SSH key (on AWX side)&lt;br&gt;
&lt;code&gt;ssh-keygen -t rsa -b 4096 -C "awx@ansible"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then grab the public key:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cat ~/.ssh/id_rsa.pub&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Add it to GitLab&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GitLab → User Settings → SSH Keys → paste key → save&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Nothing complicated here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create AWX credential&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In AWX:&lt;/p&gt;

&lt;p&gt;Credentials → Add&lt;/p&gt;

&lt;p&gt;Name: gitlab-cred&lt;br&gt;
Type: Source Control&lt;br&gt;
Username: root&lt;br&gt;
SCM Private Key: paste private key (~/.ssh/id_rsa)&lt;/p&gt;

&lt;p&gt;Click Save.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create AWX project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWX → Projects → Add&lt;/p&gt;

&lt;p&gt;Name: AnsibleAWX&lt;br&gt;
SCM Type: Git&lt;br&gt;
URL: http:///ansibleawx.git&lt;br&gt;
Branch: patches&lt;br&gt;
Credential: gitlab-cred&lt;/p&gt;

&lt;p&gt;Once saved, AWX syncs automatically.&lt;/p&gt;

&lt;p&gt;When it turns green, you’re good.&lt;/p&gt;

&lt;p&gt;That’s the moment everything starts clicking.&lt;/p&gt;

&lt;p&gt;This is exactly the gitlab-cred credential I have set up — you can see from the screenshot it shows Credential Type: Source Control and SCM Private Key: Encrypted.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3vsc05yahkvlhe7ouxm8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3vsc05yahkvlhe7ouxm8.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Part4. Alternative: HTTP with Personal Access Token&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the quicker setup I used in a lab environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create token in GitLab&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitLab → Settings → Access Tokens&lt;/p&gt;

&lt;p&gt;Scope: read_repository&lt;/p&gt;

&lt;p&gt;Copy it (you only see it once).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add to AWX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Credentials → Add&lt;/p&gt;

&lt;p&gt;Name: gitlab-http-cred&lt;br&gt;
Username: root&lt;br&gt;
Password: &lt;/p&gt;

&lt;p&gt;Save.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same as before, but use this credential instead.&lt;/p&gt;

&lt;p&gt;It works fine — just not as clean or durable as SSH.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 5 — Creating Job Templates in AWX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the project sync works, playbooks automatically appear in AWX.&lt;/p&gt;

&lt;p&gt;So I created a simple job template:&lt;/p&gt;

&lt;p&gt;AWX → Templates → Add&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: Linux Server Setup&lt;/li&gt;
&lt;li&gt;Job Type: Run&lt;/li&gt;
&lt;li&gt;Inventory: your inventory&lt;/li&gt;
&lt;li&gt;Project: AnsibleAWX&lt;/li&gt;
&lt;li&gt;Playbook: select from dropdown&lt;/li&gt;
&lt;li&gt;Credentials: your machine credentials&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save and launch.&lt;/p&gt;

&lt;p&gt;At this point, AWX is basically running whatever is in GitLab.&lt;/p&gt;

&lt;p&gt;No file copying. No manual updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors I Hit Along the Way&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Error 1 — Git Push Branch Mismatch&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;error: src refspec main does not match any&lt;br&gt;
Fix — rename the branch to match what you set in AWX:&lt;br&gt;
&lt;code&gt;git branch -M patches&lt;br&gt;
git push -u origin patches&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Error 2 — AWX Sync Failed on SSH Host Key&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Host key verification failed&lt;br&gt;
Fix — in your AWX Project settings under Source Control Options, tick:&lt;br&gt;
✅ Disregard Host Checks&lt;br&gt;
&lt;strong&gt;Error 3 — Playbook Dropdown Empty in Job Template&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After syncing, the playbook dropdown was empty when creating a job template&lt;br&gt;
.&lt;br&gt;
Fix — go back to your Project → hit the Sync button (circular arrow) → wait for green → go back to template. Playbooks will appear.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before this, updating a playbook meant manually copying files and hoping AWX had the latest version. Now my workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Edit playbook → git push to patches branch → AWX syncs → Launch job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is version controlled, auditable, and consistent.&lt;/p&gt;

&lt;p&gt;Drop your questions in the comments — happy to help!&lt;br&gt;
— Sireesha&lt;/p&gt;

</description>
      <category>ansible</category>
      <category>awx</category>
      <category>devops</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Deploying Ansible AWX on Kubernetes Using Helm - By Sireesha</title>
      <dc:creator>Sireesharaju Kamparaju</dc:creator>
      <pubDate>Tue, 19 May 2026 10:51:57 +0000</pubDate>
      <link>https://dev.to/sirisharaju_kamparaju_c9c/deploying-ansible-awx-on-kubernetes-using-helm-6i5</link>
      <guid>https://dev.to/sirisharaju_kamparaju_c9c/deploying-ansible-awx-on-kubernetes-using-helm-6i5</guid>
      <description>&lt;p&gt;In this blog, I'm going to walk you through how I deployed Ansible AWX on a Kubernetes cluster using Helm. This was one of the most hands-on projects I've worked on — it involves setting up the K8s cluster from scratch, installing the container runtime, deploying Helm, and finally getting AWX up and running. I also hit a few real errors along the way, so I'll share exactly how I fixed them.&lt;br&gt;
It was one of those projects that looks simple on paper but turns into a chain of small problems once you actually start doing it.&lt;br&gt;
Here's the overall flow I followed:&lt;br&gt;
The goal was pretty straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up a Kubernetes cluster on Ubuntu 22.04&lt;/li&gt;
&lt;li&gt;Install containerd&lt;/li&gt;
&lt;li&gt;Get Helm working&lt;/li&gt;
&lt;li&gt;Deploy AWX using the AWX Operator&lt;/li&gt;
&lt;li&gt;Fix whatever broke along the way (this part took the longest 😅)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Setting Up the Kubernetes Cluster on Ubuntu 22.04&lt;/strong&gt;&lt;br&gt;
Before anything else, I want to quickly explain the two types of nodes in a K8s cluster since this matters for the setup:&lt;br&gt;
Master Node — manages the control plane, API calls, pods, services, and everything else in the cluster.&lt;br&gt;
Worker Node — runs the actual containers. Pods can spread across multiple worker nodes for better resource management.&lt;br&gt;
&lt;strong&gt;Step 1 — Update and Upgrade (All Nodes)&lt;/strong&gt;&lt;br&gt;
I started by logging in as root and running updates on all nodes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hostnamectl set-hostname siri-awx-01   # run on master node&lt;br&gt;
hostnamectl set-hostname siri-awx-02   # run on worker node&lt;br&gt;
exec bash&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
apt update&lt;br&gt;
apt upgrade&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Disable Swap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
swapoff -a&lt;br&gt;
sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Step 3 — Load Kernel Modules&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tee /etc/modules-load.d/containerd.conf &amp;lt;&amp;lt;EOF&lt;br&gt;
overlay&lt;br&gt;
br_netfilter&lt;br&gt;
EOF&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
modprobe overlay&lt;br&gt;
modprobe br_netfilter&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Configure Kernel Parameters&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;tee /etc/sysctl.d/kubernetes.conf &amp;lt;&amp;lt;EOF&lt;br&gt;
net.bridge.bridge-nf-call-ip6tables = 1&lt;br&gt;
net.bridge.bridge-nf-call-iptables = 1&lt;br&gt;
net.ipv4.ip_forward = 1&lt;br&gt;
EOF&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sysctl --system&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Installing Containerd Runtime (All Nodes)&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;apt install -y curl gnupg2 software-properties-common apt-transport-https ca-certificates&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmour -o /etc/apt/trusted.gpg.d/docker.gpg&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;apt update&lt;br&gt;
apt install -y containerd.io&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;containerd config default | sudo tee /etc/containerd/config.toml &amp;gt;/dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;br&gt;
&lt;code&gt;sed -i 's/SystemdCgroup \= false/SystemdCgroup \= true/g' /etc/containerd/config.toml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemctl restart containerd&lt;br&gt;
systemctl enable containerd&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Installing Kubernetes Components (All Nodes)&lt;/strong&gt;&lt;br&gt;
`sudo apt-get install -y apt-transport-https ca-certificates curl gpg&lt;/p&gt;

&lt;p&gt;curl -fsSL &lt;a href="https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key" rel="noopener noreferrer"&gt;https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key&lt;/a&gt; | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg`&lt;/p&gt;

&lt;p&gt;&lt;code&gt;echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sudo apt-get update&lt;br&gt;
sudo apt-get install -y kubelet kubeadm kubectl&lt;br&gt;
sudo apt-mark hold kubelet kubeadm kubectl&lt;/code&gt;&lt;/p&gt;

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

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

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

&lt;p&gt;&lt;strong&gt;Initialising the Cluster (Master Node — siri-awx-01 Only)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;systemctl enable kubelet &amp;amp;&amp;amp; systemctl start kubelet&lt;br&gt;
kubeadm init&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mkdir -p $HOME/.kube&lt;br&gt;
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config&lt;br&gt;
chown $(id -u):$(id -g) $HOME/.kube/config&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Tip: If you forget to save the kubeadm join output, regenerate it with:&lt;br&gt;
bashkubeadm token create --print-join-command&lt;/p&gt;

&lt;p&gt;$&lt;code&gt;kubectl get nodes&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Join Worker Node (siri-awx-02)&lt;/strong&gt;&lt;br&gt;
Run this on siri-awx-02 (use your actual token from kubeadm init output):&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubeadm join 192.168.237.144:6443 --token uhkvxh.mjp6aiyhy18vaxh0 \&lt;br&gt;
  --discovery-token-ca-cert-hash sha256:c0200907cc68957eea6d9cc4ad314282fb58ac92bceef2416e8212040441f130&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install Calico Network Plugin (siri-awx-01 Only)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/calico.yaml -O&lt;br&gt;
kubectl apply -f calico.yaml&lt;br&gt;
kubectl get nodes&lt;br&gt;
kubectl get pods -n kube-system&lt;/code&gt;&lt;/p&gt;

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

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

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

&lt;p&gt;&lt;strong&gt;Installing Helm&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3&lt;br&gt;
chmod 700 get_helm.sh&lt;br&gt;
./get_helm.sh&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Deploying Ansible AWX Using Helm&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;helm install ansible-awx-operator awx-operator/awx-operator -n awx --create-namespace&lt;br&gt;
kubectl get pods -n awx&lt;/code&gt;&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;Setting Up Storage — StorageClass, PV and PVC&lt;br&gt;
local-storage-class.yaml&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;apiVersion: storage.k8s.io/v1&lt;br&gt;
kind: StorageClass&lt;br&gt;
metadata:&lt;br&gt;
  name: local-storage&lt;br&gt;
  namespace: awx&lt;br&gt;
provisioner: kubernetes.io/no-provisioner&lt;br&gt;
volumeBindingMode: WaitForFirstConsumer&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;kubectl create -f local-storage-class.yaml&lt;br&gt;
kubectl get sc -n awx&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;pv.yaml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolume&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-pv&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;awx&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;capacity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2Gi&lt;/span&gt;
  &lt;span class="na"&gt;volumeMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Filesystem&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class="na"&gt;persistentVolumeReclaimPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete&lt;/span&gt;
  &lt;span class="na"&gt;storageClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local-storage&lt;/span&gt;
  &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/mnt/storage&lt;/span&gt; &lt;span class="c1"&gt;# Mount point should available on worker node&lt;/span&gt;
  &lt;span class="na"&gt;nodeAffinity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;nodeSelectorTerms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matchExpressions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes.io/hostname&lt;/span&gt;
          &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;In&lt;/span&gt;
          &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;siri-awx-02&lt;/span&gt;    &lt;span class="c1"&gt;# worker node hostname&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;strong&gt;pvc.yaml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-13-ansible-awx-postgres-13-0&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;awx&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;storageClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local-storage&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;code&gt;kubectl apply -f pv.yaml&lt;br&gt;
kubectl create -f pvc.yaml&lt;br&gt;
kubectl get pv,pvc -n awx&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Deploying AWX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;`kubectl create -f ansible-awx.yaml&lt;/p&gt;

&lt;p&gt;kubectl logs -f deployments/awx-operator-controller-manager -c awx-manager -n awx&lt;br&gt;
`&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;Accessing the AWX Web Interface&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl expose deployment ansible-awx-web --name ansible-awx-web-svc --type NodePort -n awx&lt;br&gt;
kubectl get svc ansible-awx-web-svc -n awx&lt;/code&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Get admin password
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;kubectl get secret ansible-awx-admin-password -o jsonpath="{.data.password}" -n awx | base64 --decode ; echo&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;AWX is now accessible at http://: — in my case port 32418. Log in with username admin and the decoded password.&lt;/p&gt;

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

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

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

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

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

&lt;p&gt;&lt;strong&gt;Errors I Hit and How I Fixed Them&lt;br&gt;
Error 1 — Node Taint Issue (Pods Stuck in Pending)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Warning  FailedScheduling  2m37s  default-scheduler&lt;br&gt;&lt;br&gt;
0/1 nodes are available: 1 node(s) had untolerated taint &lt;br&gt;
{node.kubernetes.io/not-ready: }&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, check the actual taint on your node:&lt;/strong&gt;&lt;br&gt;
kubectl describe node siri-awx-01 | grep Taints&lt;/p&gt;

&lt;h1&gt;
  
  
  Taints: node.kubernetes.io/not-ready:NoSchedule
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Remove it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;kubectl taint nodes siri-awx-01 node.kubernetes.io/not-ready:NoSchedule-&lt;/p&gt;

&lt;h1&gt;
  
  
  node/siri-awx-01 untainted
&lt;/h1&gt;

&lt;p&gt;After removing the correct taint, pods started coming up properly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2 — ImagePullSecret Error&lt;/strong&gt;&lt;br&gt;
FailedToRetrieveImagePullSecret  Unable to retrieve some image pull secrets &lt;br&gt;
(redhat-operators-pull-secret); attempting to pull the image may not succeed.&lt;/p&gt;

&lt;p&gt;Debug with:&lt;br&gt;
&lt;code&gt;kubectl logs awx-operator-controller-manager-6586fccbdd-xtdvj -n awx&lt;br&gt;
kubectl get events -n awx&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Fix — edit the deployment and comment out the imagePullSecrets section:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl edit deployment awx-operator-controller-manager -n awx&lt;/code&gt;&lt;br&gt;
Kubernetes automatically picks up the changes. No manual restart needed.&lt;br&gt;
&lt;strong&gt;Accessing the AWX Web Interface&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl expose deployment ansible-awx-web --name ansible-awx-web-svc --type NodePort -n awx&lt;br&gt;
kubectl get svc ansible-awx-web-svc -n awx&lt;/code&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Get admin password
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;kubectl get secret ansible-awx-admin-password -o jsonpath="{.data.password}" -n awx | base64 --decode ; echo&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final Verification&lt;/strong&gt;&lt;br&gt;
Once logged in, I ran a demo playbook to make sure everything was wired up correctly. Seeing that first successful job run in the AWX UI after all of this setup was genuinely satisfying.&lt;/p&gt;

&lt;p&gt;This wasn’t a smooth “follow steps → done” kind of project.&lt;/p&gt;

&lt;p&gt;It was more like:&lt;/p&gt;

&lt;p&gt;set up cluster → something breaks&lt;br&gt;
fix networking → something else breaks&lt;br&gt;
fix storage → something else breaks again&lt;br&gt;
finally get AWX running → feels like victory&lt;/p&gt;

&lt;p&gt;But I did learn a lot about how Kubernetes actually behaves when things aren’t ideal.&lt;/p&gt;

&lt;p&gt;If you’re doing this yourself and hit weird issues, you’re probably not alone—most of the fixes I found were pieced together from random threads and trial-and-error.&lt;/p&gt;

&lt;p&gt;Still worth it though.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;happy to help!
— Sireesha&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>kubernetes</category>
      <category>ansible</category>
    </item>
    <item>
      <title>Automating Windows Server Setup with Ansible: My DevOps Journey (Part 2)</title>
      <dc:creator>Sireesharaju Kamparaju</dc:creator>
      <pubDate>Tue, 12 May 2026 13:12:31 +0000</pubDate>
      <link>https://dev.to/sirisharaju_kamparaju_c9c/automating-windows-server-setup-with-ansible-my-devops-journey-part-2-92e</link>
      <guid>https://dev.to/sirisharaju_kamparaju_c9c/automating-windows-server-setup-with-ansible-my-devops-journey-part-2-92e</guid>
      <description>&lt;p&gt;In my previous blog, I covered how I automated Linux server provisioning using Ansible — things like SSH hardening, reusable roles, and playbooks. That setup worked really well, so the next thing I wanted to tackle was Windows.&lt;/p&gt;

&lt;p&gt;This post is about the Windows side of the setup — enabling WinRM, creating a reusable Windows role, and finally combining both Linux and Windows automation into a single Ansible workflow..&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows Automation Feels Different at First&lt;/strong&gt;&lt;br&gt;
I’ll be honest — moving from Linux automation to Windows automation was a little frustrating in the beginning.&lt;/p&gt;

&lt;p&gt;With Linux, Ansible over SSH just works. Most tutorials online assume that setup, so things feel straightforward pretty quickly. Windows was different.&lt;/p&gt;

&lt;p&gt;The first thing I learned was that Ansible talks to Windows through WinRM instead of SSH. That meant extra configuration before I could even run my first playbook.&lt;/p&gt;

&lt;p&gt;A few things tripped me up early on:&lt;/p&gt;

&lt;p&gt;WinRM isn’t enabled by default on fresh Windows servers&lt;br&gt;
The modules are completely different from Linux&lt;br&gt;
Even simple tasks use different syntax and collections&lt;br&gt;
Some errors from WinRM are vague and take time to troubleshoot&lt;/p&gt;

&lt;p&gt;At one point I thought my firewall rules were wrong, but it turned out I had messed up the WinRM transport settings in the inventory file.&lt;/p&gt;

&lt;p&gt;Once I got past those initial issues though, the setup became much smoother.&lt;br&gt;
Note: Port 5985 should be allowed in firewall.&lt;/p&gt;

&lt;p&gt;**First Thing — Bootstrap WinRM (Just Once)&lt;br&gt;
Before Ansible can do anything on a Windows server, WinRM needs to be enabled. I ran this PowerShell script once on each new Windows machine — after that, Ansible handles everything:&lt;/p&gt;

&lt;p&gt;[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12&lt;br&gt;
$url = "&lt;a href="https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1&lt;/a&gt;"&lt;br&gt;
$file = "$env:temp\ConfigureRemotingForAnsible.ps1"&lt;br&gt;
(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)&lt;br&gt;
powershell.exe -ExecutionPolicy ByPass -File $file&lt;/p&gt;

&lt;p&gt;The first successful win_ping honestly felt like progress because I’d already spent a while debugging connection issues before getting there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding Windows Hosts to the Inventory&lt;/strong&gt;&lt;br&gt;
I kept windows and linux in the same inventory file because I wanted one central place to manage everything.&lt;/p&gt;

&lt;p&gt;The structure stayed mostly the same, but the connection settings were obviously different:&lt;/p&gt;

&lt;p&gt;all:&lt;br&gt;
  children:&lt;br&gt;
    linux_servers:&lt;br&gt;
      hosts:&lt;br&gt;
        linux-01:&lt;br&gt;
          ansible_host: 10.0.1.10&lt;br&gt;
          ansible_user: ec2-user&lt;br&gt;
          ansible_ssh_private_key_file: ~/.ssh/id_rsa&lt;br&gt;
    windows_servers:&lt;br&gt;
      hosts:&lt;br&gt;
        win-01:&lt;br&gt;
          ansible_host: 10.0.2.10&lt;br&gt;
          ansible_user: Administrator&lt;br&gt;
          ansible_password: "{{ vault_win_password }}"&lt;br&gt;
          ansible_connection: winrm&lt;br&gt;
          ansible_winrm_transport: ntlm&lt;br&gt;
          ansible_port: 5985&lt;/p&gt;

&lt;p&gt;The Windows password is vaulted using ansible-vault — I never put credentials in plain text. That's just a habit I've built early on and I'd recommend everyone do the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building the Windows Role&lt;/strong&gt;&lt;br&gt;
I kept the same role-based structure I used for Linux. Here's how the Windows role looks:&lt;/p&gt;

&lt;p&gt;roles/&lt;br&gt;
  windows_setup/&lt;br&gt;
    ├── tasks/main.yml&lt;br&gt;
    └── defaults/main.yml&lt;/p&gt;

&lt;p&gt;roles/windows_setup/defaults/main.yml&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;p&gt;name: Ensure WinRM service is running and set to auto start&lt;br&gt;
ansible.windows.win_service:&lt;br&gt;
name: WinRM&lt;br&gt;
state: started&lt;br&gt;
start_mode: auto&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;name: Disable unencrypted WinRM traffic&lt;br&gt;
ansible.windows.win_shell: |&lt;br&gt;
winrm set winrm/config/service '@{AllowUnencrypted="false"}'&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;name: Configure Windows Firewall to allow WinRM&lt;br&gt;
ansible.windows.win_firewall_rule:&lt;br&gt;
name: WinRM HTTP&lt;br&gt;
localport: "{{ winrm_port }}"&lt;br&gt;
action: allow&lt;br&gt;
direction: in&lt;br&gt;
protocol: tcp&lt;br&gt;
state: present&lt;br&gt;
enabled: true&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;name: Check for available Windows Security Updates&lt;br&gt;
ansible.windows.win_updates:&lt;br&gt;
category_names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SecurityUpdates
state: searched
register: update_result&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;name: Display available updates&lt;br&gt;&lt;br&gt;
ansible.builtin.debug:&lt;br&gt;&lt;br&gt;
msg: "{{ update_result.updates | length }} security update(s) available"&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Windows Playbook&lt;/strong&gt;&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;name: Configure Windows Servers
hosts: windows_servers
roles:

&lt;ul&gt;
&lt;li&gt;windows_setup&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;One thing I noticed here — there's no become: true like I used on Linux. Windows doesn't use sudo. The Administrator account takes care of privilege escalation directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bringing It All Together — site.yml&lt;/strong&gt;&lt;br&gt;
This is the part I enjoyed the most. One playbook, one command, both Linux and Windows configured together:&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;import_playbook: playbooks/linux_setup.yml&lt;/li&gt;
&lt;li&gt;import_playbook: playbooks/windows_setup.yml&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;And to run everything:&lt;/strong&gt;&lt;br&gt;
ansible-playbook site.yml -i inventory/hosts.yml --ask-vault-pass&lt;/p&gt;

&lt;p&gt;That's it. Ansible runs through Linux first, then Windows — clean and consistent every single time.&lt;/p&gt;

&lt;p&gt;ansible windows_servers -i inventory/hosts.yml -m ansible.windows.win_ping&lt;/p&gt;

&lt;p&gt;If I get pong back, I know I'm good to go.&lt;br&gt;
&lt;strong&gt;WinRM transport depends on your environment&lt;/strong&gt;. I used ntlm since my servers weren't in a domain. If you're working in an Active Directory setup, kerberos is the better and more secure option.&lt;br&gt;
&lt;strong&gt;Don't mix Linux and Windows modules.&lt;/strong&gt; Early on I made the mistake of trying to use a Linux module on a Windows host — it fails and the error isn't always obvious. Stick to ansible.windows.* for everything Windows-related.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Changed After Automating This&lt;/strong&gt;&lt;br&gt;
Before automation, setting up a Windows server usually meant opening RDP, clicking through configuration screens, enabling services manually, and hoping I didn’t forget something important.&lt;/p&gt;

&lt;p&gt;Now the process is much simpler:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;add the server to inventory,&lt;/li&gt;
&lt;li&gt;run the playbook,&lt;/li&gt;
&lt;li&gt;wait a few minutes,&lt;/li&gt;
&lt;li&gt;done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That consistency alone made the effort worth it.&lt;/p&gt;

&lt;p&gt;Combined with the Linux setup from Part 1, I now have a single Ansible environment managing both Linux and Windows servers from one place. Getting both platforms working together took more troubleshooting than I expected, but finishing it felt genuinely rewarding.&lt;br&gt;
Coming Up in Part 3&lt;br&gt;
I'm planning to cover:&lt;/p&gt;

&lt;p&gt;User management across Linux and Windows&lt;br&gt;
Scheduling automated patching&lt;br&gt;
Plugging Ansible into a CI/CD pipeline&lt;/p&gt;

&lt;p&gt;Drop your questions or thoughts in the comments — always happy to discuss!&lt;br&gt;
— Sireesha&lt;/p&gt;

</description>
      <category>ansible</category>
      <category>automation</category>
    </item>
    <item>
      <title>Automating Linux &amp; Windows Server Setup with Ansible: My DevOps Journey</title>
      <dc:creator>Sireesharaju Kamparaju</dc:creator>
      <pubDate>Tue, 12 May 2026 13:01:58 +0000</pubDate>
      <link>https://dev.to/sirisharaju_kamparaju_c9c/automating-linux-windows-server-setup-with-ansible-my-devops-journey-4ig9</link>
      <guid>https://dev.to/sirisharaju_kamparaju_c9c/automating-linux-windows-server-setup-with-ansible-my-devops-journey-4ig9</guid>
      <description>&lt;p&gt;I started this project because managing servers manually was getting repetitive — especially when it came to setup and configuration across both Linux and Windows machines.&lt;/p&gt;

&lt;p&gt;So I decided to automate the whole workflow using Ansible. The goal wasn’t just to “use Ansible,” but to actually standardize how servers are configured and reduce the amount of manual SSH and RDP work I was doing every time a new system came up.&lt;/p&gt;

&lt;p&gt;In this blog, I’ll walk through how I used Ansible playbooks and roles to automate server setup, including SSH configuration on Linux and basic provisioning tasks across Windows systems.&lt;/p&gt;

&lt;p&gt;**&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Chose Ansible for This
&lt;/h2&gt;

&lt;p&gt;**&lt;br&gt;
Before I started using Ansible, server setup meant logging into each machine manually, running the same commands over and over, and hoping nothing was missed. One wrong step and the configuration was inconsistent across environments. Sound familiar?&lt;br&gt;
What drew me to Ansible was its simplicity — no agents to install, no complex setup. Just SSH into Linux, WinRM into Windows, and you're managing your entire fleet from a single control node. That agentless architecture was a game changer for me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Overview&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The goal was straightforward: automate the initial setup of both Linux and Windows servers using a clean, reusable Ansible structure. Here's what I set out to configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH hardening and configuration on Linux servers&lt;/li&gt;
&lt;li&gt;WinRM configuration to enable Ansible to talk to Windows servers&lt;/li&gt;
&lt;li&gt;Reusable roles for both OS types&lt;/li&gt;
&lt;li&gt;A master playbook to tie everything together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Setting Up the Project Structure&lt;/strong&gt;&lt;br&gt;
The first thing I did was organize everything into roles. Roles keep your code clean, reusable, and easy to share across projects. Here's the structure I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ansible-server-setup/
├── inventory/
│   ├── hosts.yml
├── roles/
│   ├── linux_ssh/
│   │   ├── tasks/main.yml
│   │   ├── templates/sshd_config.j2
│   │   ├── handlers/main.yml
│   │   └── defaults/main.yml
│   ├── windows_setup/
│   │   ├── tasks/main.yml
│   │   └── defaults/main.yml
├── playbooks/
│   ├── linux_setup.yml
│   ├── windows_setup.yml
└── site.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping Linux and Windows roles separate made everything much easier to maintain and debug independently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Inventory&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I defined both Linux and Windows hosts in a single YAML inventory, with the right connection settings for each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;linux_servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;linux-01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ansible_host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.1.10&lt;/span&gt;
          &lt;span class="na"&gt;ansible_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ec2-user&lt;/span&gt;
          &lt;span class="na"&gt;ansible_ssh_private_key_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.ssh/id_rsa&lt;/span&gt;
    &lt;span class="na"&gt;windows_servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;win-01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ansible_host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.2.10&lt;/span&gt;
          &lt;span class="na"&gt;ansible_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Administrator&lt;/span&gt;
          &lt;span class="na"&gt;ansible_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;vault_win_password&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="na"&gt;ansible_connection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;winrm&lt;/span&gt;
          &lt;span class="na"&gt;ansible_winrm_transport&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ntlm&lt;/span&gt;
          &lt;span class="na"&gt;ansible_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5985&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice I vaulted the Windows password using ansible-vault — never hardcode credentials in plain text. Learned that lesson early!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH Hardening Role for Linux&lt;/strong&gt;&lt;br&gt;
This was the core of my Linux setup. The linux_ssh role handles SSH daemon configuration to make servers secure and consistent from day one.&lt;br&gt;
roles/linux_ssh/defaults/main.yml&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ssh_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
&lt;span class="na"&gt;permit_root_login&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no"&lt;/span&gt;
&lt;span class="na"&gt;password_authentication&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no"&lt;/span&gt;
&lt;span class="na"&gt;max_auth_tries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;roles/linux_ssh/templates/sshd_config.j2&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;Port &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;ssh_port&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
PermitRootLogin &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;permit_root_login&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
PasswordAuthentication &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;password_authentication&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
MaxAuthTries &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;max_auth_tries&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy SSH configuration&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sshd_config.j2&lt;/span&gt;
    &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/ssh/sshd_config&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;root&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0600"&lt;/span&gt;
  &lt;span class="na"&gt;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restart SSH&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure SSH service is running and enabled&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sshd&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;roles/linux_ssh/handlers/main.yml&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restart SSH&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sshd&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restarted&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler ensures SSH only restarts when the config actually changes — not on every run. That small detail matters a lot in production.&lt;/p&gt;

</description>
      <category>ansible</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
