<?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: manny300</title>
    <description>The latest articles on DEV Community by manny300 (@manny300).</description>
    <link>https://dev.to/manny300</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%2F787659%2F6c3da68e-1c2c-42f3-a42b-885328ad64d9.jpg</url>
      <title>DEV Community: manny300</title>
      <link>https://dev.to/manny300</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manny300"/>
    <language>en</language>
    <item>
      <title>I Built an IAM System That Onboards and Offboards Users in Under 60 Seconds</title>
      <dc:creator>manny300</dc:creator>
      <pubDate>Wed, 12 Aug 2026 11:26:22 +0000</pubDate>
      <link>https://dev.to/manny300/i-built-an-iam-system-that-onboards-and-offboards-users-in-under-60-seconds-9n0</link>
      <guid>https://dev.to/manny300/i-built-an-iam-system-that-onboards-and-offboards-users-in-under-60-seconds-9n0</guid>
      <description>&lt;p&gt;If you are just getting started with Identity and Access Management on Azure, this post is for you. I will walk you through how I designed and automated a complete access governance system for a fictional fintech company; covering infrastructure provisioning, intern onboarding, and zero-trust offboarding. Using Bash scripts, Azure RBAC, and GitHub Actions CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;This is the kind of project that sits at the intersection of cloud engineering and security. It is not glamorous, but it is one of those solutions that keep organizations from ending up in the news for all the wrong reasons.&lt;/p&gt;

&lt;p&gt;This is One of my Capstone projects in my TechCrush Cloud Engineering bootcamp series. If you want to see where this journey started, you can read my previous posts where I tackled &lt;a href="https://dev.to/manny300/from-zero-to-multi-region-my-experience-deploying-on-azure-for-the-first-time-cj3"&gt;deploying a web app across two Azure regions&lt;/a&gt; and &lt;a href="https://dev.to/manny300/how-to-automate-azure-resource-group-creation-with-a-bash-script-8jk"&gt;automating resource group creation for multi-environment workflows&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;VerdantPay is a fintech company growing fast. They bring on interns every cycle, they have permanent engineering teams working across web and database tiers, and people leave. The company needs three things to happen reliably:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;When infrastructure is provisioned&lt;/strong&gt;, the network, resource groups, security groups, and RBAC role assignments should all be created in one operation with a full audit trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When an intern joins&lt;/strong&gt;, they should be added to the correct security group in one command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When someone leaves&lt;/strong&gt;, every group membership, every role assignment, and the account itself should be revoked and verified. Not tomorrow. Not when someone remembers. Immediately.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third one is the one that matters most. A departed engineer who still has Contributor access to your production resource group is not a hypothetical risk. It is one that shows up in compliance audits and incident reports.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Will Need
&lt;/h2&gt;

&lt;p&gt;Before running any of these scripts, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure CLI installed&lt;/strong&gt; on your local machine. Follow the &lt;a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli" rel="noopener noreferrer"&gt;official installation guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An active Azure account&lt;/strong&gt; with at least Pay-As-You-Go. A free account works for most of this, but you will need Entra ID (Azure AD) permissions to create groups and manage users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A GitHub repository&lt;/strong&gt; with &lt;code&gt;AZURE_CREDENTIALS&lt;/code&gt; configured as a repository secret for the CI/CD pipelines.&lt;/li&gt;
&lt;li&gt;A terminal that runs Bash; Linux, macOS, or WSL on Windows.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Understanding the Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Principle: Least Privilege, Enforced by Structure
&lt;/h3&gt;

&lt;p&gt;The core design decision behind this entire system is that &lt;strong&gt;role assignments are made to Azure AD groups, not to individual users&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you assign a role directly to a user, revoking access means finding every individual assignment across every scope. When you assign roles to groups, revoking access means removing the user from the group. One operation. One place to audit.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Role Matrix
&lt;/h3&gt;

&lt;p&gt;Before writing a single line of code, I built a role matrix. Every group, its scope, its permission level, whether the access is time-bound, and the justification for why that level was chosen and not a higher one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Azure Role&lt;/th&gt;
&lt;th&gt;Time-bound&lt;/th&gt;
&lt;th&gt;Why This Level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;InternWebDevs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;web-subnet&lt;/td&gt;
&lt;td&gt;Reader&lt;/td&gt;
&lt;td&gt;Yes, 6 weeks&lt;/td&gt;
&lt;td&gt;Interns need to inspect web-tier resources. Contributor would let them modify infrastructure they should not touch.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;InternDBReadOnly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;db-subnet&lt;/td&gt;
&lt;td&gt;Reader&lt;/td&gt;
&lt;td&gt;Yes, 6 weeks&lt;/td&gt;
&lt;td&gt;Interns need to inspect DB config. They must not modify the database or its network rules.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WebAdmins&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;web-subnet&lt;/td&gt;
&lt;td&gt;Contributor&lt;/td&gt;
&lt;td&gt;No, permanent&lt;/td&gt;
&lt;td&gt;Web admins manage the web tier. Owner is not needed because they do not manage access.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DBAdmins&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;db-subnet&lt;/td&gt;
&lt;td&gt;Contributor&lt;/td&gt;
&lt;td&gt;No, permanent&lt;/td&gt;
&lt;td&gt;DB admins manage the database tier. Reader covers infrastructure. Data actions are granted separately.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule: &lt;strong&gt;no group or role assignment is created without a corresponding row in this matrix.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture
&lt;/h3&gt;

&lt;p&gt;The system has three layers: automation scripts, Azure AD groups, and Azure infrastructure, connected by GitHub Actions pipelines.&lt;/p&gt;

&lt;p&gt;The provisioning script creates the resource group, VNet, subnets, AD groups, and role assignments. The onboarding script adds a user to a group. The offboarding script revokes everything and verifies the revocation. GitHub Actions orchestrates the provisioning and offboarding pipelines with full artifact logging.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Scripts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Provisioning: &lt;code&gt;provision.sh&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This script builds the entire infrastructure and access control layer in one run.&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;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;LOG_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"provision-log.txt"&lt;/span&gt;
&lt;span class="nv"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%dT%H:%M:%SZ'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

log&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;] &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

log &lt;span class="s2"&gt;"Starting provisioning..."&lt;/span&gt;

&lt;span class="nv"&gt;SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az account show &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 1. Create Resource Group&lt;/span&gt;
&lt;span class="nv"&gt;RG_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"verdantpay-rg"&lt;/span&gt;
&lt;span class="nv"&gt;LOCATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"eastus"&lt;/span&gt;

log &lt;span class="s2"&gt;"Creating Resource Group: &lt;/span&gt;&lt;span class="nv"&gt;$RG_NAME&lt;/span&gt;&lt;span class="s2"&gt; in &lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$RG_NAME&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nv"&gt;$LOCATION&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Create VNet and Subnets&lt;/span&gt;
&lt;span class="nv"&gt;VNET_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"verdantpay-vnet"&lt;/span&gt;

az network vnet create &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RG_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$VNET_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--address-prefix&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

az network vnet subnet create &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RG_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--vnet-name&lt;/span&gt; &lt;span class="nv"&gt;$VNET_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"web-subnet"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--address-prefixes&lt;/span&gt; &lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

az network vnet subnet create &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RG_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--vnet-name&lt;/span&gt; &lt;span class="nv"&gt;$VNET_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"db-subnet"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--address-prefixes&lt;/span&gt; &lt;span class="s2"&gt;"10.0.2.0/24"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What matters here:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;set -euo pipefail&lt;/code&gt; at the top is non-negotiable. If any command fails, the script stops. If any variable is unset, the script stops. In infrastructure automation, a script that silently continues after a failure is worse than a script that crashes loudly.&lt;/p&gt;

&lt;p&gt;The script also checks whether subnets and groups already exist before creating them. This makes the script &lt;strong&gt;idempotent&lt;/strong&gt; you can run it ten times and get the same result. This matters in CI/CD where pipelines re-run on retries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating AD Groups and Role Assignments
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;create_ad_group&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;group_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
    &lt;span class="nv"&gt;GROUP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az ad group show &lt;span class="nt"&gt;--group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$group_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GROUP_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;az ad group create &lt;span class="nt"&gt;--display-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$group_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--mail-nickname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$group_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else
        &lt;/span&gt;log &lt;span class="s2"&gt;"Group &lt;/span&gt;&lt;span class="nv"&gt;$group_name&lt;/span&gt;&lt;span class="s2"&gt; already exists."&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

create_ad_group &lt;span class="s2"&gt;"verdantpay-InternWebDevs"&lt;/span&gt;
create_ad_group &lt;span class="s2"&gt;"verdantpay-InternDBReadOnly"&lt;/span&gt;
create_ad_group &lt;span class="s2"&gt;"verdantpay-WebAdmins"&lt;/span&gt;
create_ad_group &lt;span class="s2"&gt;"verdantpay-DBAdmins"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After creating groups, the script waits 30 seconds for Entra ID replication before attempting role assignments. This upgrade was as a result of a notable failed case that I encountered. Group creation is eventually consistent, and assigning a role to a group that has not replicated yet will fail silently or throw an error depending on your timing.&lt;/p&gt;

&lt;p&gt;The role assignment function includes a retry loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;assign_role&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;group_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;
    &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
    &lt;span class="nb"&gt;local wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10

    &lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 &lt;span class="nv"&gt;$retries&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
        &lt;/span&gt;&lt;span class="nv"&gt;GROUP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az ad group show &lt;span class="nt"&gt;--group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$group_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GROUP_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
            &lt;/span&gt;&lt;span class="nb"&gt;break
        &lt;/span&gt;&lt;span class="k"&gt;fi
        &lt;/span&gt;log &lt;span class="s2"&gt;"Group '&lt;/span&gt;&lt;span class="nv"&gt;$group_name&lt;/span&gt;&lt;span class="s2"&gt;' not found yet, retrying in &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;wait&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s... (attempt &lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$retries&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
        &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="nv"&gt;$wait&lt;/span&gt;
    &lt;span class="k"&gt;done

    &lt;/span&gt;&lt;span class="nv"&gt;EXISTING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az role assignment list &lt;span class="nt"&gt;--assignee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GROUP_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'[].id'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXISTING&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;az role assignment create &lt;span class="nt"&gt;--assignee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GROUP_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--role&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$role&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--scope&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else
        &lt;/span&gt;log &lt;span class="s2"&gt;"Role assignment already exists for &lt;/span&gt;&lt;span class="nv"&gt;$group_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of defensive code you write after learning the hard way that Azure AD group replication is not instant. The retry loop gives the system time to catch up without failing the entire pipeline.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Onboarding: &lt;code&gt;onboard.sh&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This script is intentionally simple. One command. One log entry.&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;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;USER_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;GROUP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
&lt;span class="nv"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%dT%H:%M:%SZ'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;EXECUTOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az ad signed-in-user show &lt;span class="nt"&gt;--query&lt;/span&gt; userPrincipalName &lt;span class="nt"&gt;-o&lt;/span&gt; tsv 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"service-principal"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;USER_OBJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az ad user show &lt;span class="nt"&gt;--id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_EMAIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

az ad group member add &lt;span class="nt"&gt;--group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GROUP_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--member-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_OBJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[&lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;] ADDED: User '&lt;/span&gt;&lt;span class="nv"&gt;$USER_EMAIL&lt;/span&gt;&lt;span class="s2"&gt;' to Group '&lt;/span&gt;&lt;span class="nv"&gt;$GROUP_NAME&lt;/span&gt;&lt;span class="s2"&gt;' by '&lt;/span&gt;&lt;span class="nv"&gt;$EXECUTOR&lt;/span&gt;&lt;span class="s2"&gt;'"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"onboarding-log.txt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it logs the executor&lt;/strong&gt;: When an intern gets added to &lt;code&gt;verdantpay-InternWebDevs&lt;/code&gt;, you need to know &lt;em&gt;who&lt;/em&gt; added them. The script captures the signed-in user's principal name. If it is running via a service principal in CI/CD, it logs that instead. This is a basic audit requirement that most onboarding scripts skip.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Offboarding: &lt;code&gt;offboard.sh&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the most important script in the entire system. It follows a strict sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up the user's Object ID&lt;/li&gt;
&lt;li&gt;Capture the &lt;strong&gt;before state&lt;/strong&gt; — every group membership and role assignment&lt;/li&gt;
&lt;li&gt;Remove the user from every AD group&lt;/li&gt;
&lt;li&gt;Delete every direct role assignment&lt;/li&gt;
&lt;li&gt;Disable the Azure AD account&lt;/li&gt;
&lt;li&gt;Verify that all access has been revoked&lt;/li&gt;
&lt;li&gt;Write &lt;code&gt;VERIFICATION: PASS&lt;/code&gt; or &lt;code&gt;VERIFICATION: FAIL&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;USER_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;LOG_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"offboarding-log.txt"&lt;/span&gt;
&lt;span class="nv"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%dT%H:%M:%SZ'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Step 1: Get Object ID&lt;/span&gt;
&lt;span class="nv"&gt;OBJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az ad user show &lt;span class="nt"&gt;--id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_EMAIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Step 2: Capture before-state&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'--- BEFORE STATE ---'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Timestamp: &lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"User: &lt;/span&gt;&lt;span class="nv"&gt;$USER_EMAIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
az ad user get-member-groups &lt;span class="nt"&gt;--id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OBJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--security-enabled-only&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
az role assignment list &lt;span class="nt"&gt;--assignee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OBJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Step 3: Remove from all groups&lt;/span&gt;
&lt;span class="nv"&gt;USER_GROUPS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az ad user get-member-groups &lt;span class="nt"&gt;--id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OBJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--security-enabled-only&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'[].displayName'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;GROUP &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$USER_GROUPS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;az ad group member remove &lt;span class="nt"&gt;--group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GROUP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--member-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OBJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Step 4: Delete direct role assignments&lt;/span&gt;
&lt;span class="nv"&gt;ROLE_IDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az role assignment list &lt;span class="nt"&gt;--assignee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OBJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'[].id'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;ROLE_ID &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$ROLE_IDS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;az role assignment delete &lt;span class="nt"&gt;--ids&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROLE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Step 5: Disable account&lt;/span&gt;
az ad user update &lt;span class="nt"&gt;--id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OBJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--account-enabled&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;

&lt;span class="c"&gt;# Step 6: Verify with retries&lt;/span&gt;
&lt;span class="nv"&gt;VERIFY_RETRIES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;span class="nv"&gt;VERIFY_WAIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;15

&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 &lt;span class="nv"&gt;$VERIFY_RETRIES&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;REMAINING_ROLES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az role assignment list &lt;span class="nt"&gt;--assignee&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OBJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'[]'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;REMAINING_GROUPS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az ad user get-member-groups &lt;span class="nt"&gt;--id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OBJECT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REMAINING_ROLES&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REMAINING_GROUPS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'VERIFICATION: PASS -- all access removed for '&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER_EMAIL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="nb"&gt;exit &lt;/span&gt;0
    &lt;span class="k"&gt;fi

    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Residual access detected, retrying in &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERIFY_WAIT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s... (attempt &lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$VERIFY_RETRIES&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
    &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="nv"&gt;$VERIFY_WAIT&lt;/span&gt;
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'VERIFICATION: FAIL -- residual access detected after retries'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script queries Azure AD and Azure RBAC after the revocation to confirm that all groups and roles are actually gone. If they are not because of replication lag or a partial failure the script retries up to 5 times over 75 seconds. If it still fails, it exits with code 1, which causes the CI/CD pipeline to fail and alert the team.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;|| true&lt;/code&gt; on the removal commands is deliberate. If a user has already been removed from a group (maybe by a previous partial run), the command would fail and &lt;code&gt;set -e&lt;/code&gt; would kill the script before it gets to the other groups. The &lt;code&gt;|| true&lt;/code&gt; lets it continue to the next group while the verification step at the end catches anything that was actually missed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CI/CD Pipelines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Provisioning Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VerdantPay IAM Provisioning&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;scripts/provision.sh'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;scripts/onboard.sh'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&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;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Azure Login&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;creds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CREDENTIALS }}&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;Run provisioning script&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash scripts/provision.sh&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;Upload access log&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&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;access-log&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;provision-log.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Path filtering&lt;/strong&gt; is the design decision here. The pipeline only triggers when &lt;code&gt;provision.sh&lt;/code&gt; or &lt;code&gt;onboard.sh&lt;/code&gt; are modified. Updating the README does not trigger a cloud deployment. This is was done to saves unnecessary runs and keep the audit logs clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  Offboarding Pipeline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VerdantPay Offboarding&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;user_email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Email&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;address&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;offboard'&lt;/span&gt;
        &lt;span class="na"&gt;required&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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;offboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&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;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Azure Login&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;creds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CREDENTIALS }}&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;Run offboarding script&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash scripts/offboard.sh ${{ github.event.inputs.user_email }}&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;Upload offboarding log&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&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;offboarding-log&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;offboarding-log.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;workflow_dispatch&lt;/code&gt; instead of an automated trigger?&lt;/strong&gt; Offboarding is a destructive, irreversible action. Triggering it automatically on a push would be dangerous. &lt;code&gt;workflow_dispatch&lt;/code&gt; requires a security administrator to manually type in the user's email and click Run.&lt;/p&gt;

&lt;p&gt;The offboarding log is uploaded as a GitHub Actions artifact. Security teams can download it after the run to prove that access was revoked at a specific timestamp.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Result Looks Like
&lt;/h2&gt;

&lt;p&gt;After running the full provisioning, I tested the system with real user accounts. The onboarding log captured every addition:&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;[2026-08-03T07:01:49Z] ADDED: User 'temiloluwa@...' to Group 'verdantpay-WebAdmins' by 'emmanuelajibokunedu...'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the offboarding log shows the complete before-and-after state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;---&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;BEFORE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;STATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Timestamp:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2026-08-03&lt;/span&gt;&lt;span class="err"&gt;T&lt;/span&gt;&lt;span class="mi"&gt;07&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="err"&gt;Z&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;User:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;temiloluwa@...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Groups&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;before:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"verdantpay-WebAdmins"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;VERIFICATION:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;PASS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;--&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;access&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;removed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;temiloluwa@...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;VERIFICATION: PASS&lt;/code&gt; line means the script queried Azure after revocation, found zero remaining group memberships and zero remaining role assignments, and confirmed the offboarding was complete.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Improve in a v2
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Time-bound access with Azure AD PIM&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The role matrix specifies that intern access should be time-bound to 6 weeks, but the current scripts do not enforce expiry automatically. In a v2, I would integrate Azure AD Privileged Identity Management (PIM) to set eligible assignments with an automatic expiry date. That way, even if the offboarding script is never run, the access disappears on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Slack or Teams notifications on offboarding&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the offboarding pipeline completes, the result sits in a GitHub Actions log that someone has to go look at. A webhook that posts the PASS/FAIL result to a Slack or Teams channel would close that loop and give security teams instant visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A dedicated teardown script&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I actually built this already, a &lt;code&gt;teardown.sh&lt;/code&gt; that removes everything in reverse order: role assignments first, then AD groups, then the resource group.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Assign roles to groups, never to individual users.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a role matrix before writing code.&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify your revocations.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;set -euo pipefail&lt;/code&gt; in every script.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make your CI/CD pipelines produce evidence.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write a teardown script for everything you provision.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Is Next
&lt;/h2&gt;

&lt;p&gt;Follow me for more contents about Cloud engineering and DevOps&lt;br&gt;
Follow along on &lt;a href="https://dev.to/manny300"&gt;my Dev.to profile&lt;/a&gt; if you want to see how it goes.&lt;/p&gt;




&lt;p&gt;You can find the full scripts, pipeline configs, role matrix, and architecture diagram here: &lt;a href="https://github.com/EmmanuelAjibokun/VerdantPay-IAM" rel="noopener noreferrer"&gt;github.com/EmmanuelAjibokun/VerdantPay-IAM&lt;/a&gt;&lt;/p&gt;

</description>
      <category>identitygovernance</category>
      <category>devsecops</category>
      <category>cloudengineering</category>
      <category>azureiam</category>
    </item>
    <item>
      <title>How to Automate Azure Resource Group Creation with a Bash Script</title>
      <dc:creator>manny300</dc:creator>
      <pubDate>Mon, 08 Jun 2026 18:17:27 +0000</pubDate>
      <link>https://dev.to/manny300/how-to-automate-azure-resource-group-creation-with-a-bash-script-8jk</link>
      <guid>https://dev.to/manny300/how-to-automate-azure-resource-group-creation-with-a-bash-script-8jk</guid>
      <description>&lt;p&gt;If you are just getting started with Azure CLI and Bash scripting, this post is for you. I will walk you through how I automated the creation of Azure resource groups for multiple environments using a single Bash script — something that was taking a cloud admin several manual steps every week.&lt;/p&gt;

&lt;p&gt;This is Project 2 in my TechRush Cloud Engineering bootcamp series. If you want to see where this journey started, you can read my previous post where I tackled &lt;a href="https://dev.to/manny300/from-zero-to-multi-region-my-experience-deploying-on-azure-for-the-first-time-cj3"&gt;deploying a web app across two Azure regions for the first time&lt;/a&gt;. That project involved real blockers — quota limits, CLI version mismatches, and a deep dive into Azure Resource Providers. This one went smoother, and I think that is because the previous project was the hard school.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Imagine a cloud administrator who has to create five resource groups every single week, one for each active project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project-A-RG
Project-B-RG
Project-C-RG
Project-D-RG
Project-E-RG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every week. By hand. Management's response was simple: automate it.&lt;/p&gt;

&lt;p&gt;But here is where the task gets more interesting. Instead of creating one flat resource group per project, the better approach is to create &lt;strong&gt;four resource groups per project&lt;/strong&gt; — one for each environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dev&lt;/li&gt;
&lt;li&gt;Test&lt;/li&gt;
&lt;li&gt;UAT&lt;/li&gt;
&lt;li&gt;Production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because each environment needs its own access controls, cost tracking, and lifecycle rules. You do not want your Development environment sharing a resource group with Production. Keeping them separate is a real-world cloud best practice, not just a bootcamp exercise.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Will Need
&lt;/h2&gt;

&lt;p&gt;Before running this script, make sure you have the following set up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure CLI installed&lt;/strong&gt; on your local machine. You can follow the &lt;a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli" rel="noopener noreferrer"&gt;official installation guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An active Azure account&lt;/strong&gt;. A free account works fine for this.&lt;/li&gt;
&lt;li&gt;A terminal that runs Bash — Linux, macOS, or WSL on Windows.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Understanding the Design
&lt;/h2&gt;

&lt;p&gt;The core idea behind this script is &lt;strong&gt;parameterization&lt;/strong&gt;. Instead of hardcoding project names, the script accepts a project name as input and uses it as a prefix for every resource group it creates.&lt;/p&gt;

&lt;p&gt;So if you enter &lt;code&gt;Project-A&lt;/code&gt;, you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project-A-RG-Dev
Project-A-RG-Test
Project-A-RG-UAT
Project-A-RG-Production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next week, you run the same script, enter &lt;code&gt;Project-B&lt;/code&gt;, and get the same structure with a different prefix. The script never changes. Only the input does.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;RG&lt;/code&gt; in the middle is there to make the resource type clear at a glance. When you are looking at a list of twenty Azure resources, names that include what the resource is save you a lot of time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Script
&lt;/h2&gt;

&lt;p&gt;Create a file called &lt;code&gt;deploy.sh&lt;/code&gt; and paste the following:&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;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Check if the user is logged into Azure&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; az account show &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Not logged in. Run 'az login' first."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Prompt the user for a project name&lt;/span&gt;
&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Enter Project Name: "&lt;/span&gt; ProjectName

&lt;span class="c"&gt;# Validate that the input is not empty&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ProjectName&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Project Name cannot be empty."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Inform the user that resource group creation is starting&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Creating resource groups for &lt;/span&gt;&lt;span class="nv"&gt;$ProjectName&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;

&lt;span class="c"&gt;# Create one resource group per environment&lt;/span&gt;
az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ProjectName&lt;/span&gt;&lt;span class="s2"&gt;-RG-Dev"&lt;/span&gt;        &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"eastus"&lt;/span&gt;
az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ProjectName&lt;/span&gt;&lt;span class="s2"&gt;-RG-Test"&lt;/span&gt;       &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"eastus"&lt;/span&gt;
az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ProjectName&lt;/span&gt;&lt;span class="s2"&gt;-RG-UAT"&lt;/span&gt;        &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"eastus"&lt;/span&gt;
az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ProjectName&lt;/span&gt;&lt;span class="s2"&gt;-RG-Production"&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"eastus"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Resource groups created successfully."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now make the script executable:&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;chmod&lt;/span&gt; +x deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./deploy.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Walking Through the Script
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Login check&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; az account show &amp;amp;&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The very first thing the script does is check whether you are already logged into Azure. If you are not, it stops immediately and tells you exactly what to do. This is called a &lt;strong&gt;guard clause&lt;/strong&gt; — you check your preconditions before doing any real work. It prevents confusing errors further down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input prompt&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="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Enter Project Name: "&lt;/span&gt; ProjectName
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;read&lt;/code&gt; command pauses the script and waits for you to type something. Whatever you type gets stored in the variable &lt;code&gt;ProjectName&lt;/code&gt;. The &lt;code&gt;-p&lt;/code&gt; flag lets you show a prompt message at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty input validation&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ProjectName&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the user just presses Enter without typing anything, &lt;code&gt;ProjectName&lt;/code&gt; will be empty. Without this check, the script would go ahead and try to create resource groups with names like &lt;code&gt;-RG-Dev&lt;/code&gt;, which is not useful to anyone. This check catches that and exits cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resource group creation&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;az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ProjectName&lt;/span&gt;&lt;span class="s2"&gt;-RG-Dev"&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"eastus"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the Azure CLI command that does the actual work. The &lt;code&gt;--name&lt;/code&gt; flag uses string interpolation to combine the variable with the environment suffix. The &lt;code&gt;--location&lt;/code&gt; flag tells Azure which region to deploy to. You can change &lt;code&gt;eastus&lt;/code&gt; to any region that is available on your subscription.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Result Looks Like
&lt;/h2&gt;

&lt;p&gt;After running the script with &lt;code&gt;FI_deparment&lt;/code&gt; as the project name (from the actual assignment run), the Azure portal showed the following resource groups created successfully:&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%2Fecfecnj26gboi2ie60nk.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%2Fecfecnj26gboi2ie60nk.png" alt="Azure portal showing four resource groups: FI_deparment-RG-Dev, FI_deparment-RG-Test, FI_deparment-RG-UAT, FI_deparment-RG-Production"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Improve in a v2
&lt;/h2&gt;

&lt;p&gt;Shipping something that works is step one. Thinking about what comes next is what separates a script you wrote once from a script a team can actually use.&lt;/p&gt;

&lt;p&gt;Two things I would change:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Add a location prompt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Right now the region is hardcoded to &lt;code&gt;eastus&lt;/code&gt;. A slightly better script would also ask the user for their preferred region. Different teams or clients might need resources in different geographies, and hardcoding a region removes that flexibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Replace the four &lt;code&gt;az group create&lt;/code&gt; lines with a loop&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The current script has four nearly identical lines. If the environments ever changed — say, you needed to add a &lt;code&gt;Staging&lt;/code&gt; environment — you would have to manually add another line. A loop over an array is cleaner and easier to extend:&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="nv"&gt;environments&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"Dev"&lt;/span&gt; &lt;span class="s2"&gt;"Test"&lt;/span&gt; &lt;span class="s2"&gt;"UAT"&lt;/span&gt; &lt;span class="s2"&gt;"Production"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="nb"&gt;env &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;environments&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;az group create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ProjectName&lt;/span&gt;&lt;span class="s2"&gt;-RG-&lt;/span&gt;&lt;span class="nv"&gt;$env&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"eastus"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same result, but now adding a new environment is a one-word change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parameterize, do not hardcode.&lt;/strong&gt; A script that accepts input is reusable. A script with hardcoded values is a one-time tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate your inputs early.&lt;/strong&gt; Check that required values exist before doing any real work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name things clearly.&lt;/strong&gt; &lt;code&gt;ProjectName-RG-Dev&lt;/code&gt; tells you the project, the resource type, and the environment at a glance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate environments into separate resource groups.&lt;/strong&gt; Dev and Production should never share a resource group in a real setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a destroy script alongside every deploy script.&lt;/strong&gt; I did not need it here, but the habit of writing a teardown script is what keeps your Azure bill from surprising you.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What Is Next
&lt;/h2&gt;

&lt;p&gt;Assignment 3 is more complex. It covers two scenarios: a one-click deploy script that provisions a full environment stack for non-technical staff, and a university migration to Azure where each department gets its own set of resources — with a requirement to support 20 more departments the following year. That last part is a design thinking challenge, not just a scripting challenge.&lt;/p&gt;

&lt;p&gt;I will write about that one too. Follow along on &lt;a href="https://dev.to/manny300"&gt;my Dev.to profile&lt;/a&gt; if you want to see how it goes.&lt;/p&gt;




&lt;p&gt;You can find the full script and repo here: &lt;a href="https://github.com/EmmanuelAjibokun/Techcrush-Ass-2" rel="noopener noreferrer"&gt;github.com/EmmanuelAjibokun/Techcrush-Ass-2&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudcomputing</category>
      <category>azure</category>
      <category>devops</category>
      <category>bash</category>
    </item>
    <item>
      <title>From Zero to Multi-Region: My experience deploying on Azure for the First Time</title>
      <dc:creator>manny300</dc:creator>
      <pubDate>Sat, 30 May 2026 17:27:37 +0000</pubDate>
      <link>https://dev.to/manny300/from-zero-to-multi-region-my-experience-deploying-on-azure-for-the-first-time-cj3</link>
      <guid>https://dev.to/manny300/from-zero-to-multi-region-my-experience-deploying-on-azure-for-the-first-time-cj3</guid>
      <description>&lt;p&gt;Starting this project, I was not as lost as I was with the first one.&lt;br&gt;
If you haven't seen the first one, here it is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/manny300/a-cloud-service-model-decision-framework-comparing-iaas-paas-saas-trade-offs-for-three-4jd8" class="crayons-story__hidden-navigation-link"&gt;A cloud service model decision framework comparing IaaS / PaaS / SaaS trade-offs for three production workloads&lt;/a&gt;


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

          &lt;a href="/manny300" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F787659%2F6c3da68e-1c2c-42f3-a42b-885328ad64d9.jpg" alt="manny300 profile" class="crayons-avatar__image" width="800" height="739"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/manny300" class="crayons-story__secondary fw-medium m:hidden"&gt;
              manny300
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                manny300
                
              
              &lt;div id="story-author-preview-content-3668993" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/manny300" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F787659%2F6c3da68e-1c2c-42f3-a42b-885328ad64d9.jpg" class="crayons-avatar__image" alt="" width="800" height="739"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;manny300&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/manny300/a-cloud-service-model-decision-framework-comparing-iaas-paas-saas-trade-offs-for-three-4jd8" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 14&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/manny300/a-cloud-service-model-decision-framework-comparing-iaas-paas-saas-trade-offs-for-three-4jd8" id="article-link-3668993"&gt;
          A cloud service model decision framework comparing IaaS / PaaS / SaaS trade-offs for three production workloads
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cloudcomputing"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cloudcomputing&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/solutionarchitect"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;solutionarchitect&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cloudsolution"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cloudsolution&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/manny300/a-cloud-service-model-decision-framework-comparing-iaas-paas-saas-trade-offs-for-three-4jd8#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

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

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

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

&lt;/div&gt;


&lt;p&gt;This time I had some familiarity coming in. The first thing I did was explore the Azure portal, clicked around, created a Resource Group, setup an App Service, and it worked.&lt;/p&gt;

&lt;p&gt;Then I moved on to the CLI, which is where things started to feel funny.&lt;/p&gt;




&lt;h2&gt;
  
  
  First Attempt: Azure Cloud Shell
&lt;/h2&gt;

&lt;p&gt;My first attempt was with Azure Cloud Shell. It did not feel natural to me. My main confusion was around shell scripts. I was not sure if I could write and run a deploy script from inside Cloud Shell.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In hindsight, the answer is yes. You can use a text editor like Vim/Emacs right inside Cloud Shell and execute your script from there. But I did not think of that at the time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I moved on to installing the Azure CLI locally on my machine.&lt;/p&gt;

&lt;p&gt;That took a couple of days on its own, but working on my local machine felt more natural. The first installation method I tried did not work. I eventually got it working, moved into VS Code, and started writing my deploy script properly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;Take the same static website I built for Project 1 and deploy it to Azure using the Web App Service, across two different regions.&lt;/p&gt;

&lt;p&gt;I wrote a &lt;code&gt;deploy.sh&lt;/code&gt; script and a &lt;code&gt;destroy.sh&lt;/code&gt; script. The destroy script is the habit that saves you from racking up charges on resources you forgot about.&lt;/p&gt;

&lt;p&gt;The script flow followed Azure's hierarchy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a &lt;strong&gt;Resource Group&lt;/strong&gt;, every resource on Azure must belong to one&lt;/li&gt;
&lt;li&gt;Create an &lt;strong&gt;App Service Plan&lt;/strong&gt;, this is where you choose the OS for your web app&lt;/li&gt;
&lt;li&gt;Create the &lt;strong&gt;Web App&lt;/strong&gt; on top of that plan&lt;/li&gt;
&lt;li&gt;Deploy your files&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The App Service Plan is worth understanding properly. It is not just a billing tier. Even though we are working with Platform as a Service, meaning we are not managing the underlying infrastructure ourselves, we still choose the operating system. Azure handles everything below that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting West Europe Working
&lt;/h2&gt;

&lt;p&gt;Getting the West Europe deployment working took some back and forth with the commands. Some flags I used were not recognized by my version of the CLI, and some runtime options that older documentation referenced no&lt;br&gt;
longer exist.&lt;br&gt;
&lt;em&gt;Static site hosting on a Web App Service Plan with a Linux OS, for example, is not supported. The Web App Service Plan works with Windows OS for deploying static sites.&lt;/em&gt; Each of these were small walls that required reading error messages carefully, researching, and adjusting.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Wall: East US Quota Limits
&lt;/h2&gt;

&lt;p&gt;The second half of the challenge was deploying to a second region, East US.&lt;br&gt;
I expected it to work the same way West Europe did. It did not.&lt;br&gt;
The error I kept hitting was about quota. Specifically, the &lt;strong&gt;Total VMs limit for East US was set to zero&lt;/strong&gt; on my subscription. No VMs available meant no App Service Plan could be created, which meant no deployment.&lt;/p&gt;

&lt;p&gt;My first instinct was that maybe the SKU was the problem. I was using &lt;code&gt;F1&lt;/code&gt;, which is the free tier. I went into the Azure portal and looked through the quota section under the Web App provider. I could see that certain SKUs like &lt;code&gt;P1V4&lt;/code&gt; had limits of 10 or 30 in that region, while &lt;code&gt;F1&lt;/code&gt; was sitting at zero.&lt;/p&gt;

&lt;p&gt;I changed the SKU in my script to one of those available tiers, ran the script again, and hit the same wall. &lt;em&gt;The error was not about the SKU specifically. It was about the Total VM quota for the region, and that articular quota had no option to adjust the limit from the portal.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Rabbit Hole: Resource Providers
&lt;/h2&gt;

&lt;p&gt;Around this time I started paying closer attention to how Azure&lt;br&gt;
organizes its services. I came across &lt;strong&gt;Resource Providers&lt;/strong&gt;, which I had not really thought about before.&lt;/p&gt;

&lt;p&gt;Every Azure service belongs to a provider namespace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.Web&lt;/code&gt; for App Services&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.Compute&lt;/code&gt; for Virtual Machines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before you can use any service, the corresponding provider needs to be registered on your subscription. On a free account, some providers are not registered by default, and quotas are locked.&lt;/p&gt;

&lt;p&gt;I went down the rabbit hole of manually registering the Compute&lt;br&gt;
provider, only to discover that the Web provider I actually needed had already been registered automatically.&lt;br&gt;
&lt;em&gt;It was a detour, but it was a useful one.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Upgrading to Pay As You Go
&lt;/h2&gt;

&lt;p&gt;I upgraded from the free tier to Pay As You Go. I still have my $200 free credit for the first 30 days, so the cost risk was manageable.&lt;br&gt;
After upgrading, the provider situation improved and quotas became adjustable.&lt;br&gt;
But the East US Total VM limit was still at zero and still had no adjustment option in the portal. I reached out to Microsoft engineering support and submitted a formal quota increase request for two instances.&lt;br&gt;
&lt;em&gt;That request is still being processed.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Finding a Different Path
&lt;/h2&gt;

&lt;p&gt;While that was still on, I found a different path.&lt;br&gt;
Since what I am deploying is a static website, Azure has a dedicated &lt;strong&gt;Static Web Apps&lt;/strong&gt; service that sits outside the App Service quota system entirely. I deployed to it and it worked without hitting any quota restrictions at all. I was able to deploy to two different regions successfully using that service.&lt;/p&gt;

&lt;p&gt;I also went back and tested West Europe with the original Web App Service approach, and it worked there as well. So I now have deployments running across multiple regions through two different methods.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Unanswered Question
&lt;/h2&gt;

&lt;p&gt;As a Pay As You Go subscriber, I would expect to be able to deploy to any region Azure offers. But right now I am restricted to one region for Web App Service deployments while the quota request gets processed.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If anyone has run into this and know the reason for this, I would genuinely like to know. It is the kind of thing that would matter on a real project with a deadline.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read the Full Project&lt;/p&gt;

&lt;p&gt;You can have a look at the project by clicking the link below:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/EmmanuelAjibokun" rel="noopener noreferrer"&gt;
        EmmanuelAjibokun
      &lt;/a&gt; / &lt;a href="https://github.com/EmmanuelAjibokun/Azure-Web-App" rel="noopener noreferrer"&gt;
        Azure-Web-App
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A small web application (a static site is fine; even better, a "Hello, [your name]" Flask app) deployed into Azure App Service in two regions, behind a Resource Group you provisioned entirely from the Azure CLI — no portal clicks for the deployment itself.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Azure Web App&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Deploy a Node.js app to Azure using the Azure CLI. This repo contains two deployment scripts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/EmmanuelAjibokun/Azure-Web-App/deploy-app.sh" rel="noopener noreferrer"&gt;deploy-app.sh&lt;/a&gt; — deploys the local Node.js app from the &lt;code&gt;app/&lt;/code&gt; folder using &lt;code&gt;az webapp up&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/EmmanuelAjibokun/Azure-Web-App/deploy-static.sh" rel="noopener noreferrer"&gt;deploy-static.sh&lt;/a&gt; — provisions a Static Web App (East US 2) and a Web App from GitHub (West Europe)&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Prerequisites&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli" rel="nofollow noopener noreferrer"&gt;Azure CLI&lt;/a&gt; installed&lt;/li&gt;
&lt;li&gt;An active Azure subscription&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://git-scm.com/" rel="nofollow noopener noreferrer"&gt;Git&lt;/a&gt; installed&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quickstart&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;1. Clone the repo&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;git clone https://github.com/EmmanuelAjibokun/cloud-eng.git
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; azure-web-app&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;2. Log in to Azure&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;az login&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;3. (Optional) Set your active subscription&lt;/h3&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;az account list --output table
az account &lt;span class="pl-c1"&gt;set&lt;/span&gt; --subscription &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&amp;lt;your-subscription-id&amp;gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Option A — Deploy the local Node.js app&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;This deploys the &lt;code&gt;app/&lt;/code&gt; folder directly to an Azure Web App using &lt;code&gt;az webapp up&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;chmod +x deploy-app.sh
./deploy-app.sh&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;What it does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Checks that you are logged in to the Azure CLI — exits with an error if not&lt;/li&gt;
&lt;li&gt;Checks if &lt;code&gt;cloud-decision-east&lt;/code&gt; is already…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/EmmanuelAjibokun/Azure-Web-App" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;If you would like to follow along on the learn-by-doing journey,&lt;br&gt;
comment "github" and I will send you the project guide link. Kindly star the project and follow me on GitHub.&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/EmmanuelAjibokun" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F96999656%3Fv%3D4%3Fs%3D400" height="460" class="m-0" width="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/EmmanuelAjibokun" rel="noopener noreferrer" class="c-link"&gt;
            EmmanuelAjibokun (Emmanuel Ajibokun ) · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            A computer programming enthusiast, Geologist by field of study and a fast learner - EmmanuelAjibokun
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg" width="32" height="32"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>cloudcomputing</category>
      <category>azure</category>
      <category>cloudsolution</category>
      <category>staticwebapps</category>
    </item>
    <item>
      <title>A cloud service model decision framework comparing IaaS / PaaS / SaaS trade-offs for three production workloads</title>
      <dc:creator>manny300</dc:creator>
      <pubDate>Thu, 14 May 2026 10:27:54 +0000</pubDate>
      <link>https://dev.to/manny300/a-cloud-service-model-decision-framework-comparing-iaas-paas-saas-trade-offs-for-three-4jd8</link>
      <guid>https://dev.to/manny300/a-cloud-service-model-decision-framework-comparing-iaas-paas-saas-trade-offs-for-three-4jd8</guid>
      <description>&lt;h2&gt;
  
  
  From Geology to Cloud Computing: My First Project 🌍
&lt;/h2&gt;

&lt;p&gt;Diving into the world of cloud computing as an applied geology graduate, &lt;br&gt;
having moved from frontend development with React and Next.js to backend &lt;br&gt;
development with Node.js.&lt;/p&gt;

&lt;p&gt;This is not just another tech skill to learn. This season could be the &lt;br&gt;
beginning of a career transition for me. Because of that, I paid a &lt;br&gt;
subsidised price to join the TechCrush bootcamp. But I know that is not &lt;br&gt;
enough. If I want to finish with the outcome I desire, I must put in &lt;br&gt;
extra work.&lt;/p&gt;


&lt;h2&gt;
  
  
  How I Planned the Work
&lt;/h2&gt;

&lt;p&gt;The first thing I did was prompt Claude with some considerations that &lt;br&gt;
included 10 job posting requirements in cloud computing and DevOps, the &lt;br&gt;
course modules that would be covered during the bootcamp, and a sample &lt;br&gt;
task by Forrest Brazeal called the #CloudResumeChallenge. Here is the &lt;br&gt;
prompt I used:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I'm transitioning my career into cloud engineering, and I feel the &lt;br&gt;
3-month training covers the majority of these opportunities. I want to &lt;br&gt;
have projects for each milestone, in the same order that I would be &lt;br&gt;
doing them in the training. Create a similar guide to the one by &lt;br&gt;
Forrest Brazeal's #CloudResumeChallenge. Juxtapose my learning and job &lt;br&gt;
requirements to map out the projects."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This mapped out &lt;strong&gt;9 projects&lt;/strong&gt; to work on by the end of my bootcamp.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first project is what this post is about.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Humbling Part
&lt;/h2&gt;

&lt;p&gt;I had one introductory class and one main class with my tutor. I &lt;br&gt;
actually started working on the first project before my classes began. &lt;br&gt;
I had studied on my own, watched about 6 YouTube videos, and was &lt;br&gt;
feeling pretty confident.&lt;/p&gt;

&lt;p&gt;Then, to my great amusement, I did not seem to understand the task at &lt;br&gt;
all, let alone know where to get started. It was one of the strangest &lt;br&gt;
feelings, and I remember thinking to myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I don't understand what is expected of me."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I gave AI this prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I'm just starting out in this career, I don't want the answer &lt;br&gt;
handed out to me, but I need resources that can help me better &lt;br&gt;
understand the question, and information on what to learn in order to &lt;br&gt;
complete the task."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With some targeted reading and research, I was able to complete the &lt;br&gt;
project. 🎉&lt;/p&gt;


&lt;h2&gt;
  
  
  Read the Full Project
&lt;/h2&gt;

&lt;p&gt;You can read about my findings by clicking the link below:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://emmanuelajibokun.github.io/cloud-decision-document/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;emmanuelajibokun.github.io&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;&lt;em&gt;If you would like to follow along on the learn-by-doing journey, &lt;br&gt;
comment "github" and I will send you the project guide link. Kindly &lt;br&gt;
star the project and follow me on GitHub.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudcomputing</category>
      <category>devops</category>
      <category>solutionarchitect</category>
      <category>cloudsolution</category>
    </item>
  </channel>
</rss>
