<?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: Ashish Nair</title>
    <description>The latest articles on DEV Community by Ashish Nair (@ashish_nair_d9b10ba4f8126).</description>
    <link>https://dev.to/ashish_nair_d9b10ba4f8126</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%2F2176306%2F7d07f10c-0efc-4f5d-a8cd-a6ed1a06b7ec.png</url>
      <title>DEV Community: Ashish Nair</title>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ashish_nair_d9b10ba4f8126"/>
    <language>en</language>
    <item>
      <title>I Built Hybrid DNS Between AWS and My On-Prem Linux Server -- Here's What Finally Made Route 53 Resolver Click</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Mon, 24 Aug 2026 17:13:50 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/i-built-hybrid-dns-between-aws-and-my-on-prem-linux-server-heres-what-finally-made-route-53-i7l</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/i-built-hybrid-dns-between-aws-and-my-on-prem-linux-server-heres-what-finally-made-route-53-i7l</guid>
      <description>&lt;h3&gt;
  
  
  Why I built this?
&lt;/h3&gt;

&lt;p&gt;I wanted to actually build a hybrid DNS environment and see the DNS queries travel between an on-premises environment and AWS.&lt;/p&gt;

&lt;p&gt;My lab ended up having two separate DNS requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A public DNS name for my application: &lt;strong&gt;app.xcv.publicvm.com&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A private DNS namespace inside AWS: &lt;strong&gt;aws.internal&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the 2 hosted zones I created in Route53:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fthqqpvel2sjx300iu95o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fthqqpvel2sjx300iu95o.png" alt=" " width="799" height="157"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interesting part was making my on-premises Linux DNS server — which is also my MariaDB server at 192.168.1.10 — resolve a private AWS RDS name: &lt;strong&gt;rds.aws.internal.&lt;/strong&gt;&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;The DNS pieces and why they exist:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.Public DNS - app.xcv.publicvm.com&lt;/strong&gt;&lt;br&gt;
In my Public hosted zone I created failover routing with 2 A records:&lt;br&gt;
    app.xcv.publicvm.com &lt;br&gt;
                - primary  -&amp;gt; 13.206.252.123&lt;br&gt;
                - failover -&amp;gt; 13.233.70.186&lt;br&gt;
This is the also the DNS record that my application lives on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.Private DNS - aws.internal&lt;/strong&gt;&lt;br&gt;
I created a private hosted zone:  aws.internal. This zone isn't resolvable publicly. This simply has a CNAME that points to my RDS' endpoint.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;3. On-premise DNS - onprem.example.com&lt;/strong&gt;&lt;br&gt;
My on-premises environment has a Linux server at 192.168.1.10. This is also my mariadb server (which is ofcourse not a good practice but laziness 😁)&lt;/p&gt;

&lt;p&gt;Now, we have 2 DNS authorities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On prem - onprem.example.com&lt;/li&gt;
&lt;li&gt;Route53 - aws.internal&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the good part is neither needs to become authoritative for the other environment (Because my DNS is not greedy like humans 😁). Instead, we forward queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now, The problem!&lt;/strong&gt;&lt;br&gt;
There has to be a problem for us to provide a solution! Imagine, an application that sits on-premise wants to resolve rds.aws.internal -&amp;gt; 192.168.1.10(A.K.A on-premise DNS) . This fails. Because our on-premise isn't authoritative for aws.internal(It's a private hosted zone on AWS).&lt;/p&gt;

&lt;p&gt;You can beat me up and say - "Add a static record on the On-premise DNS server".&lt;br&gt;
I'd say hold my beer, this is why my setup exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding a static entry is not a good hybrid architecture practice.&lt;/li&gt;
&lt;li&gt;RDS is owned by AWS, what if it changes the IP?&lt;/li&gt;
&lt;li&gt;And lastly, I'd say meet our friend - &lt;strong&gt;Route53 resolver Inbound Endpoint.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;what is a &lt;strong&gt;Route53 resolver Inbound Endpoint&lt;/strong&gt; ?&lt;br&gt;
An Inbound Resolver Endpoint is used when DNS queries are coming into AWS from an external network(External is our on-premise network).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The flow (Roughly!) is:&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The benefit&lt;/strong&gt; : our on-prem DNS doesn't need to know how AWS works the magic! It just forwards anything *.aws.internal to the resolver.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And this is the validation that it worked:&lt;/strong&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyj5jdkye7osxltjkzgn1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyj5jdkye7osxltjkzgn1.png" alt=" " width="476" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dns</category>
      <category>devops</category>
    </item>
    <item>
      <title>Migrating on-premise block-level workloads to the cloud</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Tue, 11 Aug 2026 17:20:53 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/migrating-on-premise-block-level-workloads-to-the-cloud-1023</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/migrating-on-premise-block-level-workloads-to-the-cloud-1023</guid>
      <description>&lt;p&gt;Migrating on-premises block-based workloads to the cloud can be a daunting task, especially when the workload contains a large amount of data. &lt;/p&gt;

&lt;p&gt;This is a follow-up to the site-to-site VPN setup I documented here &lt;a href="https://dev.to/ashish_nair_d9b10ba4f8126/aws-site-to-site-vpn-using-strongswan-1olk"&gt;https://dev.to/ashish_nair_d9b10ba4f8126/aws-site-to-site-vpn-using-strongswan-1olk&lt;/a&gt; . &lt;/p&gt;

&lt;p&gt;For this lab, I wanted to combine the best of both worlds: keep the workload on-premises while using AWS as the destination for storage, backup and recovery — and keep the connectivity private using the VPN and AWS private endpoints.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The Goal:
&lt;/h3&gt;

&lt;p&gt;My on-premise MariaDB server writes it's data to &lt;code&gt;/var/lib/myql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For this experiment, I wanted to explore two possibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep a copy of the block-based workload in AWS and use EBS snapshots for backup/recovery.&lt;/li&gt;
&lt;li&gt;Recover the block-based filesystem in AWS, attach it to an EC2 instance, and eventually move the compute workload to AWS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I configured the Storage Gateway appliance to communicate with AWS through the private connectivity already established by my Site-to-Site VPN and used AWS private endpoints for the AWS-side connectivity.&lt;/p&gt;

&lt;p&gt;This allowed me to keep the Storage Gateway traffic on private network paths rather than sending it over the public Internet.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Final test:
&lt;/h3&gt;

&lt;p&gt;Can I take the data that my on-premises MariaDB server was writing to, recover that storage in AWS, attach it to an EC2 instance, and verify that my database data is still there?&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just use mysqldump?
&lt;/h3&gt;

&lt;p&gt;A very reasonable question is: &lt;em&gt;Why not simply take a MariaDB/MySQL dump and restore it in AWS?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a database migration, that can certainly be a much simpler approach.&lt;/p&gt;

&lt;p&gt;However, that isn't what I wanted to experiment with here.&lt;/p&gt;

&lt;p&gt;Let's assume instead that we have a very large block-based filesystem containing a large amount of data.&lt;/p&gt;

&lt;p&gt;Rather than thinking about the contents of the filesystem as individual database objects or files, this experiment looks at the problem from a block-storage perspective.&lt;/p&gt;

&lt;p&gt;That makes Storage Gateway a much more interesting technology to explore.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does this work?
&lt;/h3&gt;

&lt;p&gt;The interesting part of this setup is that MariaDB doesn't need to know that AWS is involved.&lt;/p&gt;

&lt;p&gt;The Storage Gateway appliance exposes an iSCSI block device to the on-premises MariaDB server.&lt;/p&gt;

&lt;p&gt;From the MariaDB server's perspective, it simply sees a block device&lt;/p&gt;

&lt;p&gt;I scanned for the iSCSI target from the MariaDB server, logged into it, formatted the volume and mounted it as a normal filesystem.&lt;/p&gt;

&lt;p&gt;The application therefore continues to interact with the filesystem normally.&lt;/p&gt;

&lt;p&gt;Behind the scenes, the Storage Gateway handles the transfer of the data to AWS. For a cached volume, the gateway maintains local cache/upload-buffer storage and asynchronously transfers data to AWS.&lt;/p&gt;

&lt;p&gt;This is what makes the experiment interesting: the application is working with a normal block device while the underlying storage is being integrated with AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lab:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I first created a Storage Gateway in AWS and selected the appropriate gateway configuration for the experiment.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqwb79n5mdwfyoe5uvts.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxqwb79n5mdwfyoe5uvts.png" alt=" " width="800" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The next step required downloading the VMware appliance and starting it in my local VMware environment. The appliance provides the interface between my on-premises environment and AWS.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nwdj12cua1sgpfu49qm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nwdj12cua1sgpfu49qm.png" alt=" " width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The gateway appliance console looked something like this:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fze8qe8vbtnz9y4v3pe5r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fze8qe8vbtnz9y4v3pe5r.png" alt=" " width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I attached a 150G disk to the storage appliance. This storage is used by the gateway as part of the volume configuration and allows it to expose an iSCSI volume to the on-premises server.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatusrcjag8vsi5m4uzfa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatusrcjag8vsi5m4uzfa.png" alt=" " width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From the on-premises MariaDB server, I scanned for the iSCSI target exposed by the Storage Gateway appliance and logged into it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5uxn436yfj0jlkrki40b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5uxn436yfj0jlkrki40b.png" alt=" " width="798" height="75"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I formatted the newly discovered block device and mounted it on the MariaDB server.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizwims1ohs8276np7a80.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizwims1ohs8276np7a80.png" alt=" " width="746" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Once the filesystem was mounted, I generated some entries in the database. This gave me some known data that I could later use to verify whether the migration/recovery actually worked.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F31o74oe5y7vv27nath7c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F31o74oe5y7vv27nath7c.png" alt=" " width="799" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;After the volume was being used by the on-premises server, I could see the corresponding volume through the AWS Storage Gateway console.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyk8myuzd4b0a0wjrz2yn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyk8myuzd4b0a0wjrz2yn.png" alt=" " width="798" height="180"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I then created a snapshot of the Storage Gateway volume. This was the point where I could verify that AWS had successfully received the data. However, a successful snapshot alone isn't enough. I wanted to prove that the data could actually be recovered.&lt;/p&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fie5j8t54gpdmwvlphlks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fie5j8t54gpdmwvlphlks.png" alt=" " width="800" height="57"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; I used the snapshot to create an EBS volume and attached that volume to an EC2 instance.I then mounted the recovered filesystem on the EC2 instance.The final test was to inspect the MariaDB data.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5qmqrwos2kjqrxp48zij.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5qmqrwos2kjqrxp48zij.png" alt=" " width="799" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The database entries that I had created while the workload was running on-premises were present in the recovered volume in AWS.&lt;/p&gt;

&lt;p&gt;That gave me confidence that the complete path had worked. &lt;/p&gt;

&lt;p&gt;This experiment demonstrated that I could take a block-based workload being accessed from my on-premises environment, expose it through Storage Gateway, create a recovery point in AWS, recover that storage as an EBS volume and verify the data from an EC2 instance.&lt;/p&gt;

&lt;p&gt;The important part for me was that the storage abstraction was transparent to the application. MariaDB interacted with a normal filesystem and didn't need to know whether the underlying block storage was ultimately being integrated with AWS.&lt;/p&gt;

&lt;p&gt;However, this experiment should not be interpreted as demonstrating an application-consistent MariaDB backup strategy. For production database workloads, database-native replication, AWS DMS, or an application-consistent backup procedure may be more appropriate depending on the RPO/RTO requirements.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AWS site-to-site VPN using StrongSwan</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Mon, 03 Aug 2026 17:16:09 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/aws-site-to-site-vpn-using-strongswan-1olk</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/aws-site-to-site-vpn-using-strongswan-1olk</guid>
      <description>&lt;p&gt;It's a Sunday and I get a call from one of my ex-colleagues and he said "site-to-site VPN does not work when I want to reach VM's on your laptop because we are behind our ISP's NAT". And immediately I was like "Devops engineers and their Networking pitfalls". I popped open my beer and started setting this up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup is:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have an EC2 instance (10.100.11.0/24)&lt;/li&gt;
&lt;li&gt;I have 2 VM's on VMware Workstation.&lt;/li&gt;
&lt;li&gt;One of them is a strongswan VM which will act as the on-premise router.(192.168.1.9)&lt;/li&gt;
&lt;li&gt;Second One is a mariadb VM that the EC2 instance should be able to ping. (192.168.1.10)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Long-story-short version of system admins A.K.A - The architecture diagram&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The easiest part of the setup - &lt;strong&gt;The AWS side of configuration&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Virtual Private Gateway and associate it with your VPC.&lt;/li&gt;
&lt;li&gt;Identify your Public IP at the On-premise end and create the customer gateway in AWS. Enter your public ip here.&lt;/li&gt;
&lt;li&gt;Create a site-to-site VPN, associate it with the Virtual Private Gateway and the customer gateway we created in step 1 and 2.&lt;/li&gt;
&lt;li&gt;Download the configuration from the top-end of the page. This has the instructions to set up the tunnel interface on StrongSwan, along with other configurations.&lt;/li&gt;
&lt;li&gt;Launch a Ec2 instance in your VPC .&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trickiest part of the setup - &lt;strong&gt;The on-premise configuration&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the configuration we downloaded and make changes . We are actually making way for the IKE1 and IKE2 setups. I will be covering what are these in a separate document.&lt;/li&gt;
&lt;li&gt;Make changes to the ipsec.conf and ipsec.secrets on the strongswan VM. These settings are in the configuration we downloaded from AWS when we created the VPN. The settings should be something like:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ecfsmzmm58b3t7dpej1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ecfsmzmm58b3t7dpej1.png" alt=" " width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Create tunnel interface and assign IP's as mentioned in the downloaded file.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0g25puw2locaxv5swkqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0g25puw2locaxv5swkqa.png" alt=" " width="800" height="98"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Add sysctl settings mentioned in the file.It should have something like:&lt;br&gt;
&lt;code&gt;net.ipv4.conf.Tunnel1.rp_filter=2 #This value allows the Linux kernel to handle asymmetric routing&lt;br&gt;
net.ipv4.conf.Tunnel1.disable_policy=1 #This value disables IPsec policy (SPD) for the interface&lt;br&gt;
net.ipv4.conf.&amp;lt;PHYSICAL INTERFACE&amp;gt;.disable_xfrm=1 #This value disables crypto transformations on the physical interface&lt;br&gt;
net.ipv4.conf.&amp;lt;PHYSICAL INTERFACE&amp;gt;.disable_policy=1 #This value disables IPsec policy (SPD) for the interface&lt;br&gt;
net.ipv4.ip_forward = 1&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Flush routes:&lt;br&gt;
&lt;code&gt;ip route flush table 220&lt;br&gt;
ip route flush cache&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a route. Now, this is the CIDR where the EC2 instance lives. We are making this route so the packet destined for the ec2 instance goes via the tunnel interface.&lt;br&gt;
&lt;code&gt;ip route add 192.168.0.0/16 via 169.254.51.57 dev vti1&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add a route to the Ec2 instance's route table. This route tells the packet to travel via the Virtual Private Gateway if the destination network is our on-premise network.&lt;/p&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9x13f6bzbfctjsarpso.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft9x13f6bzbfctjsarpso.png" alt=" " width="799" height="28"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And If we have everything in place our Ec2 should be able to ping our on-premise strongswan VM:&lt;/p&gt;

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

&lt;p&gt;Now, this strongswan is the gateway at the on-premise end. So in order for the EC2 to be able to reach the mariadb instance we have to add a route. A return route to be specific.&lt;/p&gt;

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

&lt;p&gt;And let's test the connectivity. Notice the difference in the number of hops when we ran traceroute to 192.168.1.9 and 192.168.1.10 &lt;/p&gt;

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

&lt;p&gt;And obviously, ping will also work:&lt;/p&gt;

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

&lt;p&gt;While the configuration worked, I must admit it didn't work in the first try or even or the 4th one. &lt;/p&gt;

&lt;p&gt;I will create a follow-up article how I troubleshot the AWS side and the On-premise end.  &lt;/p&gt;

</description>
    </item>
    <item>
      <title>From Copy-Pasting YAML to Building My Own Kubernetes Manifest Studio</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Sun, 28 Jun 2026 07:44:00 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/from-copy-pasting-yaml-to-building-my-own-kubernetes-manifest-studio-5d3d</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/from-copy-pasting-yaml-to-building-my-own-kubernetes-manifest-studio-5d3d</guid>
      <description>&lt;p&gt;Over the last few months I've been spending a lot of time building for  Kubernetes and OpenShift while working as a Platform Engineer. Almost every lab started the same way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Deployment.&lt;/li&gt;
&lt;li&gt;Create a Service.&lt;/li&gt;
&lt;li&gt;Create a ConfigMap.&lt;/li&gt;
&lt;li&gt;Maybe a Secret.&lt;/li&gt;
&lt;li&gt;Sometimes an Ingress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repeat.&lt;/p&gt;

&lt;p&gt;Then do the same thing again for another application.&lt;/p&gt;

&lt;p&gt;An idea stemmed out of a conversation I had with a developer friend who seemed visibly frustrated over this. To be honest, he had fair point - Remembering how to right a manifest to test their application than to concentrate on writing a better application. IDE's help up to an extent but I didn't want to argue. So I thought what if we can develop something that has a UI, asks for options and generates a YAML out of it. I had written something in perl more than a decade back to generate an Linux kickstart file which were on similar lines.(At that moment Perl was a thing!)&lt;/p&gt;

&lt;p&gt;I thought why do something similar to that and write something in Python instead(Also, since I'm learning python it made sese to choose python). And something that can assimilate into the current GitOps flow. &lt;/p&gt;

&lt;p&gt;The idea was never to replace a dashboard . Just provide a minimalistic approach to the developers and engineers whilst they concentrate on their niche instead of pulling their hair over kubernetes manifest syntax. &lt;/p&gt;

&lt;p&gt;The final architecture:&lt;/p&gt;

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

&lt;p&gt;The application currently uses Python's built-in HTTP server together with HTML, CSS, JavaScript and SQLite.&lt;br&gt;
Also, it's debatable why i didn't chose Django. More on it some other day😁. &lt;/p&gt;

&lt;p&gt;The code lives here: &lt;a href="https://github.com/ashishnair86/mani" rel="noopener noreferrer"&gt;https://github.com/ashishnair86/mani&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since the application itself is intended for Kubernetes users, it made sense to package it as a Docker container.&lt;/p&gt;

&lt;p&gt;The image can be found here: &lt;a href="https://hub.docker.com/r/ashishnair/manifest-studio" rel="noopener noreferrer"&gt;https://hub.docker.com/r/ashishnair/manifest-studio&lt;/a&gt;. As of now, a docker image does not make that much of sense as the app itself does not require anything more than the standard python modules. Or a moo-point as Joey Tribbiani would have said 😂&lt;/p&gt;

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

&lt;p&gt;The first version intentionally focuses on Deployments and the resources commonly created alongside them.&lt;/p&gt;

&lt;p&gt;Current features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployment generation&lt;/li&gt;
&lt;li&gt;Namespace generation&lt;/li&gt;
&lt;li&gt;Service generation&lt;/li&gt;
&lt;li&gt;ConfigMap generation&lt;/li&gt;
&lt;li&gt;Secret generation&lt;/li&gt;
&lt;li&gt;Ingress generation&lt;/li&gt;
&lt;li&gt;Horizontal Pod Autoscaler generation&lt;/li&gt;
&lt;li&gt;Optional readiness and liveness probes&lt;/li&gt;
&lt;li&gt;User authentication&lt;/li&gt;
&lt;li&gt;Manifest history&lt;/li&gt;
&lt;li&gt;YAML download&lt;/li&gt;
&lt;li&gt;GitHub integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Few words about the UI:&lt;/p&gt;

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

&lt;p&gt;Whatever entries you make in the left pane will reflect immediately in the right pane. &lt;/p&gt;

&lt;p&gt;Once you register, it also keep an history of your manifests if you select "Save History" :&lt;/p&gt;

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

&lt;p&gt;Push to github allows to you do exactly what it says:&lt;/p&gt;

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

&lt;p&gt;And if your CI/CD tool is configured poll SCM or If you configured webhooks your pipeline should kick-off after the commit:&lt;/p&gt;

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

&lt;p&gt;I deliberately kept the scope small. Building one feature well teaches far more than trying to support every Kubernetes resource from day one.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>python</category>
    </item>
    <item>
      <title>From Metrics to Mitigation: Protecting my Application with NGINX Rate Limiting</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Thu, 18 Jun 2026 17:02:35 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/from-metrics-to-mitigation-protecting-my-application-with-nginx-rate-limiting-52n8</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/from-metrics-to-mitigation-protecting-my-application-with-nginx-rate-limiting-52n8</guid>
      <description>&lt;p&gt;In my previous article, I integrated Spring Boot Actuator with Prometheus and Grafana to gain visibility into application behavior. The goal was simple: expose metrics, scrape them with Prometheus, and visualize them in Grafana.&lt;/p&gt;

&lt;p&gt;What I didn't expect was that the metrics would immediately influence architectural decisions.&lt;/p&gt;

&lt;p&gt;After enabling metrics and exploring Prometheus, I started looking at request patterns using PromQL queries.&lt;/p&gt;

&lt;p&gt;One metric in particular stood out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="nf"&gt;sum&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt; &lt;span class="n"&gt;http_server_requests_seconds_count&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This showed the total number of requests received by each endpoint. &lt;/p&gt;

&lt;p&gt;The results were interesting(As an Admin not as someone who built this lab and send traffic on-purpose). &lt;/p&gt;

&lt;p&gt;I thought (hypothetically), what if the Authentication and API endpoints were receiving significantly more traffic than expected? What if someone is sending more traffic than expected, what if someone is intentionally flooding the endpoints? (It could be my wife for not buying her gold last weekend 😂)&lt;/p&gt;

&lt;p&gt;A login endpoint could be of the first targets during a brute-force attack, credential stuffing attempt, or denial-of-service event(Only a low esteemed hacker would be interested in my app 😂. But in production environments this is a real issue)&lt;/p&gt;

&lt;p&gt;So the problem now is all legitimate and illegitimate requests are reaching my app and I'm wasting time processing the request, because the flow is:&lt;/p&gt;

&lt;p&gt;Client  -&amp;gt;   Application   -&amp;gt;   MariaDB&lt;/p&gt;

&lt;p&gt;So, I thought of introducing a new member to the family, tada - it's Nginx(Actually, reverse proxy). So now, the flow is :&lt;/p&gt;

&lt;p&gt;client  -&amp;gt;  nginx  -&amp;gt; App  -&amp;gt; MariaDB&lt;/p&gt;

&lt;p&gt;This architecture gave me several advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A single entry point for all my services:

&lt;ul&gt;
&lt;li&gt;Spring App&lt;/li&gt;
&lt;li&gt;Prometheus&lt;/li&gt;
&lt;li&gt;Grafana&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Centralized routing. Meaning, all of the above will reach my reverse proxy which will forward the traffic to the correct endpoint.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reverse proxy capabilities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rate-limiting (In Gen-z lingo, This is our main-character)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is how i configured nginx for "Rate-limiting". &lt;/p&gt;

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

&lt;p&gt;In simple words, this config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;allows 5 requests per minute.&lt;/li&gt;
&lt;li&gt;Burst of 2 additional requests. (Because people make mistakes and having tolerations only helps)&lt;/li&gt;
&lt;li&gt;Shouts a 429 when limits exceed ( Like a typical parent )&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's test this out. Here I'm using a for-loop - A system admin's better half.&lt;/p&gt;

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

&lt;p&gt;This started shouting at the 4th request:&lt;/p&gt;

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

&lt;p&gt;But we configured nginx to tolerate 5 requests per minute right? Here's the math:&lt;/p&gt;

&lt;p&gt;5 requests per minute = 1 request in 12 seconds.&lt;br&gt;
Here 3 requests came in probably 3 seconds which means it allowed 1 request and already spent the 2 burst tokens it had it had since it happened within 3 seconds . Now, it did not have more burst tokens to assign it could not tolerate the 4th request and it blocked the request. It will block until a new token arrives every 12 seconds. &lt;/p&gt;

&lt;p&gt;3 cheers to Nginx's Leaky Bucket Algorithm!! And 2 beers for me for breaking this down!! &lt;/p&gt;

&lt;p&gt;The most important part is NGINX allowed the initial requests and began rejecting excess traffic before it ever reached our application. &lt;/p&gt;

&lt;p&gt;With this setup the biggest learning is not the configuration itself but what we can improve on by enabling metrics, those dashboards have a meaning. In this case it forced an architectural change even if it's just adding one small component. &lt;/p&gt;

&lt;p&gt;It's also important to know what endpoints to not limit. In my case, I only wanted to limit the /auth/register endpoint as only authenticated requests will reach my /students endpoint. If I limit my /students endpoint it will be like shooting myself in the foot. Also, I didn't want to limit my actuator endpoints as prometheus scrapes it every 15s.&lt;/p&gt;

&lt;p&gt;You can configure dashboards in Grafana by connecting it to your prometheus instance to make you life easier. &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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuv5jb23bozf2j5mtbp44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuv5jb23bozf2j5mtbp44.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Spring Boot + Prometheus: A Practical Introduction to Application Metrics</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Wed, 10 Jun 2026 17:38:23 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/spring-boot-prometheus-a-practical-introduction-to-application-metrics-50d0</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/spring-boot-prometheus-a-practical-introduction-to-application-metrics-50d0</guid>
      <description>&lt;p&gt;A developer spends a lot of time building features, but very little time asking an important question:&lt;/p&gt;

&lt;p&gt;How healthy is our application when it is running? To be very specific we need to answer the below questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many requests are we serving?&lt;/li&gt;
&lt;li&gt;How much memory are we using?&lt;/li&gt;
&lt;li&gt;Is our database connection pool exhausted?&lt;/li&gt;
&lt;li&gt;Are users encountering errors?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fortunately, Spring Boot and Prometheus make it surprisingly easy to answer these questions. &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%2Fxtnk9rbf6lojgfnxlcmz.webp" 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%2Fxtnk9rbf6lojgfnxlcmz.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we'll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable Prometheus metrics in a Spring Boot application&lt;/li&gt;
&lt;li&gt;Configure Prometheus to scrape those metrics&lt;/li&gt;
&lt;li&gt;Explore the most useful metrics exposed by Spring Boot&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Enable Prometheus metrics in a Spring Boot application
&lt;/h3&gt;

&lt;p&gt;Enabling prometheus metrics is a 2-step process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring Boot uses Micrometer as its metrics facade. To expose metrics in a Prometheus-compatible format, add the following dependencies in pom.xml&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%2F39x10eiez6lqlh1oq8gq.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%2F39x10eiez6lqlh1oq8gq.png" alt=" " width="630" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Actuator provides production-ready endpoints, while Micrometer translates metrics into a format Prometheus understands.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expose the Prometheus Endpoint. Add the following properties to application.properties:&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%2Fwqyzrsovduaosmas2kyp.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%2Fwqyzrsovduaosmas2kyp.png" alt=" " width="657" height="58"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you start your application, you should be able to see the metrics in the /actuator/prometheus endpoint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft57f27kr5191z9x4udyd.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%2Ft57f27kr5191z9x4udyd.png" alt=" " width="798" height="166"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now our application is exposing operational data that we can scrape using Prometheus.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Prometheus to scrape those metrics
&lt;/h3&gt;

&lt;p&gt;configure prometheus.yml with the "target" we want to scrape, metric path and a scrape interval.&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%2F10b1xey9jgzk4w0adyl8.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%2F10b1xey9jgzk4w0adyl8.png" alt=" " width="523" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Explore the most useful metrics exposed by Spring Boot
&lt;/h3&gt;

&lt;p&gt;At this point, Prometheus is scraping metrics every 15 seconds. Let's take a look at some important ones and query using PromQL:&lt;/p&gt;

&lt;p&gt;jvm_memory_used_bytes&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%2Fsyfcisfasrwdaxq77sgz.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%2Fsyfcisfasrwdaxq77sgz.png" alt=" " width="799" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This tells us how much heap memory the JVM is currently using.&lt;/p&gt;

&lt;p&gt;Let's check how many '403's we have(unauthorized requests):&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%2F3oqkgiekvmq1r5rdgo68.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%2F3oqkgiekvmq1r5rdgo68.png" alt=" " width="800" height="128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How many requests are made to each endpoint in the last couple of hours?&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%2F6itu7mczfdvne62ozcjh.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%2F6itu7mczfdvne62ozcjh.png" alt=" " width="800" height="190"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without observability such metrics often go unnoticed. Usually when there's a firefighting situation these queries are like gold.&lt;/p&gt;

&lt;p&gt;Traditionally, developers(and lot of Admins like me) focus on logs. Observability encourages us to ask different questions:&lt;/p&gt;

&lt;p&gt;Is the application healthy?&lt;br&gt;
Are requests succeeding?&lt;br&gt;
Are users experiencing failures?&lt;br&gt;
Is the database becoming a bottleneck?&lt;br&gt;
Is the JVM under stress?&lt;/p&gt;

&lt;p&gt;Metrics provide the answers.&lt;/p&gt;

&lt;p&gt;In the next article, we'll connect Prometheus to Grafana and build dashboards that allow us to visualize these metrics in real time.&lt;/p&gt;

</description>
      <category>prometheus</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>From CI/CD to AI-Powered DevSecOps: Teaching a Local LLM to Analyze Security Reports</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Tue, 26 May 2026 16:18:54 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/from-cicd-to-ai-powered-devsecops-teaching-a-local-llm-to-analyze-security-reports-2ejj</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/from-cicd-to-ai-powered-devsecops-teaching-a-local-llm-to-analyze-security-reports-2ejj</guid>
      <description>&lt;p&gt;In the previous &lt;a href="https://dev.to/ashish_nair_d9b10ba4f8126/building-a-spring-boot-monolith-application-and-a-devsecops-pipeline-around-it-2dah"&gt;article&lt;/a&gt;, I built a complete CI pipeline around a Spring Boot monolith: GitHub → Jenkins → SonarQube → Trivy → Nexus → Docker. I introduced quality gates, security scanning, and artifact management.&lt;/p&gt;

&lt;p&gt;But at the end of it, I had more questions than answers. The most important one being - What happens after Trivy generates the security report and uploads it to a nexus repository ? Do people actually go through the entire report? &lt;/p&gt;

&lt;p&gt;In this article, we'll continue from the previous project and build an AI-powered DevSecOps workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jenkins generates Trivy security reports&lt;/li&gt;
&lt;li&gt;Reports are uploaded into Nexus&lt;/li&gt;
&lt;li&gt;Jenkins triggers an n8n webhook&lt;/li&gt;
&lt;li&gt;n8n downloads the report&lt;/li&gt;
&lt;li&gt;Local Ollama + phi3 analyzes vulnerabilities(smaller LLM because of compute restraints)&lt;/li&gt;
&lt;li&gt;AI summarizes HIGH/CRITICAL findings&lt;/li&gt;
&lt;li&gt;Email notifications are generated automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Architecture:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frjm5rpsfdk503o3kv026.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%2Frjm5rpsfdk503o3kv026.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build application&lt;/li&gt;
&lt;li&gt;SonarQube analysis&lt;/li&gt;
&lt;li&gt;Quality Gate validation&lt;/li&gt;
&lt;li&gt;Docker build&lt;/li&gt;
&lt;li&gt;Trivy security scan&lt;/li&gt;
&lt;li&gt;Upload reports to Nexus&lt;/li&gt;
&lt;li&gt;Trigger AI workflow&lt;/li&gt;
&lt;li&gt;Download report dynamically&lt;/li&gt;
&lt;li&gt;Ask local LLM to analyze findings&lt;/li&gt;
&lt;li&gt;Send AI-generated recommendations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI component in this setup answers the "What if's" questions that I had before. Before I move to the setup, I want to justify why I added the layer of AI . Let's be fair to the security guys here - Security reports can be long and noisy. It can have dozens of CVE's, dependent vulnerabilities and what not. Having them accountable for any miss sounds unfair. Here's where AI can help, It's like telling someone "read this report and give me the top 10 things i should be worried about in 10 seconds".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sneak Peak of the setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ran ollama locally using its docker image. Locally, because i wanted to avoid external API's and also because of poverty (😂)&lt;/li&gt;
&lt;li&gt;Setup n8n also locally using its docker image. This is where we orchestrate the entire process using workflows.

&lt;ul&gt;
&lt;li&gt;The jenkins job calls the webhook that n8n listens on.&lt;/li&gt;
&lt;li&gt;N8n kicks off the workflow by : Fetching the trivy report -&amp;gt; The next step is to prepare the AI prompt -&amp;gt; Send to Ollama and transform the response -&amp;gt; Send the report via email&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;This is how my workflow looks in n8n UI:&lt;/strong&gt;&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%2Feifdg89lwkkptkq1x0f1.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%2Feifdg89lwkkptkq1x0f1.png" alt=" " width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The transformed summary of the vulnerability report:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgq8t45mzvxy2ayomfe9w.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%2Fgq8t45mzvxy2ayomfe9w.png" alt=" " width="800" height="770"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Further improvement is to train our LLM to be a junior SRE. &lt;/p&gt;

</description>
      <category>ai</category>
      <category>cicd</category>
      <category>nexus</category>
      <category>trivy</category>
    </item>
    <item>
      <title>Building a Spring Boot Monolith Application and a DevSecOps Pipeline Around It</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Wed, 20 May 2026 17:51:31 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/building-a-spring-boot-monolith-application-and-a-devsecops-pipeline-around-it-2dah</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/building-a-spring-boot-monolith-application-and-a-devsecops-pipeline-around-it-2dah</guid>
      <description>&lt;p&gt;A lot of CI/CD tutorials show a simple “Hello World” app and a Jenkins job that prints Build Successful. That never felt close to how systems work in real environments. At least, they never helped me. &lt;/p&gt;

&lt;p&gt;So I decided to build a small University Management Spring Boot monolith application with JWT authentication and protected APIs, and then build a complete pipeline around it — from source control to deployment, quality checks, security scans, artifact management and validation. Since I'm not a developer, the code itself isn't great it was just me exploring a new language along with some help from AI (Needless to say!)&lt;/p&gt;

&lt;p&gt;The application itself is intentionally simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JWT-based authentication (/auth/login)&lt;/li&gt;
&lt;li&gt;Protected APIs (/students)&lt;/li&gt;
&lt;li&gt;Spring Security integration&lt;/li&gt;
&lt;li&gt;Maven build and Docker packaging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal wasn't to show how to build an application (In that case, this would be the worst tutorial). Th goal is, to show a CI/CD(partly) flow to someone in less than 15 minutes.&lt;/p&gt;

&lt;p&gt;So I created a mini DevSecOps pipeline that includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub webhooks&lt;/li&gt;
&lt;li&gt;Jenkins Controller + Agent&lt;/li&gt;
&lt;li&gt;SonarQube quality gates&lt;/li&gt;
&lt;li&gt;Docker image creation&lt;/li&gt;
&lt;li&gt;Trivy security scans&lt;/li&gt;
&lt;li&gt;Nexus artifact storage&lt;/li&gt;
&lt;li&gt;Automated deployment using Docker Compose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just incase, anyone's interested in the code, it stays &lt;a href="https://github.com/ashishnair86/spring-app/tree/university-monolith" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By the end, a Git push automatically builds, scans, deploys and validates the application.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;The diagram: *&lt;/em&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%2Fjr0s107yt7idkn38xcev.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%2Fjr0s107yt7idkn38xcev.png" alt=" " width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The App:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdwwhnv011cpc595zuor.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%2Fxdwwhnv011cpc595zuor.png" alt=" " width="296" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Register using the &lt;code&gt;/auth/register&lt;/code&gt; API
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;[root@server1 config]#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:8081/auth/register?username=ram&amp;amp;password=redhat123"&lt;/span&gt;
&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Pass the credentials to the &lt;code&gt;/auth/login&lt;/code&gt; API to login . The App will return a JWT token, which will then be used to interact with the API.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;[root@server1 config]#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;"http://localhost:8081/auth/login?username=ram&amp;amp;password=redhat123"
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyYW0iLCJpYXQiOjE3NzkxNzA4MjIsImV4cCI6MTc3OTE3NDQyMn0.4AwQQqEa07f_-YId7LSlXYnctaWf7TdqG6xPYxTum_4

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This step is actually not required, just a part of convenience.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;[root@server1 config]#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJyYW0iLCJpYXQiOjE3NzkxNzA4MjIsImV4cCI6MTc3OTE3NDQyMn0.4AwQQqEa07f_-YId7LSlXYnctaWf7TdqG6xPYxTum_4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create entries using the &lt;code&gt;JWT token&lt;/code&gt; to authenticate using the &lt;code&gt;/students&lt;/code&gt; API.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;[root@server1 config]#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;http://localhost:8081/students \
&lt;/span&gt;&lt;span class="gp"&gt;-H "Authorization: Bearer $&lt;/span&gt;TOKEN&lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="go"&gt;-H "Content-Type: application/json" \
-d '{
"name":"John",
"email":"john@test.com"
}'
{"id":1,"name":"John","email":"john@test.com"}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Query the API using the &lt;code&gt;token&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;[root@server1 config]#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://localhost:8081/students&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;
&lt;span class="go"&gt;[{"id":1,"name":"John","email":"john@test.com"}]

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The pipeline:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We will make use of Jenkins to run the pipeline.&lt;/li&gt;
&lt;li&gt;I have chosen to run the jobs off of a jenkins agent, simply to separate  orchestration and execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The very first stage of the pipeline after the developer pushes the code is  to &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checkout the code from source control&lt;/li&gt;
&lt;li&gt;Build the application using Maven.&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%2Fmdbzq4v273g0w96d4n9y.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%2Fmdbzq4v273g0w96d4n9y.png" alt=" " width="777" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The build generates a jar file, which is quite famously referred to as the artifact.&lt;/li&gt;
&lt;li&gt;Now, this artifact is scanned by our quality gates which checks it for any code smells, Bugs, Vulnerabilities, etc.&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%2F2190jb01f0w9ed53uhky.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%2F2190jb01f0w9ed53uhky.png" alt=" " width="662" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The sonarqube interface shows something like:&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%2Fp5z43cso9ibfn5u822mx.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%2Fp5z43cso9ibfn5u822mx.png" alt=" " width="800" height="117"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My 2 cents:&lt;/strong&gt; Do not forget to add Jenkins call back in Sonarqube, else the job in Jenkins will wait to get the status from sonarqube and will eventually timeout.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If our code passes quality gates, we build the docker image and bake the artifact into the image.&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%2Fiymh9jntg0pm16wizpta.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%2Fiymh9jntg0pm16wizpta.png" alt=" " width="745" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Earlier, we scanned for vulnerabilities, bugs in the code. Now, it's time to scan for vulnerabilities in the docker image. I used Trivy for this. The ideal way to do this is to exit the job when there are vulnerabilities in the image. But for this lab I'm just running the scan as the image i pulled had loads of them.&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%2Fpefozuvk8iu06qzo46ye.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%2Fpefozuvk8iu06qzo46ye.png" alt=" " width="681" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Additionally, I also posted the scan results on a nexus repository with the job number appended.&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%2Fz690tjo6bkuxo7ou9q9f.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%2Fz690tjo6bkuxo7ou9q9f.png" alt=" " width="800" height="166"&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%2Fqnxppef4bv7xj1h9mgaw.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%2Fqnxppef4bv7xj1h9mgaw.png" alt=" " width="800" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is the smallest CD part of the pipeline where we bring up the app using the image we built earlier using docker-compose.&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%2Fxj5nxfp1a2eu8l3rp8uk.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%2Fxj5nxfp1a2eu8l3rp8uk.png" alt=" " width="800" height="155"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finally, keeping the artifact on the agent is not a good idea, so I moved it to a Nexus repo and ran a cleanup of the workspace to remove it from the agent.&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%2Fn4nthtgv6z2931u8tkxh.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%2Fn4nthtgv6z2931u8tkxh.png" alt=" " width="800" height="279"&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%2Fg0qrzb93vmdmbakhfeyc.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%2Fg0qrzb93vmdmbakhfeyc.png" alt=" " width="640" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Future enhancements:&lt;/strong&gt; I must admit, this document is incomplete without :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;converting this to a microservice&lt;/li&gt;
&lt;li&gt;Moving the application to K8s or Openshift.&lt;/li&gt;
&lt;li&gt;Adding monitoring for our App.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>security</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Integrating FreeIPA (LDAP) with OpenShift + Automated Group Sync Using CronJob</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Thu, 14 May 2026 03:59:42 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/integrating-freeipa-ldap-with-openshift-automated-group-sync-using-cronjob-2boe</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/integrating-freeipa-ldap-with-openshift-automated-group-sync-using-cronjob-2boe</guid>
      <description>&lt;p&gt;Managing users and groups centrally is critical in enterprise Kubernetes environments. In this guide, I’ll walk you through how I integrated FreeIPA (LDAP) with OpenShift and set up an automated CronJob to sync groups periodically.&lt;/p&gt;

&lt;p&gt;This setup ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Centralized authentication via FreeIPA&lt;/li&gt;
&lt;li&gt;Automatic onboarding of users&lt;/li&gt;
&lt;li&gt;Continuous group synchronization inside OpenShift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Architecture:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfk8n89dr5u3gj5x91x1.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%2Fyfk8n89dr5u3gj5x91x1.png" alt=" " width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And now, the subtitles for the Architecture!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;User Login&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developer logs in via OpenShift Console or CLI (&lt;code&gt;oc login&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Authentication via LDAP/FreeIPA&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenShift OAuth server uses the configured LDAP Identity Provider&lt;/li&gt;
&lt;li&gt;Performs &lt;strong&gt;bind + search&lt;/strong&gt; using the &lt;code&gt;ldapbind&lt;/code&gt; service account&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Group Sync Process&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;oc adm groups sync&lt;/code&gt; queries LDAP periodically&lt;/li&gt;
&lt;li&gt;Fetches:

&lt;ul&gt;
&lt;li&gt;Users&lt;/li&gt;
&lt;li&gt;Groups&lt;/li&gt;
&lt;li&gt;Membership mapping&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Updates OpenShift &lt;code&gt;Group&lt;/code&gt; objects&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Automation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CronJob runs the sync automatically every minute (in our setup)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Integration:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a bind user (ldapbind in our case) to query LDAP/FreeIPA. Of course, you need to run this on the LDAP server. I have redacted some part of the output.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;ipa user-add ldapbind &lt;span class="nt"&gt;--first&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ldap &lt;span class="nt"&gt;--last&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;bind&lt;/span&gt; &lt;span class="nt"&gt;--password&lt;/span&gt;
&lt;span class="go"&gt;Password: 
Enter Password again to verify: 
---------------------
Added user "ldapbind"
---------------------
  User login: ldapbind
  First name: ldap
  Last name: bind
  Full name: ldap bind
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Using the password you set, create a "secret" in openshift.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;oc create secret generic ldap-bind-password &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;bindPassword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'redhat'&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; openshift-config
&lt;span class="go"&gt;
secret/ldap-bind-password created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Configure Oauth to use the LDAP/FreeIPA server. Basically, telling OAuth who's our LDAP server, where to search, the user who's allowed to search along with reference to the secret we created above,  &lt;em&gt;Yadda, Yadda, Yadda!!&lt;/em&gt; Here's the manifest and the command:
&lt;/li&gt;
&lt;/ul&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;config.openshift.io/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;OAuth&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;cluster&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;identityProviders&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;freeipa&lt;/span&gt;
      &lt;span class="na"&gt;mappingMethod&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;claim&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LDAP&lt;/span&gt;
      &lt;span class="na"&gt;ldap&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;ldap://192.168.122.246/dc=example,dc=com?uid"&lt;/span&gt;
        &lt;span class="na"&gt;bindDN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uid=ldapbind,cn=users,cn=accounts,dc=example,dc=com"&lt;/span&gt;
        &lt;span class="na"&gt;bindPassword&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;ldap-bind-password&lt;/span&gt;
        &lt;span class="na"&gt;insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;dn&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cn&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;mail&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;preferredUsername&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;


&lt;span class="c1"&gt;#oc apply -f ldap.yaml &lt;/span&gt;
&lt;span class="s"&gt;oauth.config.openshift.io/cluster configured&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Note: If you're too impatient, you can delete the pods in the "openshift-authentication" namespace so the integration is immediately picked up when the pods start back.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I created a user named "ashish" and &lt;em&gt;Voila!&lt;/em&gt; I was able to login.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;oc login &lt;span class="nt"&gt;-u&lt;/span&gt; ashish api.snomaster.lab:6443 
&lt;span class="go"&gt;Console URL: https://api.snomaster.lab:6443/console
Authentication required for https://api.snomaster.lab:6443 (openshift)
Username: ashish
Password: 
Login successful.

You don't have any projects. You can try to create a new project, by running

&lt;/span&gt;&lt;span class="gp"&gt;oc new-project &amp;lt;projectname&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Here's the tricky part. Whatever groups you create in LDAP/FreeIPA are not automatically imported by Openshift. This is where the second part of this article kicks in - _Automatic Group Sync. _&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Automatic Group Sync - Part 1 ( The Manual way)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; In this part we will create a manifest and run it to test if the manual sync works, if that works it's just a matter of creating a cron resource (Isolation is key!).This method is like a test that our manifest and group-sync works. So, here's the manifest
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LDAPSyncConfig&lt;/span&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;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ldap://192.168.122.246&lt;/span&gt;

&lt;span class="na"&gt;bindDN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uid=ldapbind,cn=users,cn=accounts,dc=example,dc=com"&lt;/span&gt;

&lt;span class="na"&gt;bindPassword&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/etc/secrets/bindPassword"&lt;/span&gt;

&lt;span class="na"&gt;insecure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;rfc2307&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;groupsQuery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;baseDN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cn=groups,cn=accounts,dc=example,dc=com"&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sub&lt;/span&gt;
    &lt;span class="na"&gt;derefAliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;
    &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(objectClass=groupofnames)"&lt;/span&gt;

  &lt;span class="na"&gt;groupUIDAttribute&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dn&lt;/span&gt;
  &lt;span class="na"&gt;groupNameAttributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;cn&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;groupMembershipAttributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;member&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;usersQuery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;baseDN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cn=users,cn=accounts,dc=example,dc=com"&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sub&lt;/span&gt;
    &lt;span class="na"&gt;derefAliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;never&lt;/span&gt;

  &lt;span class="na"&gt;userUIDAttribute&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dn&lt;/span&gt;

  &lt;span class="na"&gt;userNameAttributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;uid&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a file that holds the "bind" password . These are the reasons why this is a bad method and should only be used for testing our sync. (These plain text passwords can give security guys an heart-attack!!)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#printf '%s' 'redhat' &amp;gt; /home/user/ldap-manifs/bindPassword&lt;/span&gt;

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Time to test our group-sync. You might as well create some groups on your LDAP/FreeIPA server.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#oc adm groups sync --sync-config=group-sync.yaml --confirm&lt;/span&gt;
group/admins
group/ipausers
group/editors
group/trust admins
group/developers
group/platform-admins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;We can also cross verify if they're actually visible in openshift.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#oc get groups
NAME              USERS
admins            admin
developers        bob
editors           
ipausers          ldapbind, ashish, bob
platform-admins   ashish
trust admins      admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Why I call this automatic is, you can still add the &lt;code&gt;oc adm groups sync&lt;/code&gt; command to cron and this will work just fine. The only bad part is the handling of password. There are ways to work around it, like ansible-ize your sync method and use ansible-vault to store the password or use Hashi Vault to store the password and fetch it using API's.  We aren't going to touch base on those methods today. I will stick to the &lt;em&gt;openshift-way&lt;/em&gt; of doing it. That brings us to part 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Automatic Group Sync - Part 2 ( The Openshift way)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The manifest remains the same, it's just that we will add the following resources to comply with production-grade standards:&lt;/li&gt;
&lt;li&gt;We will create a secret to store the bind password. (Which we already did!).&lt;/li&gt;
&lt;li&gt;Create a configMap that will store our group-sync manifest.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a CronJob Resource, this is the most interesting part.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a group-sync configMap&lt;br&gt;
This is same group-sync manifest we used before, just we add it as a configMap resource this time. Here's the manifest&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&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;ConfigMap&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;ldap-group-sync-config&lt;/span&gt;
&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group-sync.yaml&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;kind: LDAPSyncConfig&lt;/span&gt;
    &lt;span class="s"&gt;apiVersion: v1&lt;/span&gt;

    &lt;span class="s"&gt;url: ldap://192.168.122.246&lt;/span&gt;

    &lt;span class="s"&gt;bindDN: "uid=ldapbind,cn=users,cn=accounts,dc=example,dc=com"&lt;/span&gt;

    &lt;span class="s"&gt;bindPassword:&lt;/span&gt;
      &lt;span class="s"&gt;file: "/etc/secrets/bindPassword"&lt;/span&gt;

    &lt;span class="s"&gt;insecure: true&lt;/span&gt;

    &lt;span class="s"&gt;rfc2307:&lt;/span&gt;
     &lt;span class="s"&gt;groupsQuery:&lt;/span&gt;
       &lt;span class="s"&gt;baseDN: "cn=groups,cn=accounts,dc=example,dc=com"&lt;/span&gt;
       &lt;span class="s"&gt;scope: sub&lt;/span&gt;
       &lt;span class="s"&gt;derefAliases: never&lt;/span&gt;
       &lt;span class="s"&gt;filter: "(objectClass=groupofnames)"&lt;/span&gt;

     &lt;span class="s"&gt;groupUIDAttribute: dn&lt;/span&gt;
     &lt;span class="s"&gt;groupNameAttributes: [ cn ]&lt;/span&gt;

     &lt;span class="s"&gt;groupMembershipAttributes: [ member ]&lt;/span&gt;

     &lt;span class="s"&gt;usersQuery:&lt;/span&gt;
      &lt;span class="s"&gt;baseDN: "cn=users,cn=accounts,dc=example,dc=com"&lt;/span&gt;
      &lt;span class="s"&gt;scope: sub&lt;/span&gt;
      &lt;span class="s"&gt;derefAliases: never&lt;/span&gt;

     &lt;span class="s"&gt;userUIDAttribute: dn&lt;/span&gt;

     &lt;span class="s"&gt;userNameAttributes: [ uid ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The most interesting part of this process. We create a CronJob resource, this resource creates a temporary pod. This pod will mount our configMap and Secret inside the pod, run our job and terminate the pod. You're free to change the schedule.
&lt;/li&gt;
&lt;/ul&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;batch/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;CronJob&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;ldap-group-sync&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;schedule&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;*&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;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
  &lt;span class="na"&gt;jobTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&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;template&lt;/span&gt;&lt;span class="pi"&gt;:&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;serviceAccountName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ldap-group-sync&lt;/span&gt;
       &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OnFailure&lt;/span&gt;
       &lt;span class="na"&gt;containers&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;ldap-group-sync&lt;/span&gt;
           &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;registry.redhat.io/openshift4/ose-cli&lt;/span&gt;
           &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
             &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/bin/bash&lt;/span&gt;
             &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-c&lt;/span&gt; 
             &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
             &lt;span class="s"&gt;- oc adm groups sync --sync-config=/config/group-sync.yaml --confirm &lt;/span&gt;
           &lt;span class="na"&gt;volumeMounts&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;sync-config&lt;/span&gt;
               &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/config&lt;/span&gt;
           &lt;span class="na"&gt;volumeMounts&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;bind-secret&lt;/span&gt;
               &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/secrets&lt;/span&gt;
       &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync-config&lt;/span&gt;
          &lt;span class="na"&gt;configMap&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;ldap-group-sync-config&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;bind-secret&lt;/span&gt;
          &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
           &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ldap-bind-password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;You can monitor the status under oc logs job/. You will see something to the tune of :
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"apiVersion":"v1","bindDN":"uid=ldapbind,cn=users,cn=accounts,dc=example,dc=com","bindPassword":{"file":"/etc/secrets/bindPassword"},"insecure":true,"kind":"LDAPSyncConfig","rfc2307":{"groupMembershipAttributes":["member"],"groupNameAttributes":["cn"],"groupUIDAttribute":"dn","groupsQuery":{"baseDN":"cn=groups,cn=accounts,dc=example,dc=com","derefAliases":"never","filter":"(objectClass=groupofnames)","scope":"sub"},"userNameAttributes":["uid"],"userUIDAttribute":"dn","usersQuery":{"baseDN":"cn=users,cn=accounts,dc=example,dc=com","derefAliases":"never","scope":"sub"}},"url":"ldap://192.168.122.246"}
group/admins
group/ipausers
group/editors
group/trust admins
group/developers
group/platform-admins
group/testers
group/hr

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

&lt;/div&gt;



&lt;p&gt;And just like that we have a fully automated Identity and Access management up and running.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>kubernetes</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>OpenShift Upgrade Deep Dive: 4.19 4.20 (Step-by-Step + What Happens Under the Hood)</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Thu, 07 May 2026 04:16:54 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/openshift-upgrade-deep-dive-419-420-step-by-step-what-happens-under-the-hood-2kmf</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/openshift-upgrade-deep-dive-419-420-step-by-step-what-happens-under-the-hood-2kmf</guid>
      <description>&lt;p&gt;Upgrading Red Hat OpenShift isn’t just about running oc adm upgrade.&lt;br&gt;
Behind the scenes, a carefully orchestrated process ensures your cluster stays available while everything gets updated.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll walk through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real Upgrade path 4.19 to 4.20&lt;/li&gt;
&lt;li&gt;Handling AdminAckRequired blockers&lt;/li&gt;
&lt;li&gt;Setting upgrade channels properly&lt;/li&gt;
&lt;li&gt;Understanding how OpenShift upgrades nodes (masters + workers)&lt;/li&gt;
&lt;li&gt;Why Pod Disruption Budgets (PDBs) can make or break your upgrade&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pre-Upgrade&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check that none of the operators are "Degraded"&lt;/li&gt;
&lt;li&gt;Check PDB's.&lt;/li&gt;
&lt;li&gt;Check Failing Pods.&lt;/li&gt;
&lt;li&gt;Run a must gather (oc adm must-gather). &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;1. Check current status&lt;/strong&gt;&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;#oc adm upgrade&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The output will look 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;Cluster version is 4.19.27

Upgradeable=False

Reason: AdminAckRequired
Message: The admissionregistration.k8s.io/v1beta1 group version is deprecated in 4.19 and will be removed in 4.20.Any clients using the v1beta1 version of these resources must be updated to use the corresponding v1 version instead. See https://access.redhat.com/articles/7130599 for more information.

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

&lt;/div&gt;



&lt;p&gt;This essentially means our upgrade, usually Redhat will compliment the message with a knowledgebase as you can see in the above output.That knowledgebase will actually give you pointers on how to unblock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Acknowledgement and Approval&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The knowledgebase actually asked use to patch a configmap named admin-acks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oc &lt;span class="nt"&gt;-n&lt;/span&gt; openshift-config patch cm admin-acks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--patch&lt;/span&gt; &lt;span class="s1"&gt;'{"data":{"ack-4.19-admissionregistration-v1beta1-api-removals-in-4.20":"true"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Setting the upgrade channel&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the time when I ran this upgrade updates were only available in the fast channel but In production consider using stable or EUS channels.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oc adm upgrade channel fast-4.20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify by running the "oc adm upgrade" command again. Output should show something on these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Channel: fast-4.20

VERSION     IMAGE
  4.20.18     quay.io/openshift-release-dev/ocp-release@sha256:2dab927fd20984e247301b2483083b71f942a1f550f5d8a1db42897edc042e39
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I could explain the relevance of channels in one word:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast channels&lt;/strong&gt; - Latest features but mostly used for Dev/Test environments&lt;br&gt;
&lt;strong&gt;Stable channels&lt;/strong&gt; - Production grade upgrades&lt;br&gt;
&lt;strong&gt;EUS channels&lt;/strong&gt; - LTS upgrades&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Trigger the upgrade&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oc adm upgrade &lt;span class="nt"&gt;--to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4.20.18
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, this is where most articles/guides stop. Because the next steps are taken care by openshift and most Engineers leave their desk for coffee breaks here. If you ask me, this part is equally important  to understand, I will briefly walk though what openshift does "Under the Hood".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.1 Upgrades the control planes first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Suppose you have 3 masters,  It will upgrade one master at a time, so that the cluster quorum is maintained. Each master goes through the below process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node gets cordoned.&lt;/li&gt;
&lt;li&gt;Node gets drained.&lt;/li&gt;
&lt;li&gt;Update is applied.&lt;/li&gt;
&lt;li&gt;Node is rebooted.&lt;/li&gt;
&lt;li&gt;API restored.&lt;/li&gt;
&lt;li&gt;Uncordoned.&lt;/li&gt;
&lt;li&gt;Moves on to the next Master.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4.2. Next it moves on the Worker nodes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The worker nodes also go through a similar process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cordon&lt;/li&gt;
&lt;li&gt;Drain&lt;/li&gt;
&lt;li&gt;Update&lt;/li&gt;
&lt;li&gt;Reboot&lt;/li&gt;
&lt;li&gt;Uncordoned &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another topic that's less spoken about when an upgrade gets stuck is PDB's. If your PDB's are too strict your upgrade gets stuck and if it's too generous, the application is impacted. Be very careful while budgeting PDB's.&lt;/p&gt;

&lt;p&gt;And lastly, Skip the coffee breaks and monitor the upgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oc get clusterversion &lt;span class="nt"&gt;-w&lt;/span&gt;
NAME      VERSION   AVAILABLE   PROGRESSING   SINCE   STATUS
version   4.19.27   True        True          11s     Working towards 4.20.18: 10 of 959 &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;1% &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
version   4.19.27   True        True          11s     Working towards 4.20.18
version   4.19.27   True        True          11s     Working towards 4.20.18: 1 of 959 &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0% &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
version   4.19.27   True        True          11s     Working towards 4.20.18: 4 of 959 &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0% &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
version   4.19.27   True        True          11s     Working towards 4.20.18: 6 of 959 &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0% &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
version   4.19.27   True        True          11s     Working towards 4.20.18: 8 of 959 &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0% &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
version   4.19.27   True        True          11s     Working towards 4.20.18: 10 of 959 &lt;span class="k"&gt;done&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;1% &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Verify completion&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oc get clusterversion 
NAME      VERSION   AVAILABLE   PROGRESSING   SINCE   STATUS
version   4.20.18   True        False         10m     Cluster version is 4.20.18
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My verdict on the upgrade process is, the upgrade is straight forward only when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you handle any deprecations properly&lt;/li&gt;
&lt;li&gt;Set correct channels.&lt;/li&gt;
&lt;li&gt;Don't skip prechecks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this helped you, feel free to share or drop your upgrade experiences too!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>infrastructure</category>
      <category>kubernetes</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Autoscaling in Openshift with Cluster Autoscaler and Machine Autoscaler</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Mon, 27 Apr 2026 04:56:21 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/autoscaling-in-openshift-with-cluster-autoscaler-and-machine-autoscaler-2i9h</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/autoscaling-in-openshift-with-cluster-autoscaler-and-machine-autoscaler-2i9h</guid>
      <description>&lt;p&gt;In my previous post, I covered how to scale compute (worker) nodes in OpenShift using a semi-automated approach. While OpenShift handled most of the heavy lifting—such as powering on the node via BMC, installing RHCOS, and joining the cluster—the scaling action itself still required manual intervention.&lt;/p&gt;

&lt;p&gt;This document focuses on removing that manual step altogether. Specifically, it explores how Cluster Autoscaler and Machine Autoscaler work together to enable automatic, workload-driven scaling of compute nodes. While technologies like Cluster API and Machine API provide the underlying framework for managing machine lifecycles, the real decision-making around when to scale happens at the autoscaler layer.&lt;/p&gt;

&lt;p&gt;Before I dive into it, I would like to explain in a line about the building blocks(So you don't think of me of a complete idiot!).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cluster Autoscaler&lt;/strong&gt; - responsible for observing the state of workloads in the cluster. It continuously monitors pending and unschedulable pods and determines whether adding (or removing) nodes would help satisfy resource requirements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machine Autoscaler&lt;/strong&gt; - acts as the bridge between high-level scaling decisions and infrastructure changes. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machinsets&lt;/strong&gt;- Define how a worker node should be created&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cluster API&lt;/strong&gt;: Cluster API is a Kubernetes project that provides a declarative way to create, manage, and scale Kubernetes clusters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Machine API&lt;/strong&gt;: Red Hat’s opinionated implementation of Cluster API concepts. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MachineSets&lt;/strong&gt;: Equivalent of a ReplicaSet — but for nodes instead of pods.&lt;/p&gt;

&lt;p&gt;The way these different component work together is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The cluster Autoscaler watches for unschedulable pods.&lt;/li&gt;
&lt;li&gt;Informs the Machine Autoscaler who updates the machineSets limits.Simply speaking, runs the "oc scale machineset/ replicas=2"
&lt;/li&gt;
&lt;li&gt;Our hardworking employee, Machine-API prepares the new node (of course, with help from BMH)&lt;/li&gt;
&lt;li&gt;Pods get scheduled to the new worker node.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We will see how to perform the teeny-tiny set of 4 steps in Openshift:&lt;/p&gt;

&lt;p&gt;Note: You can change to the "openshift-machine-api" namespace as most of the steps will be in that namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oc project openshift-machine-api

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

&lt;/div&gt;



&lt;p&gt;Let's check our current nodes, machinsets and BMH(That's BareMetalHosts and not Jaspreet Bhumrah!)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get nodes
NAME      STATUS   ROLES                  AGE    VERSION
master    Ready    control-plane,master   8d     v1.27.10+28ed2d7
master2   Ready    control-plane,master   8d     v1.27.10+28ed2d7
master3   Ready    control-plane,master   8d     v1.27.10+28ed2d7
worker1   Ready    worker                 3d7h   v1.27.10+28ed2d7

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get machinesets
NAME                       DESIRED   CURRENT   READY   AVAILABLE   AGE
mycluster-7ln8n-worker-0   1         1         1       1           8d

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get bmh
NAME      STATE                    CONSUMER                         ONLINE   ERROR                AGE
master    externally provisioned   mycluster-7ln8n-master-0         true        8d
master2   externally provisioned   mycluster-7ln8n-master-1         true        8d
master3   externally provisioned   mycluster-7ln8n-master-2         true        8d
worker1   provisioned              mycluster-7ln8n-worker-0-qv4cn   true        3d10h

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

&lt;/div&gt;



&lt;p&gt;Let's create our cluster Autoscaler and Machineautoscaler manifests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc apply -f machineautoscaler.yaml 
machineautoscaler.autoscaling.openshift.io/worker-autoscaler created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc apply -f clusautoscaler.yaml 
clusterautoscaler.autoscaling.openshift.io/default created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: You can construct the machine autoscaler and cluster autoscaler manifests from Redhat's official &lt;a href="https://docs.redhat.com/en/documentation/openshift_container_platform/4.9/html/machine_management/applying-autoscaling" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's add some load to our cluster. This is just a simple manifest that requests 1G memory and spawns ~20+ replicas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc apply -f dep.yaml 
deployment.apps/load-test created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This manifest has actually created someone load on the current worker node and needs probably another one to accommodate all the pods. Hence a lot of them are now in "Pending" state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;load-test-65777f99f7-458kp                            0/1     Pending   0          2m22s   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&amp;lt;none&amp;gt;    &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;span class="gp"&gt;load-test-65777f99f7-4p72l                            1/1     Running   0          2m22s   10.128.2.43       worker1   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&amp;lt;none&amp;gt;
&lt;span class="gp"&gt;load-test-65777f99f7-4v6hq                            0/1     Pending   0          2m22s   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&amp;lt;none&amp;gt;    &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;span class="gp"&gt;load-test-65777f99f7-84n79                            1/1     Running   0          2m22s   10.128.2.39       worker1   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&amp;lt;none&amp;gt;
&lt;span class="gp"&gt;load-test-65777f99f7-8955k                            0/1     Pending   0          2m22s   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&amp;lt;none&amp;gt;    &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;span class="gp"&gt;load-test-65777f99f7-9h4j6                            0/1     Pending   0          2m22s   &amp;lt;none&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&amp;lt;none&amp;gt;    &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our Autoscalers have actually updated our machineset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get machineset
NAME                       DESIRED   CURRENT   READY   AVAILABLE   AGE
mycluster-7ln8n-worker-0   2         2         1       1           8d
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which has started provisioning a new node for us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get bmh
NAME      STATE                    CONSUMER                         ONLINE   ERROR                AGE
master    externally provisioned   mycluster-7ln8n-master-0         true        8d
master2   externally provisioned   mycluster-7ln8n-master-1         true        8d
master3   externally provisioned   mycluster-7ln8n-master-2         true        8d
worker2   provisioning             mycluster-7ln8n-worker-0-9vszs   true                          12m
worker1   provisioned              mycluster-7ln8n-worker-0-qv4cn   true                          3d10h
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In no time(Ok, around 20 minutes) you should see your new worker node ready to take workload. The pending pods will move to this new worker gradually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get nodes
NAME      STATUS   ROLES                  AGE     VERSION
master    Ready    control-plane,master   8d      v1.27.10+28ed2d7
master2   Ready    control-plane,master   8d      v1.27.10+28ed2d7
master3   Ready    control-plane,master   8d      v1.27.10+28ed2d7
worker1   Ready    worker                 3d8h    v1.27.10+28ed2d7
worker2   Ready    worker                 3m14s   v1.27.10+28ed2d7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "oc get pods -o wide" will actually tell you that the workloads are actually moving to this new worker node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load-test-65777f99f7-458kp                            1/1     Running   0          33m     10.131.0.5        worker2   &amp;lt;none&amp;gt;           
load-test-65777f99f7-4p72l                            1/1     Running   0          33m     10.128.2.43       worker1   &amp;lt;none&amp;gt; 
load-test-65777f99f7-84n79                            1/1     Running   0          33m     10.128.2.39       worker1   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
load-test-65777f99f7-9h4j6                            1/1     Running   0          33m     10.131.0.6        worker2   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
load-test-65777f99f7-ctvc7                            1/1     Running   0          33m     10.128.2.41       worker1   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

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

&lt;/div&gt;



&lt;p&gt;The autoscalers not only works well for scaling-out, it also works pretty well for scaling-in. &lt;/p&gt;

&lt;p&gt;To test this out I deleted the deployment I had created earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc delete deployment/load-test
deployment.apps "load-test" deleted
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also check the event logs using 'oc get events'. It actually tells us the entire flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;24m                  Normal    Killing                  Pod/load-test-65777f99f7-ctvc7                     Stopping container stress
24m                  Normal    Killing                  Pod/load-test-65777f99f7-hgf2n                     Stopping container stress

13m                  Normal    ScaleDownEmpty           ConfigMap/cluster-autoscaler-status                Scale-down: removing empty node "worker2"
13m                  Normal    ScaleDownEmpty           ConfigMap/cluster-autoscaler-status                Scale-down: empty node worker2 removed
13m                  Normal    DrainProceeds            Machine/mycluster-7ln8n-worker-0-9vszs             Node drain proceeds
13m (x10 over 66m)   Normal    SuccessfulUpdate         MachineAutoscaler/worker-autoscaler                Updated MachineAutoscaler target: openshift-machine-api/mycluster-7ln8n-worker-0
13m                  Normal    Deleted                  Machine/mycluster-7ln8n-worker-0-9vszs             Node "worker2" drained
13m                  Normal    DrainSucceeded           Machine/mycluster-7ln8n-worker-0-9vszs             Node drain succeeded
13m                  Normal    DeprovisioningStarted    BareMetalHost/worker2                              Image deprovisioning started

8m34s                Normal    DeprovisioningComplete   BareMetalHost/worker2                              Image deprovisioning completed
8m33s                Normal    PowerOff                 BareMetalHost/worker2                              Host soft powered off
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a nutshell it has:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On deleting the deployment, the containers were killed.&lt;/li&gt;
&lt;li&gt;It scaled down the workload, we can also check the machineset output.&lt;/li&gt;
&lt;li&gt;Drained the node.&lt;/li&gt;
&lt;li&gt;Deprovisioned and Power-off the node.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;user@server1:~/test/manifests$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;oc get machineset &lt;span class="nt"&gt;-n&lt;/span&gt; openshift-machine-api
&lt;span class="go"&gt;NAME                       DESIRED   CURRENT   READY   AVAILABLE   AGE
mycluster-7ln8n-worker-0   1         1         1       1           8d
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;virsh list --all
 Id   Name      State
--------------------------
 1    master    running
 2    master2   running
 3    master3   running
 4    worker1   running
 -    worker2   shut off
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>automation</category>
      <category>devops</category>
      <category>kubernetes</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Scaling worker nodes in openshift with machine-api</title>
      <dc:creator>Ashish Nair</dc:creator>
      <pubDate>Mon, 13 Apr 2026 05:59:39 +0000</pubDate>
      <link>https://dev.to/ashish_nair_d9b10ba4f8126/scaling-worker-nodes-in-openshift-with-machine-api-38f0</link>
      <guid>https://dev.to/ashish_nair_d9b10ba4f8126/scaling-worker-nodes-in-openshift-with-machine-api-38f0</guid>
      <description>&lt;p&gt;In my last post, I wrote about how we can run an Openshift IPI install on &lt;a href="https://dev.to/ashish_nair_d9b10ba4f8126/deploying-openshift-ipi-on-kvm-baremetal-simulation-with-redfish-sushy-2o60"&gt;KVM&lt;/a&gt;. In this document(Which is relatively shorter), I'll talk about my experience in scaling a worker node (a semi-automated method). &lt;/p&gt;

&lt;p&gt;We will be breaking this down into 2 steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Preparation&lt;/li&gt;
&lt;li&gt;The Scaling&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Preparation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The prerequisite to start this is obviously a server( A blank VM in KVM, in our case). &lt;/li&gt;
&lt;li&gt;A DNS entry in our Antagonist's Database.&lt;/li&gt;
&lt;li&gt;The sushy webserver listening on port 8000, this is our IDrac/iLo emulation. More on that &lt;a href="https://dev.to/ashish_nair_d9b10ba4f8126/deploying-openshift-ipi-on-kvm-baremetal-simulation-with-redfish-sushy-2o60"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;And a bit of patience (did i pull-off a Robin Sharma here?)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Scaling:
&lt;/h3&gt;

&lt;p&gt;Before we start the actual scaling process let me walk through the actual process :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We create a VM in KVM (This is as good as having a spare Baremetal host)&lt;/li&gt;
&lt;li&gt;We create a Manifest (actually two) and apply it. This manifest will remind you of the install-config.yaml we used &lt;a href="https://dev.to/ashish_nair_d9b10ba4f8126/deploying-openshift-ipi-on-kvm-baremetal-simulation-with-redfish-sushy-2o60"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Once the manifest is applied, the machine is booted as a result of our BMC magic (via sushy and Redfish) and Installation is kicked-off.&lt;/li&gt;
&lt;li&gt;Once the install is completed the Machine(node) joins the cluster. &lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Gather some data:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;All (okay, most) of the work will happen in the &lt;strong&gt;openshift-machine-api&lt;/strong&gt; namespace.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Let's check the current nodes that we have&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get nodes
NAME          STATUS   ROLES                  AGE     VERSION
master        Ready    control-plane,master   3d20h   v1.27.10+28ed2d7
master2       Ready    control-plane,master   3d20h   v1.27.10+28ed2d7
master3       Ready    control-plane,master   3d20h   v1.27.10+28ed2d7
worker2.lab   Ready    worker                 2d22h   v1.27.10+28ed2d7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;It's important to check the Baremetals we currently have.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get bmh -n openshift-machine-api
NAME      STATE                    CONSUMER                         ONLINE   ERROR                AGE
master    externally provisioned   mycluster-7ln8n-master-0         true        3d21h
master2   externally provisioned   mycluster-7ln8n-master-1         true        3d21h
master3   externally provisioned   mycluster-7ln8n-master-2         true        3d21h
worker2   provisioned              mycluster-7ln8n-worker-0-hpm5j   true        2d22h

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A note on machinsets.&lt;/strong&gt; Machine sets display the "group" of workernodes we have. They are used to scale compute(Workers). Make node of the name of the Machineset this is the one we will be scaling out.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get machinesets -n openshift-machine-api
NAME                       DESIRED   CURRENT   READY   AVAILABLE   AGE
mycluster-7ln8n-worker-0   1         1         1       1           3d21h
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The manifests:
&lt;/h3&gt;

&lt;p&gt;We need to 2 manifests here and it's largely inspired by the install-config.yaml we had earlier :&lt;/p&gt;

&lt;p&gt;Snippet of install-config.yaml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt; &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker3&lt;/span&gt;
      &lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker&lt;/span&gt;
      &lt;span class="s"&gt;bmc&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redfish-virtualmedia+http://192.168.122.1:8000/redfish/v1/Systems/aa12a91d-56f9-41f4-b5ea-e5001dae179c&lt;/span&gt;
        &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
      &lt;span class="na"&gt;bootMACAddress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;52:54:00:fd:5e:1d&lt;/span&gt;
      &lt;span class="na"&gt;rootDeviceHints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="na"&gt;deviceName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/vda&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And our Manifests will do exactly what the above code we used while installation, but we split it into two:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creates a BMH &lt;/li&gt;
&lt;li&gt;Creates a secret to hold the console credentials.
&lt;/li&gt;
&lt;/ol&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;metal3.io/v1alpha1&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;BareMetalHost&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;worker3&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;openshift-machine-api&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;online&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;bootMACAddress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;52:54:00:c4:0e:6c&lt;/span&gt;
  &lt;span class="na"&gt;bmc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redfish-virtualmedia+http://192.168.122.1:8000/redfish/v1/Systems/d9fea2de-6fd9-4a44-99fc-95f32b610407"&lt;/span&gt;
    &lt;span class="na"&gt;credentialsName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;worker3-bmc-secret&lt;/span&gt;
  &lt;span class="na"&gt;rootDeviceHints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;deviceName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dev/vda&lt;/span&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;Secret&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;worker3-bmc-secret&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;openshift-machine-api&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Opaque&lt;/span&gt;
&lt;span class="na"&gt;stringData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Apply the manifests:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc apply -f .
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The wait (Part 1) :
&lt;/h3&gt;

&lt;p&gt;After you apply the manifests, the node will transition into 4 states:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Registering&lt;/li&gt;
&lt;li&gt;Inspecting&lt;/li&gt;
&lt;li&gt;Available&lt;/li&gt;
&lt;li&gt;Provisioned&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For brevity we will jump to the "available" state, as this is the most interesting part. At this point if you login to the worker using 'ssh core@IP' you will see that our new worker is booted up using a CoreOS ISO by the Installer. This is a way of openshift telling us "The system is available, what do you want me to do?" (And this will also take us to the final part)&lt;/p&gt;

&lt;p&gt;And we say we wanna scale, this is the machineset name I had asked you to make note of(If you haven't fallen asleep by now!). So the "oc scale" command uses the machineset name to scale the compute by 2 replicas in our case.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc scale machineset/mycluster-7ln8n-worker-0 -n openshift-machine-api --replicas=2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when you login to the worker machine you will see that the installation might have started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;all [-] coreos-installer: Read disk 868.2 MiB/2.4 GiB (35%) _run_install /usr/lib/python3.9/site-packages/ironic_coreos_install.py:271
Apr 10 14:41:47 worker3 podman[1179]: 2026-04-10 14:41:47.517 1 DEBUG ironic_coreos_install [-] coreos-installer: Read disk 868.2 MiB/2.4 GiB (35%) _run_install /usr/lib/python3.9/site-packages/ironic_coreos_install.py:271
Apr 10 14:41:48 worker3 ironic-agent[1191]: 2026-04-10 14:41:48.523 1 DEBUG ironic_coreos_install [-] coreos-installer: Read disk 883.6 MiB/2.4 GiB (35%) _run_install /usr/lib/python3.9/site-packages/ironic_coreos_install.py:271
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The wait (Final part, I promise!):
&lt;/h3&gt;

&lt;p&gt;In about 15-20 minutes you will see that the machine state will have transitioned from "Available" to "Provisioned"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get bmh -n openshift-machine-api
NAME      STATE                    CONSUMER                         ONLINE   ERROR   AGE
master    externally provisioned   mycluster-7ln8n-master-0         true             4d6h
master2   externally provisioned   mycluster-7ln8n-master-1         true             4d6h
master3   externally provisioned   mycluster-7ln8n-master-2         true             4d6h
worker2   provisioned              mycluster-7ln8n-worker-0-hpm5j   true             3d7h
worker3   provisioned              mycluster-7ln8n-worker-0-k67h7   true             7h41m
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "get nodes" should also show it's available for use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;oc get nodes
NAME          STATUS   ROLES                  AGE     VERSION
master        Ready    control-plane,master   5d8h    v1.27.10+28ed2d7
master2       Ready    control-plane,master   5d8h    v1.27.10+28ed2d7
master3       Ready    control-plane,master   5d8h    v1.27.10+28ed2d7
worker3       Ready    worker                 9h      v1.27.10+28ed2d7
worker2.lab   Ready    worker                 4d10h   v1.27.10+28ed2d7
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all &lt;em&gt;folks&lt;/em&gt;!!&lt;/p&gt;

&lt;p&gt;While the procedure looks lengthy while explaining and documenting, it essentially is just creating the manifests and running oc scale command. If you have scaled a compute manually you will appreciate how much work this method cuts down! &lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>kubernetes</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
