<?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>Forem: Falolu Olaitan</title>
    <description>The latest articles on Forem by Falolu Olaitan (@devops_oracle).</description>
    <link>https://forem.com/devops_oracle</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F516101%2F376e10ae-6347-4fbb-aba1-f15dc38eb730.jpg</url>
      <title>Forem: Falolu Olaitan</title>
      <link>https://forem.com/devops_oracle</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/devops_oracle"/>
    <language>en</language>
    <item>
      <title>Setting Up a Production-Ready Kubernetes Cluster on RHEL 9.7</title>
      <dc:creator>Falolu Olaitan</dc:creator>
      <pubDate>Mon, 06 Apr 2026 21:26:45 +0000</pubDate>
      <link>https://forem.com/devops_oracle/setting-up-a-production-ready-kubernetes-cluster-on-rhel-97-83g</link>
      <guid>https://forem.com/devops_oracle/setting-up-a-production-ready-kubernetes-cluster-on-rhel-97-83g</guid>
      <description>&lt;p&gt;Running Kubernetes on Red Hat Enterprise Linux (RHEL) is a common requirement in enterprise environments, especially in regulated industries where stability, security, and support matter.&lt;/p&gt;

&lt;p&gt;This guide walks through how to set up a Kubernetes cluster on RHEL 9.7, using a multi-node architecture suitable for production workloads.&lt;/p&gt;

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

&lt;p&gt;A typical production setup includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Control Plane Nodes (3)&lt;/li&gt;
&lt;li&gt;API server&lt;/li&gt;
&lt;li&gt;scheduler&lt;/li&gt;
&lt;li&gt;controller manager&lt;/li&gt;
&lt;li&gt;&lt;p&gt;etcd (stacked or external)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Worker Nodes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;run application workloads&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Load Balancer (recommended)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;provides a single endpoint for the API server&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;high availability&lt;/li&gt;
&lt;li&gt;fault tolerance&lt;/li&gt;
&lt;li&gt;scalability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;System Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each node should have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RHEL 9.7 installed&lt;/li&gt;
&lt;li&gt;at least 2 CPUs (4+ recommended)&lt;/li&gt;
&lt;li&gt;4GB RAM minimum (8GB+ recommended)&lt;/li&gt;
&lt;li&gt;stable network connectivity&lt;/li&gt;
&lt;li&gt;unique hostname&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Disable Swap&lt;/strong&gt;&lt;br&gt;
Kubernetes requires swap to be disabled.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Remove it permanently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'/swap/d'&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;free &lt;span class="nt"&gt;-h&lt;/span&gt;
swapon &lt;span class="nt"&gt;--show&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Configure Kernel Modules and Networking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enable required modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;modprobe overlay
&lt;span class="nb"&gt;sudo &lt;/span&gt;modprobe br_netfilter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set sysctl parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /etc/sysctl.d/99-kubernetes.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;--system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Configure Hostname and Hosts File&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set hostnames on each node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hostnamectl set-hostname &amp;lt;node-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update &lt;code&gt;/etc/hosts&lt;/code&gt; on all nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;IP&amp;gt; controlplane1
&amp;lt;IP&amp;gt; controlplane2
&amp;lt;IP&amp;gt; controlplane3
&amp;lt;IP&amp;gt; worker1
&amp;lt;IP&amp;gt; worker2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps with internal name resolution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Install Container Runtime (containerd)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes no longer supports Docker directly as a runtime. Use containerd.&lt;/p&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Generate default config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/containerd
containerd config default | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/containerd/config.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable systemd cgroup driver:&lt;/p&gt;

&lt;p&gt;Edit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;vi /etc/containerd/config.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;SystemdCgroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: Install Kubernetes Components&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add Kubernetes repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;cat&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&amp;lt;EOF&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="err"&gt;sudo&lt;/span&gt; &lt;span class="err"&gt;tee&lt;/span&gt; &lt;span class="err"&gt;/etc/yum.repos.d/kubernetes.repo&lt;/span&gt;
&lt;span class="nn"&gt;[kubernetes]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Kubernetes&lt;/span&gt;
&lt;span class="py"&gt;baseurl&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://pkgs.k8s.io/core:/stable:/v1.29/rpm/&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;gpgcheck&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;repo_gpgcheck&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;gpgkey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://pkgs.k8s.io/core:/stable:/v1.29/rpm/repodata/repomd.xml.key&lt;/span&gt;
&lt;span class="err"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; kubelet kubeadm kubectl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable kubelet:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 6: Initialize the Control Plane&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run on the first control plane node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;kubeadm init &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--control-plane-endpoint&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;LOAD_BALANCER_DNS&amp;gt;:6443"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--upload-certs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no load balancer exists, you can temporarily use the first node’s IP.&lt;/p&gt;

&lt;p&gt;After initialization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/kubernetes/admin.conf &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube/config
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$HOME&lt;/span&gt;/.kube/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 7: Join Additional Control Plane Nodes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use the join command generated from kubeadm init, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;token&lt;/li&gt;
&lt;li&gt;discovery token CA cert hash&lt;/li&gt;
&lt;li&gt;certificate key
Example:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubeadm &lt;span class="nb"&gt;join&lt;/span&gt; &amp;lt;endpoint&amp;gt;:6443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--token&lt;/span&gt; &amp;lt;token&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--discovery-token-ca-cert-hash&lt;/span&gt; sha256:&amp;lt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--control-plane&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-key&lt;/span&gt; &amp;lt;key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 8: Join Worker Nodes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run on each worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubeadm &lt;span class="nb"&gt;join&lt;/span&gt; &amp;lt;endpoint&amp;gt;:6443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--token&lt;/span&gt; &amp;lt;token&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--discovery-token-ca-cert-hash&lt;/span&gt; sha256:&amp;lt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 9: Install CNI (Networking)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without a CNI plugin, pods cannot communicate.&lt;br&gt;
Popular options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calico&lt;/li&gt;
&lt;li&gt;Cilium&lt;/li&gt;
&lt;li&gt;Flannel
Example using Calico:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://docs.projectcalico.org/manifests/calico.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 10: Remove Control Plane Taints (Optional for testing)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By default, control planes don’t run workloads.&lt;/p&gt;

&lt;p&gt;For lab/testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl taint nodes &lt;span class="nt"&gt;--all&lt;/span&gt; node-role.kubernetes.io/control-plane-
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, leave taints in place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 11: Verify Cluster Health&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;kubectl get nodes
kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt;
kubectl cluster-info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All nodes should be Ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 12: Add Load Balancer for Services (Optional)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For on-prem environments, you can use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MetalLB&lt;/li&gt;
&lt;li&gt;HAProxy + Keepalived&lt;/li&gt;
&lt;li&gt;external hardware load balancer
MetalLB allows you to assign IPs to services of type LoadBalancer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common Pitfalls&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Swap not disabled&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cluster will fail to initialize.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wrong cgroup driver&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mismatch between containerd and kubelet causes instability.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Firewall issues&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ensure required ports are open between nodes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Missing CNI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pods will remain in Pending.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;kubeconfig not set&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You’ll see errors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;connection refused to localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Production Considerations&lt;/strong&gt;&lt;br&gt;
For a real-world deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use a load balancer for API server&lt;/li&gt;
&lt;li&gt;separate etcd if scale increases&lt;/li&gt;
&lt;li&gt;implement monitoring (Prometheus, Grafana)&lt;/li&gt;
&lt;li&gt;enable logging aggregation&lt;/li&gt;
&lt;li&gt;enforce RBAC policies&lt;/li&gt;
&lt;li&gt;use TLS everywhere&lt;/li&gt;
&lt;li&gt;implement backup strategy for etcd&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
Setting up Kubernetes on RHEL 9.7 gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;enterprise-grade stability&lt;/li&gt;
&lt;li&gt;full control over infrastructure&lt;/li&gt;
&lt;li&gt;flexibility for hybrid or on-prem environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is not just getting the cluster running, but designing it for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;resilience&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;security&lt;/li&gt;
&lt;li&gt;scalability
Once the foundation is solid, you can confidently run critical workloads on top of it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rhel</category>
      <category>kubernetes</category>
      <category>linux</category>
    </item>
    <item>
      <title>Syncing Azure SQL Databases Across Subscriptions Using OpenShift CronJob (Without ADF)</title>
      <dc:creator>Falolu Olaitan</dc:creator>
      <pubDate>Thu, 12 Feb 2026 20:31:59 +0000</pubDate>
      <link>https://forem.com/devops_oracle/syncing-azure-sql-databases-across-subscriptions-using-openshift-cronjob-without-adf-2oah</link>
      <guid>https://forem.com/devops_oracle/syncing-azure-sql-databases-across-subscriptions-using-openshift-cronjob-without-adf-2oah</guid>
      <description>&lt;p&gt;Sometimes you need to move data between environments without introducing a heavy ETL tool like Azure Data Factory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In my case, I needed to sync a table from:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Production Azure SQL Database&lt;/li&gt;
&lt;li&gt;To a Development Azure SQL Database&lt;/li&gt;
&lt;li&gt;Across different subscriptions&lt;/li&gt;
&lt;li&gt;Over Private Endpoints&lt;/li&gt;
&lt;li&gt;Running inside Azure Red Hat OpenShift (ARO)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Incrementally sync new rows every 2 minutes.&lt;br&gt;
No ADF. No linked servers. No manual exports.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here’s how I built a lightweight, production-ready sync using:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bcp&lt;/li&gt;
&lt;li&gt;sqlcmd&lt;/li&gt;
&lt;li&gt;Kubernetes CronJob&lt;/li&gt;
&lt;li&gt;A watermark table&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Inside the OpenShift cluster:&lt;/li&gt;
&lt;li&gt;A CronJob runs every 2 minutes&lt;/li&gt;
&lt;li&gt;Reads the last synced ID from Dev&lt;/li&gt;
&lt;li&gt;Exports new rows from Prod&lt;/li&gt;
&lt;li&gt;Imports into Dev&lt;/li&gt;
&lt;li&gt;Updates watermark&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Networking:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both SQL servers use Private Endpoints&lt;/li&gt;
&lt;li&gt;privatelink.database.windows.net DNS zone linked properly&lt;/li&gt;
&lt;li&gt;No public database access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create a Watermark Table in Dev&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This keeps track of what has already been synced.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE dbo.DataSyncWatermark (
  TableName sysname PRIMARY KEY,
  LastSyncedId bigint NOT NULL DEFAULT(0),
  UpdatedAt datetime2 NOT NULL DEFAULT SYSUTCDATETIME()
);

INSERT INTO dbo.DataSyncWatermark(TableName, LastSyncedId)
VALUES ('dbo.ActivityLogs', 0);

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Example Table Schema (Generic)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To keep this reusable, here’s a sample table structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE dbo.ActivityLogs (
  Id bigint IDENTITY(1,1) NOT NULL PRIMARY KEY,
  UserId nvarchar(100),
  ActionType nvarchar(200),
  Payload nvarchar(max),
  Response nvarchar(max),
  StatusCode int,
  CreatedAt datetime2,
  ExtraMetadata nvarchar(2048)
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both Prod and Dev must have identical structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Create Kubernetes Secret&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Never hardcode credentials in YAML.&lt;/p&gt;

&lt;p&gt;Create a secret like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;oc create secret generic sql-sync-secret \
  --from-literal=PROD_SERVER=prod-sql.database.windows.net \
  --from-literal=PROD_DB=prod_database \
  --from-literal=PROD_USER=sync_user \
  --from-literal=PROD_PASS='StrongPassword!' \
  --from-literal=DEV_SERVER=dev-sql.database.windows.net \
  --from-literal=DEV_DB=dev_database \
  --from-literal=DEV_USER=sync_user \
  --from-literal=DEV_PASS='StrongPassword!'

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

&lt;/div&gt;



&lt;p&gt;Now the CronJob can safely consume these via environment variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Required SQL Permissions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On Prod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GRANT SELECT ON dbo.ActivityLogs TO sync_user;

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

&lt;/div&gt;



&lt;p&gt;On Dev:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GRANT SELECT, INSERT ON dbo.ActivityLogs TO sync_user;
GRANT SELECT, INSERT, UPDATE ON dbo.DataSyncWatermark TO sync_user;
GRANT ALTER ON dbo.ActivityLogs TO sync_user;

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

&lt;/div&gt;



&lt;p&gt;ALTER is required because we preserve identity values during import.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: The CronJob YAML&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs every 2 minutes&lt;/li&gt;
&lt;li&gt;Handles incremental sync&lt;/li&gt;
&lt;li&gt;Avoids collation errors&lt;/li&gt;
&lt;li&gt;Preserves identity&lt;/li&gt;
&lt;li&gt;Avoids duplicate inserts&lt;/li&gt;
&lt;li&gt;Auto-detects sqlcmd and bcp
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: batch/v1
kind: CronJob
metadata:
  name: sql-table-sync
spec:
  schedule: "*/2 * * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: runner
              image: mcr.microsoft.com/mssql-tools
              envFrom:
                - secretRef:
                    name: sql-sync-secret
              command: ["/bin/bash","-lc"]
              args:
                - |
                  set -e

                  SQLCMD="/opt/mssql-tools/bin/sqlcmd"
                  BCP="/opt/mssql-tools/bin/bcp"

                  TABLE="dbo.ActivityLogs"
                  WM="dbo.DataSyncWatermark"

                  LAST_ID=$($SQLCMD -S "$DEV_SERVER" -d "$DEV_DB" -U "$DEV_USER" -P "$DEV_PASS" \
                    -h -1 -W -Q "SET NOCOUNT ON; SELECT LastSyncedId FROM $WM WHERE TableName = '$TABLE';")

                  DEV_MAX_ID=$($SQLCMD -S "$DEV_SERVER" -d "$DEV_DB" -U "$DEV_USER" -P "$DEV_PASS" \
                    -h -1 -W -Q "SET NOCOUNT ON; SELECT ISNULL(MAX(Id),0) FROM $TABLE;")

                  if [ "$LAST_ID" -lt "$DEV_MAX_ID" ]; then
                    LAST_ID="$DEV_MAX_ID"
                  fi

                  NEW_COUNT=$($SQLCMD -S "$PROD_SERVER" -d "$PROD_DB" -U "$PROD_USER" -P "$PROD_PASS" \
                    -h -1 -W -Q "SET NOCOUNT ON; SELECT COUNT(1) FROM $TABLE WHERE Id &amp;gt; ${LAST_ID};")

                  if [ "$NEW_COUNT" = "0" ]; then
                    exit 0
                  fi

                  $BCP "
                  SELECT
                    Id,
                    UserId COLLATE DATABASE_DEFAULT,
                    ActionType COLLATE DATABASE_DEFAULT,
                    Payload COLLATE DATABASE_DEFAULT,
                    Response COLLATE DATABASE_DEFAULT,
                    StatusCode,
                    CreatedAt,
                    ExtraMetadata COLLATE DATABASE_DEFAULT
                  FROM $TABLE
                  WHERE Id &amp;gt; ${LAST_ID}
                  ORDER BY Id
                  " queryout /tmp/data.dat \
                    -S "$PROD_SERVER" -d "$PROD_DB" -U "$PROD_USER" -P "$PROD_PASS" -n

                  $BCP $TABLE in /tmp/data.dat \
                    -S "$DEV_SERVER" -d "$DEV_DB" -U "$DEV_USER" -P "$DEV_PASS" \
                    -n -E -b 5000

                  $SQLCMD -S "$DEV_SERVER" -d "$DEV_DB" -U "$DEV_USER" -P "$DEV_PASS" -Q "
                    UPDATE $WM
                    SET LastSyncedId = (SELECT MAX(Id) FROM $TABLE),
                        UpdatedAt = SYSUTCDATETIME()
                    WHERE TableName = '$TABLE';
                  "

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common Issues I Faced&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Private DNS returning NXDOMAIN
Fix: Ensure both SQL servers have private endpoints and DNS zone groups attached.&lt;/li&gt;
&lt;li&gt;Collation errors
Fix: Add COLLATE DATABASE_DEFAULT to string columns in export query.&lt;/li&gt;
&lt;li&gt;Duplicate primary key errors
Fix: Align watermark with MAX(Id) in Dev.&lt;/li&gt;
&lt;li&gt;Table does not exist” error
Fix: Grant proper INSERT + ALTER permissions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why This Approach Works&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No external ETL tool required&lt;/li&gt;
&lt;li&gt;Lightweight&lt;/li&gt;
&lt;li&gt;Kubernetes-native&lt;/li&gt;
&lt;li&gt;Works across subscriptions&lt;/li&gt;
&lt;li&gt;Fully automated&lt;/li&gt;
&lt;li&gt;Secure over Private Link&lt;/li&gt;
&lt;li&gt;Easy to generalize for other tables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When You Should Use Something Else&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Near real-time replication&lt;/li&gt;
&lt;li&gt;Massive table sync&lt;/li&gt;
&lt;li&gt;Transformations&lt;/li&gt;
&lt;li&gt;CDC&lt;/li&gt;
&lt;li&gt;Multi-region replication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure SQL replication&lt;/li&gt;
&lt;li&gt;Azure Data Factory&lt;/li&gt;
&lt;li&gt;Change Data Capture&lt;/li&gt;
&lt;li&gt;Streaming architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes you don’t need a heavy data pipeline.&lt;/p&gt;

&lt;p&gt;A well-designed incremental job, proper networking, and a watermark table can solve the problem cleanly.&lt;/p&gt;

</description>
      <category>cronjobs</category>
      <category>openshift</category>
      <category>kubernetes</category>
      <category>sql</category>
    </item>
    <item>
      <title>From Helm AGIC Headaches to the AKS Add-on: a Real-World Migration + Troubleshooting Playbook</title>
      <dc:creator>Falolu Olaitan</dc:creator>
      <pubDate>Thu, 16 Oct 2025 20:07:11 +0000</pubDate>
      <link>https://forem.com/devops_oracle/from-helm-agic-headaches-to-the-aks-add-on-a-real-world-migration-troubleshooting-playbook-17a8</link>
      <guid>https://forem.com/devops_oracle/from-helm-agic-headaches-to-the-aks-add-on-a-real-world-migration-troubleshooting-playbook-17a8</guid>
      <description>&lt;p&gt;This write-up distills exactly what we just did: triaging an aging Helm-based AGIC install, fixing identity and tooling gotchas, and cleanly migrating to the AKS ingress-appgw add-on while keeping the same Application Gateway and public IP. I’m keeping it practical—commands, failure modes, and what to check next.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The situation we started with&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AGIC (Helm) was old (1.5.x era) and running in default namespace.&lt;/li&gt;
&lt;li&gt;It still used AAD Pod Identity patterns (aadpodidbinding, USE_MANAGED_IDENTITY_FOR_POD), which are deprecated in favor of Azure Workload Identity (WI). &lt;/li&gt;
&lt;li&gt;A bunch of confusing errors popped up:&lt;/li&gt;
&lt;li&gt;AGIC couldn’t get tokens (“Identity not found”) after UAMI changes.&lt;/li&gt;
&lt;li&gt;APPGW_RESOURCE_ID was corrupted to C:/Program Files/Git/... (Git Bash path conversion).&lt;/li&gt;
&lt;li&gt;An invalid API version warning (older CLI/extensions) during scripting.&lt;/li&gt;
&lt;li&gt;AGIC logs showed malformed ARM targets like Subscription="Git" or empty Name—classic signs of a broken config map.&lt;/li&gt;
&lt;li&gt;We also needed to keep the same IP (52.157.252.178) on the existing App Gateway.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key decisions&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stop fighting the old chart. Microsoft moved AGIC Helm charts to OCI on MCR; the old blob repo is retired. If you stay on Helm, pull from oci://mcr.microsoft.com/azure-application-gateway/charts/ingress-azure and use Workload Identity&lt;/li&gt;
&lt;li&gt;Prefer the AKS add-on for simplicity (identity + RBAC wiring handled for you). You can point it at an existing App Gateway—no new IP if you pass --appgw-id.&lt;/li&gt;
&lt;li&gt;Ensure the gateway is v2 SKU (Standard_v2 or WAF_v2); AGIC requires v2.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What actually fixed things (chronologically)&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Kill Git Bash path mangling
If you must use Git Bash on Windows, disable MSYS path conversion so Azure resource IDs don’t become C:\Program Files\Git..
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export MSYS_NO_PATHCONV=1
export MSYS2_ARG_CONV_EXCL="*"

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Stop the old Helm controller
Running two controllers (Helm + add-on) leads to churn. Uninstall Helm, or at minimum ensure only one is active:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;helm uninstall ingress-azure -n default
kubectl -n default delete deploy,sa,cm,clusterrole,clusterrolebinding -l app=ingress-azure --ignore-not-found=true

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

&lt;/div&gt;



&lt;p&gt;If you keep Helm instead of the add-on, upgrade to the OCI chart and Workload Identity&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable the AKS add-on against the existing App Gateway
This reuses the same gateway and keeps your IP
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APPGW_ID="/subscriptions/&amp;lt;sub&amp;gt;/resourceGroups/&amp;lt;rg&amp;gt;/providers/Microsoft.Network/applicationGateways/&amp;lt;name&amp;gt;"
az aks enable-addons -g &amp;lt;rg&amp;gt; -n &amp;lt;cluster&amp;gt; -a ingress-appgw --appgw-id "$APPGW_ID"

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

&lt;/div&gt;



&lt;p&gt;Microsoft’s tutorial covers enabling the add-on on an existing AKS and existing App Gateway (even in separate VNets)&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check identity/RBAC for the add-on
The add-on wires a user-assigned identity in the node resource group (MC_...). Give it rights on the gateway:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ADDON_MI="/subscriptions/&amp;lt;sub&amp;gt;/resourceGroups/&amp;lt;mc_rg&amp;gt;/providers/Microsoft.ManagedIdentity/userAssignedIdentities/&amp;lt;addon-mi&amp;gt;"
ADDON_PRINCIPAL=$(az identity show --ids "$ADDON_MI" --query principalId -o tsv)

# Required at minimum:
az role assignment create --assignee "$ADDON_PRINCIPAL" --role "Contributor" --scope "$APPGW_ID"

# Helpful read scope at RG (prevents odd read failures of related objects):
az role assignment create --assignee "$ADDON_PRINCIPAL" --role "Reader" \
  --scope "/subscriptions/&amp;lt;sub&amp;gt;/resourceGroups/&amp;lt;gateway-rg&amp;gt;"

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Confirm AGIC is actually watching your Ingress&lt;br&gt;
AGIC processes Ingresses with kubernetes.io/ingress.class: azure/application-gateway or spec.ingressClassName: azure/application-gateway. Your manifest already has the legacy annotation, which is fine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Make sure your Services have Endpoints&lt;br&gt;
Most “backend not updated” cases are just Services resolving to zero endpoints (selectors don’t match pods, wrong targetPort, probes failing). AGIC won’t add pool members without endpoints:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl -n default get svc &amp;lt;name&amp;gt; -o wide
kubectl -n default get endpoints &amp;lt;name&amp;gt; -o wide

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why the original errors happened (and how to recognize them)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“Identity not found” from IMDS after switching identities → the UAMI wasn’t attached to the VMSS (Helm MSI pattern) or AGIC was still configured to an old clientId. Attaching the UAMI to all node scale sets and granting AppGW Contributor resolves it; add-on wires its own identity.&lt;br&gt;
&lt;a href="https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-configure-managed-identities-scale-sets?pivots=identity-mi-methods-azp" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-configure-managed-identities-scale-sets?pivots=identity-mi-methods-azp&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Invalid API version → older CLI/extensions forcing a stale API; upgrade CLI/extensions. (General Azure CLI hygiene.)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you stay on Helm instead of the add-on&lt;/strong&gt;&lt;br&gt;
Use the OCI chart and Workload Identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Enable OIDC + WI
az aks update -g &amp;lt;rg&amp;gt; -n &amp;lt;cluster&amp;gt; --enable-oidc-issuer --enable-workload-identity

# Federate your UAMI to the service account AGIC uses
AKS_OIDC_ISSUER=$(az aks show -g &amp;lt;rg&amp;gt; -n &amp;lt;cluster&amp;gt; --query oidcIssuerProfile.issuerUrl -o tsv)
az identity federated-credential create \
  --name agic \
  --identity-name &amp;lt;your-uami&amp;gt; \
  --resource-group &amp;lt;rg&amp;gt; \
  --issuer "$AKS_OIDC_ISSUER" \
  --subject "system:serviceaccount:&amp;lt;ns&amp;gt;:&amp;lt;sa&amp;gt;"

IDENTITY_CLIENT_ID=$(az identity show -g &amp;lt;rg&amp;gt; -n &amp;lt;your-uami&amp;gt; --query clientId -o tsv)
APPGW_ID="/subscriptions/&amp;lt;sub&amp;gt;/resourceGroups/&amp;lt;rg&amp;gt;/providers/Microsoft.Network/applicationGateways/&amp;lt;name&amp;gt;"

# Upgrade/install from OCI chart on MCR
helm upgrade --install ingress-azure oci://mcr.microsoft.com/azure-application-gateway/charts/ingress-azure \
  -n &amp;lt;ns&amp;gt; \
  --set appgw.applicationGatewayID="$APPGW_ID" \
  --set armAuth.type=workloadIdentity \
  --set armAuth.identityClientID="$IDENTITY_CLIENT_ID" \
  --set rbac.enabled=true

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

&lt;/div&gt;



&lt;p&gt;NOTE: When you enable the add-on with --appgw-id, it reuses your existing App Gateway and therefore keeps the same public IP. Your DNS records pointing at that IP don’t need to change. Creating the add-on without --appgw-id would create a new gateway (and new IP)&lt;/p&gt;

</description>
      <category>aks</category>
      <category>helm</category>
      <category>appgateway</category>
    </item>
    <item>
      <title>How to Deploy AI Model Endpoints in Azure Machine Learning Studio</title>
      <dc:creator>Falolu Olaitan</dc:creator>
      <pubDate>Sat, 17 May 2025 08:16:22 +0000</pubDate>
      <link>https://forem.com/devops_oracle/how-to-deploy-ai-model-endpoints-in-azure-machine-learning-studio-3bla</link>
      <guid>https://forem.com/devops_oracle/how-to-deploy-ai-model-endpoints-in-azure-machine-learning-studio-3bla</guid>
      <description>&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%2Fu13ulbz68ibjvqcsh39a.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%2Fu13ulbz68ibjvqcsh39a.png" alt="ML image" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Azure Machine Learning Studio (Azure ML) is a powerful platform for building, training, and deploying machine learning models. This guide will walk you through creating a new workspace, registering a model, setting up a custom environment, and deploying a model to an endpoint&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create a New Workspace&lt;/strong&gt;&lt;br&gt;
What is an Azure ML Workspace?&lt;br&gt;
A workspace is a foundational resource in Azure ML that provides a centralized place to manage machine learning experiments, resources, and assets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps to Create a Workspace&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to Azure Portal: Go to ml.azure.com.&lt;/li&gt;
&lt;li&gt;Create a New Workspace:&lt;/li&gt;
&lt;li&gt;Click on + Create.&lt;/li&gt;
&lt;li&gt;Fill in the required details:&lt;/li&gt;
&lt;li&gt;Subscription: Select your Azure subscription.&lt;/li&gt;
&lt;li&gt;Resource Group: Choose an existing one or create a new one.&lt;/li&gt;
&lt;li&gt;Workspace Name: Provide a unique name for your workspace.&lt;/li&gt;
&lt;li&gt;Region: Select the region closest to your team or resources.&lt;/li&gt;
&lt;li&gt;Click Review + Create and then Create.&lt;/li&gt;
&lt;/ol&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%2Fh7upifyae1hjufnu70z9.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%2Fh7upifyae1hjufnu70z9.png" alt="workspace" width="643" height="777"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Register Your Model&lt;/strong&gt;&lt;br&gt;
Why Register a Model?&lt;br&gt;
Model registration ensures version control and enables easy deployment and collaboration within your team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps to Register a Model&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to Azure ML Studio:&lt;/li&gt;
&lt;li&gt;Go to your workspace in Azure ML Studio (&lt;a href="https://ml.azure.com" rel="noopener noreferrer"&gt;https://ml.azure.com&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Register the Model:&lt;/li&gt;
&lt;li&gt;Navigate to Assets &amp;gt; Models &amp;gt; Register Model.&lt;/li&gt;
&lt;/ol&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%2Fwh9b3kryrzgu115395wy.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%2Fwh9b3kryrzgu115395wy.png" alt="upload models" width="800" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload your model file (e.g., .pkl, .onnx, or .mlmodel).&lt;/li&gt;
&lt;/ol&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%2Fgnug4rr04l817gtr4imd.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%2Fgnug4rr04l817gtr4imd.png" alt="register image" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Provide details such as:&lt;/li&gt;
&lt;li&gt;Model Name: Give it a unique name.&lt;/li&gt;
&lt;li&gt;Description: Briefly describe the model.&lt;/li&gt;
&lt;/ol&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%2Fajcmyaqz38396hgjiy82.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%2Fajcmyaqz38396hgjiy82.png" alt="model image" width="800" height="751"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Optionally, tag your model for better organization.&lt;/li&gt;
&lt;li&gt;Complete Registration:&lt;/li&gt;
&lt;li&gt;Click Register to store the model in the workspace&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Create a Custom Environment&lt;/strong&gt;&lt;br&gt;
What is an Environment in Azure ML?&lt;br&gt;
An environment encapsulates the dependencies required for model training or inference, such as Python packages, system libraries, and environment variables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps to Create a Custom Environment&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to Environments:&lt;/li&gt;
&lt;li&gt;In Azure ML Studio, go to Assets &amp;gt; Environments.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create a New Environment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click + New Environment.&lt;/li&gt;
&lt;li&gt;Choose Custom Environment and provide:&lt;/li&gt;
&lt;li&gt;Name: A unique name for the environment.&lt;/li&gt;
&lt;li&gt;Description: Details about the environment’s purpose.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Specify Dependencies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using a YAML File: Upload a .yml file containing your dependencies.&lt;/li&gt;
&lt;li&gt;Manually Add Dependencies:&lt;/li&gt;
&lt;li&gt;Choose a base image (e.g., AzureML TensorFlow or AzureML PyTorch).&lt;/li&gt;
&lt;li&gt;Add specific Python packages in the Conda or Pip section.&lt;/li&gt;
&lt;li&gt;Save the Environment&lt;/li&gt;
&lt;li&gt;Review the configuration and click Create.&lt;/li&gt;
&lt;/ol&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%2Fpjx209onznqhgbvepy9k.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%2Fpjx209onznqhgbvepy9k.png" alt="env yaml" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Deploy a Model to an Endpoint&lt;/strong&gt;&lt;br&gt;
What is an Endpoint?&lt;br&gt;
Endpoints expose your model as a web service, allowing applications to interact with it via REST APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps to Deploy a Model&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prepare Your Model and Environment:&lt;/li&gt;
&lt;li&gt;Ensure the model and environment are registered in the workspace.&lt;/li&gt;
&lt;li&gt;Create a Deployment:&lt;/li&gt;
&lt;li&gt;Go to Endpoints &amp;gt; Real-time Endpoints &amp;gt; + New Endpoint.&lt;/li&gt;
&lt;li&gt;Provide details:&lt;/li&gt;
&lt;li&gt;Name: A unique name for the endpoint.&lt;/li&gt;
&lt;li&gt;Compute Type: Choose between managed online endpoints or Kubernetes.&lt;/li&gt;
&lt;li&gt;Specify Deployment Configuration:&lt;/li&gt;
&lt;li&gt;Model: Select the registered model.&lt;/li&gt;
&lt;li&gt;Environment: Choose the custom environment you created.&lt;/li&gt;
&lt;li&gt;Inference Configuration: Define the entry script (e.g., score.py) and other runtime settings.&lt;/li&gt;
&lt;li&gt;Click Deploy and monitor the deployment status.&lt;/li&gt;
&lt;li&gt;Test the Endpoint:&lt;/li&gt;
&lt;li&gt;Once deployed, use the endpoint URL and API key to send test requests using tools like Postman or Python’s requests library.&lt;/li&gt;
&lt;/ol&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%2F9q061sq5pjit8w7f0ifr.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%2F9q061sq5pjit8w7f0ifr.png" alt="Deploy page" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
Azure Machine Learning Studio streamlines the entire machine learning lifecycle, from model development to deployment. By following the steps outlined above, you can effectively manage resources, ensure reproducibility, and deploy your models with ease.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>azure</category>
    </item>
    <item>
      <title>How to Automate Azure App Service IP Whitelisting with Azure DevOps Pipeline</title>
      <dc:creator>Falolu Olaitan</dc:creator>
      <pubDate>Sat, 17 May 2025 07:57:15 +0000</pubDate>
      <link>https://forem.com/devops_oracle/how-to-automate-azure-app-service-ip-whitelisting-with-azure-devops-pipeline-4mda</link>
      <guid>https://forem.com/devops_oracle/how-to-automate-azure-app-service-ip-whitelisting-with-azure-devops-pipeline-4mda</guid>
      <description>&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%2F132367cs4pccha7yqk94.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%2F132367cs4pccha7yqk94.png" alt=" " width="686" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’re managing IP restrictions for an Azure App Service, you’ve likely encountered the need to add, update, or remove IP addresses for access control. Doing this manually can be cumbersome and prone to errors, especially when dealing with multiple environments or services. By using an Azure DevOps (ADO) pipeline, you can automate IP whitelisting, ensuring that changes are applied consistently.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll walk you through using the Azure CLI in an Azure DevOps pipeline to manage IP restrictions dynamically. We’ll set up a pipeline that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accepts an IP address and rule name as parameters.&lt;/strong&gt;&lt;br&gt;
Checks if an existing IP restriction with the specified name already exists. Deletes the existing rule if found and adds the new IP restriction with a specified priority. let's dive in!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;br&gt;
Before we get started, make sure you have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Azure CLI installed on your DevOps agent.&lt;/li&gt;
&lt;li&gt;Azure Service Connection in ADO, allowing access to your Azure subscription.&lt;/li&gt;
&lt;li&gt;Resource Group and App Service name where you plan to implement IP restrictions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Understanding the Azure CLI Commands&lt;/strong&gt;&lt;br&gt;
The Azure CLI provides straightforward commands for managing access restrictions. Here’s a quick breakdown:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add an IP Restriction&lt;/strong&gt;&lt;br&gt;
This command adds an IP address to the list of allowed addresses for your app service, specifying a priority to manage the order of rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az webapp config access-restriction add \
--resource-group &amp;lt;RESOURCE_GROUP&amp;gt; \
--name &amp;lt;APP_SERVICE_NAME&amp;gt; \
--rule-name &amp;lt;RULE_NAME&amp;gt; \
--ip-address &amp;lt;IP_ADDRESS&amp;gt; \
--priority &amp;lt;PRIORITY&amp;gt; \
--action Allow

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Remove an IP Restriction by Name&lt;/strong&gt;&lt;br&gt;
This command deletes an IP restriction by referencing the rule name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az webapp config access-restriction remove \
--resource-group &amp;lt;RESOURCE_GROUP&amp;gt; \
--name &amp;lt;APP_SERVICE_NAME&amp;gt; \
--rule-name &amp;lt;RULE_NAME&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Setting Up the Azure DevOps Pipeline&lt;/strong&gt;&lt;br&gt;
Now, we’ll create an ADO pipeline that uses these CLI commands. This pipeline will take three parameters: ruleName, ipAddress, and priority. If a rule with the specified name already exists, it will be deleted before adding the new IP restriction.&lt;/p&gt;

&lt;p&gt;Here’s the complete YAML file for the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;trigger: none

parameters:
- name: ruleName
displayName: 'Name of the IP Rule'
type: string
default: ''
- name: ipAddress
displayName: 'IP Address to Allow'
type: string
default: ''
- name: priority
displayName: 'Priority of the Rule'
type: number # Corrected type from 'int' to 'number'
default: 100

jobs:
- job: ManageAppServiceIP
displayName: 'Manage App Service IP Whitelisting'
pool:
vmImage: 'ubuntu-latest'

steps:
- task: AzureCLI@2
displayName: 'Check and Update IP Restriction on App Service'
inputs:
azureSubscription: '&amp;lt;YOUR_AZURE_SERVICE_CONNECTION&amp;gt;'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
# Define variables
RESOURCE_GROUP="&amp;lt;RESOURCE_GROUP&amp;gt;"
APP_SERVICE_NAME="&amp;lt;APP_SERVICE_NAME&amp;gt;"
RULE_NAME="${{ parameters.ruleName }}"
IP_ADDRESS="${{ parameters.ipAddress }}"
PRIORITY="${{ parameters.priority }}"

echo "Checking if IP restriction rule exists for ${RULE_NAME}..."

# Check if the IP rule with the specified name already exists
EXISTING_RULE=$(az webapp config access-restriction show \
--resource-group $RESOURCE_GROUP \
--name $APP_SERVICE_NAME \
--query "ipSecurityRestrictions[?name=='$RULE_NAME']" \
-o tsv)

# If rule exists, delete it
if [[ -n "$EXISTING_RULE" ]]; then
echo "Rule ${RULE_NAME} exists. Deleting existing rule..."
az webapp config access-restriction remove \
--resource-group $RESOURCE_GROUP \
--name $APP_SERVICE_NAME \
--rule-name $RULE_NAME
echo "Existing rule ${RULE_NAME} deleted."
else
echo "No existing rule found for ${RULE_NAME}. Adding new rule."
fi

# Add the new IP restriction with priority
echo "Adding IP restriction for ${IP_ADDRESS} with name ${RULE_NAME} and priority ${PRIORITY}..."
az webapp config access-restriction add \
--resource-group $RESOURCE_GROUP \
--name $APP_SERVICE_NAME \
--rule-name $RULE_NAME \
--ip-address $IP_ADDRESS \
--priority $PRIORITY \
--action Allow
echo "IP restriction rule ${RULE_NAME} added successfully with priority ${PRIORITY}."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Fixing YAML Errors in ADO&lt;/strong&gt;&lt;br&gt;
When working with YAML files in ADO, you may encounter validation errors. For example, if you receive an error like String does not match the pattern of “^boolean$”, it could indicate a type mismatch.&lt;/p&gt;

&lt;p&gt;In our case, the type of priority was initially set to int, which Azure DevOps expects as number. Changing it from int to number resolved the error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- name: priority
displayName: 'Priority of the Rule'
type: number # Set type to 'number' instead of 'int'
default: 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conclusion&lt;br&gt;
Automating IP whitelisting for an Azure App Service saves time and reduces human error. By using an ADO pipeline, you ensure that IP restriction rules are managed consistently across environments. This setup is flexible, allowing you to update IP restrictions simply by providing new inputs when running the pipeline.&lt;/p&gt;

&lt;p&gt;Tip: Consider adding notifications or approval steps in ADO if you’re managing critical IP whitelisting to prevent accidental overrides.&lt;br&gt;
Happy automating!&lt;/p&gt;

</description>
      <category>azure</category>
      <category>appservivice</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
