<?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: Ipadeola Taiwo</title>
    <description>The latest articles on DEV Community by Ipadeola Taiwo (@highpee1991).</description>
    <link>https://dev.to/highpee1991</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%2F1894849%2F48eadcb7-7cf7-4299-8728-58c535f1e6bd.jpeg</url>
      <title>DEV Community: Ipadeola Taiwo</title>
      <link>https://dev.to/highpee1991</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/highpee1991"/>
    <language>en</language>
    <item>
      <title>Building And Securing An Azure Virtual Machine With Terraform, From Zero To A Working Website</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Sun, 12 Jul 2026 11:19:54 +0000</pubDate>
      <link>https://dev.to/highpee1991/building-and-securing-an-azure-virtual-machine-with-terraform-from-zero-to-a-working-website-4n17</link>
      <guid>https://dev.to/highpee1991/building-and-securing-an-azure-virtual-machine-with-terraform-from-zero-to-a-working-website-4n17</guid>
      <description>&lt;p&gt;This is a full walkthrough of a real Terraform project, provisioning a Linux virtual machine on Azure, securing it properly, and deploying a working web server on it. Every command in this post is one I actually ran, every error is one I actually hit, and every fix is exactly how I solved it. My goal here is not just to show you a finished project, my goal is to teach you how to build this yourself, line by line, understanding why each piece exists and what happens when something goes wrong.&lt;/p&gt;

&lt;p&gt;If you follow this post from top to bottom, you will end with your own Linux server running on Azure, provisioned entirely as code, reachable over SSH, and serving a real webpage to anyone who visits its public address.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;A single Ubuntu virtual machine, sitting inside its own virtual network, protected by a firewall that only allows the two ports it actually needs, SSH for management and HTTP for web traffic, with a static public IP address so that address never changes once assigned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Terraform installed on your machine, version one point zero or higher. The Azure CLI installed and signed in with &lt;code&gt;az login&lt;/code&gt;. An active Azure subscription. And an SSH key pair already generated on your machine, most systems already have one sitting at &lt;code&gt;~/.ssh/id_rsa.pub&lt;/code&gt;, if not, generate one with &lt;code&gt;ssh-keygen&lt;/code&gt; before continuing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;Four files make up this entire project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;providers.tf
variables.tf
main.tf
outputs.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Terraform does not care how you split your code across files. Every file in the same folder is automatically treated as one combined configuration. Splitting things apart like this is purely for readability, so let's go through each file and understand what it is doing.&lt;/p&gt;
&lt;h2&gt;
  
  
  providers.tf, Telling Terraform Which Cloud We Are Talking To
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/azurerm"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"4.80.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"azurerm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;features&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This file has one job, telling Terraform that this project talks to Azure, using a specific, pinned version of the provider. Pinning the version matters more than it might look like at first, if you leave this open ended, your project could quietly behave differently every time you run it, depending on whatever the latest provider release happens to be that day. Pinning it to &lt;code&gt;4.80.0&lt;/code&gt; means this project behaves exactly the same way every single time, on any machine, forever.&lt;/p&gt;
&lt;h2&gt;
  
  
  variables.tf, The Values That Drive Everything Else
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"project"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"project name"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform_vm"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"location"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"azure region"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"East US"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"owner"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"owner tag"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hagital"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"vm-size"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vm size"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard_B1ls"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Four variables, the project name, the Azure region, an owner tag for organizing resources, and the size of the virtual machine. Every resource in &lt;code&gt;main.tf&lt;/code&gt; pulls its values from here rather than having anything typed directly into it, so changing the region or the VM size later means editing one line here, not hunting through every resource individually.&lt;/p&gt;

&lt;p&gt;One thing worth explaining clearly, since it genuinely confused me the first time I wrote a Terraform project. If you come from a general programming background, you are used to variables needing to be exported from one file and imported into another. Terraform does not work that way at all. There is no export keyword, no import statement, nothing to wire together. Once a variable is declared anywhere in this folder, it is available anywhere else in this same folder, simply by writing &lt;code&gt;var.project&lt;/code&gt;, or &lt;code&gt;var.location&lt;/code&gt;, or whichever name you gave it. The folder itself is the boundary, not the individual file.&lt;/p&gt;
&lt;h2&gt;
  
  
  main.tf, The Actual Infrastructure
&lt;/h2&gt;

&lt;p&gt;This is the heart of the project. Nine resources, built up in the order they logically depend on each other. Let's go through each one.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Resource Group
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_resource_group"&lt;/span&gt; &lt;span class="s2"&gt;"hagital-rg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_hagital_rg"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hagital-vm"&lt;/span&gt;
    &lt;span class="nx"&gt;Owner&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;
    &lt;span class="nx"&gt;Project&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every resource in Azure needs a container to live inside, called a resource group. This is the first thing created, and every other resource in this file references it, either directly or indirectly.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Virtual Network And Subnet
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_network"&lt;/span&gt; &lt;span class="s2"&gt;"hagital-vnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_hagital_vnet"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;address_space&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Hagital_vnet"&lt;/span&gt;
    &lt;span class="nx"&gt;Owner&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;
    &lt;span class="nx"&gt;Project&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"hagital-subnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_hagital_subnet"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;virtual_network_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_virtual_network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-vnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;address_prefixes&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A VM needs a network to sit inside before it can exist. The virtual network defines the overall address space, and the subnet carves out a smaller section of it for this specific machine to live in. Notice how &lt;code&gt;resource_group_name&lt;/code&gt; here is not typed as a plain string, it directly references &lt;code&gt;azurerm_resource_group.hagital-rg.name&lt;/code&gt;. This is how Terraform understands dependency order automatically, it sees that the subnet needs the resource group to exist first, and builds everything in the correct sequence without you having to say so explicitly.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Firewall, And Its Rules
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"NSG"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_hagital_NSG"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_security_rule"&lt;/span&gt; &lt;span class="s2"&gt;"NSG_rule_port_80"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;nsg_port_80"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
  &lt;span class="nx"&gt;direction&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;
  &lt;span class="nx"&gt;access&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Tcp"&lt;/span&gt;
  &lt;span class="nx"&gt;source_port_range&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
  &lt;span class="nx"&gt;destination_port_range&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"80"&lt;/span&gt;
  &lt;span class="nx"&gt;source_address_prefix&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
  &lt;span class="nx"&gt;destination_address_prefix&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;network_security_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NSG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_security_rule"&lt;/span&gt; &lt;span class="s2"&gt;"NSG_rule_port_22"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;nsg_port-22"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;
  &lt;span class="nx"&gt;direction&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound"&lt;/span&gt;
  &lt;span class="nx"&gt;access&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Tcp"&lt;/span&gt;
  &lt;span class="nx"&gt;source_port_range&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
  &lt;span class="nx"&gt;destination_port_range&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"22"&lt;/span&gt;
  &lt;span class="nx"&gt;source_address_prefix&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
  &lt;span class="nx"&gt;destination_address_prefix&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;network_security_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NSG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A network security group, NSG for short, is Azure's firewall. On its own it does nothing, it is just a container for rules. Each rule below it defines exactly one allowed pattern of traffic.&lt;/p&gt;

&lt;p&gt;Both rules here are inbound, meaning traffic coming into the VM from outside, not traffic leaving it. Port eighty allows web visitors to reach the site once it is running. Port twenty two allows SSH connections in, so you can actually manage the server. Notice each rule has its own priority number, one hundred and one hundred one. Two inbound rules in Azure can never share the same priority, each one needs a unique number, and spacing them out, rather than using consecutive numbers, leaves room to insert new rules later without renumbering everything.&lt;/p&gt;

&lt;p&gt;Notice too, &lt;code&gt;source_port_range&lt;/code&gt; is set to a wildcard on both rules, while &lt;code&gt;destination_port_range&lt;/code&gt; is the actual port being protected. This trips a lot of people up at first. The source port is whatever random, temporary port your browser or SSH client happens to open the connection from, you cannot predict or control that, so it is always a wildcard. The destination port is the one you actually care about, the port on the server itself.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Public IP
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_public_ip"&lt;/span&gt; &lt;span class="s2"&gt;"public-ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_public_ip"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;allocation_method&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Static"&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public_ip"&lt;/span&gt;
    &lt;span class="nx"&gt;Owner&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;
    &lt;span class="nx"&gt;Projects&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Without this, the VM would only be reachable on a private, internal address, nothing you could actually connect to from your own laptop. &lt;code&gt;allocation_method&lt;/code&gt; is set to &lt;code&gt;Static&lt;/code&gt; rather than &lt;code&gt;Dynamic&lt;/code&gt;, deliberately. A static IP stays the same even if the VM is stopped and restarted, it only changes if the public IP resource itself is deleted. A dynamic IP, by contrast, can change every time the VM restarts, which becomes a real annoyance once you are actively developing against a known address.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Network Interface
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_interface"&lt;/span&gt; &lt;span class="s2"&gt;"NIC"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_NIC"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;ip_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;                          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"internal"&lt;/span&gt;
    &lt;span class="nx"&gt;subnet_id&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="nx"&gt;private_ip_address_allocation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Dynamic"&lt;/span&gt;
    &lt;span class="nx"&gt;public_ip_address_id&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_public_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public-ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the piece that physically connects everything together. The network interface card, NIC for short, is what actually plugs the VM into the subnet, and it is also where the public IP gets attached. A VM cannot connect directly to a subnet or a public IP on its own, it always goes through a network interface in between.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Virtual Machine Itself
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_linux_virtual_machine"&lt;/span&gt; &lt;span class="s2"&gt;"linux-vm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_linux_vm"&lt;/span&gt;
  &lt;span class="nx"&gt;computer_name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagital-vm"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;size&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vm-size&lt;/span&gt;
  &lt;span class="nx"&gt;admin_username&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;user"&lt;/span&gt;
  &lt;span class="nx"&gt;network_interface_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm_network_interface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NIC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;admin_ssh_key&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;username&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;user"&lt;/span&gt;
    &lt;span class="nx"&gt;public_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"~/.ssh/id_rsa.pub"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;os_disk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;caching&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ReadWrite"&lt;/span&gt;
    &lt;span class="nx"&gt;storage_account_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard_LRS"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;source_image_reference&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;publisher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Canonical"&lt;/span&gt;
    &lt;span class="nx"&gt;offer&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0001-com-ubuntu-server-jammy"&lt;/span&gt;
    &lt;span class="nx"&gt;sku&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"22_04-lts"&lt;/span&gt;
    &lt;span class="nx"&gt;version&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"latest"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This resource references everything built above it, the network interface for connectivity, an Ubuntu twenty two image from Canonical for the operating system, and an SSH key read directly from your local machine using the &lt;code&gt;file()&lt;/code&gt; function, rather than pasting the key as text. One detail worth calling out, &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;computer_name&lt;/code&gt; are two different things here, &lt;code&gt;name&lt;/code&gt; is the label Azure uses for the resource itself, while &lt;code&gt;computer_name&lt;/code&gt; becomes the actual hostname inside the running Linux machine, and Linux hostnames follow much stricter naming rules than Azure resource names do, more on that shortly when we get to the errors.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Missing Piece Most Tutorials Skip
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_interface_security_group_association"&lt;/span&gt; &lt;span class="s2"&gt;"nsg_association"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;network_interface_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_interface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NIC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;network_security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NSG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This resource genuinely tripped me up, and I want to explain it clearly because I suspect it will trip other people up too. Creating a network security group and writing rules for it does not automatically apply those rules to anything. The NSG just sits there, unused, until it is explicitly attached to something, in this case, the network interface. Without this association resource, your firewall rules exist in Azure but affect nothing at all, and Azure falls back to blocking inbound internet traffic by default. This one resource is what actually switches your firewall rules on.&lt;/p&gt;
&lt;h2&gt;
  
  
  outputs.tf, Getting Useful Information Back
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"resource_group_created"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"resource group name"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"Vnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vnet name"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_virtual_network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-vnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"subnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"subnet name"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hagital-subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"NSG"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"NSG name"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NSG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"port_80_rule"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound port 80 created"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_security_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NSG_rule_port_80&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination_port_range&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"port_22_rule"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Inbound port 22 created"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_security_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NSG_rule_port_22&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination_port_range&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"public_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public ip"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_public_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public-ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"vm_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"azure vm name"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_linux_virtual_machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;linux-vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"vm_public_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vm attached public ip"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_linux_virtual_machine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;linux-vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_ip_address&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Outputs print useful information to the terminal automatically after every apply, so you are not stuck manually looking things up in the Azure Portal afterward. The most useful one here by far is &lt;code&gt;vm_public_ip&lt;/code&gt;, since that is the exact address needed to actually SSH into the machine once it exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Errors I Ran Into, And How They Were Fixed
&lt;/h2&gt;
&lt;h1&gt;
  
  
  A quick note before this section, the code shown above already reflects every correction. You do not need to change anything, this section exists purely to walk through what went wrong the first time and how each issue was diagnosed.
&lt;/h1&gt;

&lt;p&gt;This is the part most tutorials skip entirely, and it is the part I think actually matters most. Nothing here worked perfectly on the first attempt. Every error below is real, and understanding each one teaches you something a clean, error free run never would.&lt;/p&gt;
&lt;h3&gt;
  
  
  Running terraform init
&lt;/h3&gt;


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

&lt;/div&gt;


&lt;p&gt;This command downloads the azurerm provider and prepares the working directory. The very first time I ran this, it failed.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Failed to query available provider packages
Could not retrieve the list of available versions for provider hashcorp/azurerm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Read that carefully. It says &lt;code&gt;hashcorp&lt;/code&gt;, missing the letter i. The real provider is published as &lt;code&gt;hashicorp&lt;/code&gt;, with the i. Somewhere in my configuration, that name had been typed incorrectly, and Terraform was searching for a provider that simply does not exist. Correcting the spelling and running &lt;code&gt;terraform init&lt;/code&gt; again resolved it immediately.&lt;/p&gt;

&lt;p&gt;A second, completely different failure showed up on another attempt.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Failed to install provider
Error while installing hashicorp/azurerm v4.80.0: releases.hashicorp.com: local error: tls: bad record MAC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This one had nothing to do with my code at all, it was a network connection problem, an interrupted or unstable connection while downloading the provider. The fix here was simply waiting a moment and retrying the same command again.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once the connection stabilized, it installed cleanly.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Subscription ID Requirement
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;terraform plan&lt;/code&gt; for the first time in this particular project produced a new error I had not seen in earlier Terraform projects.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: `subscription_id` is a required provider property when performing a plan/apply operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This turned out to be a documented change, starting with version four point zero of the azurerm provider, explicitly setting a subscription ID in the provider block became mandatory. In an earlier Terraform project of mine, this had not come up, likely due to how that specific environment resolved it implicitly. The fix here was reverting cleanly to provider version &lt;code&gt;4.80.0&lt;/code&gt; after clearing out the previously cached &lt;code&gt;.terraform&lt;/code&gt; folder and lock file, forcing Terraform to properly resolve the version fresh, which sidestepped the issue entirely for this setup.&lt;/p&gt;
&lt;h3&gt;
  
  
  Reviewing The Plan
&lt;/h3&gt;

&lt;p&gt;Once initialization succeeded, &lt;code&gt;terraform plan&lt;/code&gt; produced a full, detailed preview of every resource about to be created, nine in total, resource group, virtual network, subnet, NSG, two security rules, public IP, network interface, and the VM itself.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: 9 to add, 0 to change, 0 to destroy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the single most important habit in Terraform, always read the plan before applying it. It tells you exactly what is about to happen before it actually happens, and it is your last chance to catch a mistake before real infrastructure gets created and starts costing money.&lt;/p&gt;
&lt;h3&gt;
  
  
  Running terraform apply, And The First Real Failure
&lt;/h3&gt;


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

&lt;/div&gt;


&lt;p&gt;Typing &lt;code&gt;yes&lt;/code&gt; at the confirmation prompt kicked off creation. Eight resources succeeded cleanly, resource group, network, subnet, NSG, both rules, public IP, and network interface, each completing within seconds. Then the VM itself failed.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: unable to assume default computer name "computer_name" cannot contain the special characters:
`\/"[]:|&amp;lt;&amp;gt;+=;,?*@&amp;amp;~!#$%^()_{}'`. Please adjust the `name`, or specify an explicit `computer_name`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is a genuinely useful error to understand. Azure resource names and Linux hostnames follow completely different rule sets. My VM's &lt;code&gt;name&lt;/code&gt; value contained underscores, which Azure resource names allow without issue, that is exactly why the resource group, VNet, and NSG all created successfully with underscores in their own names. But without an explicit &lt;code&gt;computer_name&lt;/code&gt; set, Terraform tries to generate one automatically from the resource name, and a Linux hostname cannot contain underscores at all. The fix was adding an explicit, separate &lt;code&gt;computer_name&lt;/code&gt; value using only letters, numbers, and hyphens, which is exactly what you see in the finished &lt;code&gt;main.tf&lt;/code&gt; above, &lt;code&gt;computer_name = "hagital-vm"&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Second Failure, An Invalid VM Size
&lt;/h3&gt;

&lt;p&gt;After fixing the computer name and running &lt;code&gt;terraform plan&lt;/code&gt; again, a different error appeared, this time about the VM size itself.&lt;/p&gt;

&lt;p&gt;The originally typed size, &lt;code&gt;Standard_B1_ls&lt;/code&gt;, is not a valid Azure size string. Azure VM sizes follow a fixed format defined by Azure itself, and this one had an extra underscore that did not belong. The correct value is &lt;code&gt;Standard_B1ls&lt;/code&gt;, no separator between B one and ls, one continuous string. You can always confirm exactly which sizes are valid and available in your specific region directly from the CLI, rather than typing one from memory:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vm list-sizes &lt;span class="nt"&gt;--location&lt;/span&gt; eastus &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Correcting &lt;code&gt;variables.tf&lt;/code&gt; to &lt;code&gt;Standard_B1ls&lt;/code&gt; and running &lt;code&gt;terraform plan&lt;/code&gt; again confirmed it was accepted.&lt;/p&gt;
&lt;h3&gt;
  
  
  Applying Again, Successfully This Time
&lt;/h3&gt;


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

&lt;/div&gt;


&lt;p&gt;This time, all nine resources built successfully.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

Outputs:

NSG = "terraform_vm_hagital_NSG"
Vnet = "terraform_vm_hagital_vnet"
port_22_rule = "22"
port_80_rule = "80"
public_ip = "/subscriptions/.../publicIPAddresses/terraform_vm_public_ip"
resource_group_created = "terraform_vm_hagital_rg"
subnet = "terraform_vm_hagital_subnet"
vm_name = "terraform_vm_linux_vm"
vm_public_ip = "20.169.155.100"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every output printed automatically, exactly as designed, including the actual public IP address, ready to copy straight into an SSH command.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Connection That Timed Out
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh Hagitaluser@20.169.155.100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh: connect to host 20.169.155.100 port 22: Connection timed out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the error that taught me the most out of this entire project. Everything in the plan had applied successfully, no errors, nine resources created, yet SSH could not reach the machine at all. A connection timeout like this, rather than an outright refusal, is the signature of a firewall silently dropping the request rather than actively rejecting it.&lt;/p&gt;

&lt;p&gt;The cause traced back to something I had genuinely overlooked. Creating a network security group and writing rules for it does not automatically apply those rules to anything. I had built the NSG, and I had written both inbound rules, port twenty two and port eighty, but I had never actually attached that NSG to the network interface. The rules existed, but they were not switched on.&lt;/p&gt;

&lt;p&gt;The fix was adding one more resource, exactly the association block shown earlier in &lt;code&gt;main.tf&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_interface_security_group_association"&lt;/span&gt; &lt;span class="s2"&gt;"nsg_association"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;network_interface_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_interface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NIC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;network_security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_network_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NSG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One resource added. Trying SSH again immediately after:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh Hagitaluser@20.169.155.100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;The authenticity of host '20.169.155.100 (20.169.155.100)' can't be established.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '20.169.155.100' (ED25519) to the list of known hosts.
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1059-azure x86_64)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Connected. That fingerprint warning the first time is completely normal, it is simply your machine confirming, for the first time, that it trusts this brand new server's identity, the same thing happens connecting to any new server for the first time, not just this one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Configuring The Server
&lt;/h2&gt;

&lt;p&gt;With SSH access working, I updated the system and installed Nginx, the actual web server this whole project was built to 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="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once installed, I confirmed the service was genuinely running, not just installed.&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 status nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;●&lt;/span&gt; &lt;span class="err"&gt;nginx.service&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;A&lt;/span&gt; &lt;span class="err"&gt;high&lt;/span&gt; &lt;span class="err"&gt;performance&lt;/span&gt; &lt;span class="err"&gt;web&lt;/span&gt; &lt;span class="err"&gt;server&lt;/span&gt; &lt;span class="err"&gt;and&lt;/span&gt; &lt;span class="err"&gt;a&lt;/span&gt; &lt;span class="err"&gt;reverse&lt;/span&gt; &lt;span class="err"&gt;proxy&lt;/span&gt; &lt;span class="err"&gt;server&lt;/span&gt;
     &lt;span class="err"&gt;Active:&lt;/span&gt; &lt;span class="err"&gt;active&lt;/span&gt; &lt;span class="err"&gt;(running)&lt;/span&gt; &lt;span class="err"&gt;since&lt;/span&gt; &lt;span class="err"&gt;Sun&lt;/span&gt; &lt;span class="err"&gt;2026-07-12&lt;/span&gt; &lt;span class="err"&gt;10:30:47&lt;/span&gt; &lt;span class="err"&gt;UTC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then verified it was actually serving content, first from directly inside the VM itself.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl 20.169.155.100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Welcome to nginx!&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Verifying From The Outside World
&lt;/h2&gt;

&lt;p&gt;The final and most satisfying check, opening the VM's public IP address directly in a browser from my own laptop, completely outside of Azure and outside of the VM itself.&lt;/p&gt;

&lt;p&gt;The page loaded correctly, the default Nginx welcome page, confirmed reachable from the open internet. That "not secure" warning next to the address bar is expected and not a problem, it simply means the connection is running over plain HTTP rather than HTTPS, adding a TLS certificate would be the natural next step to remove that warning, a good project on its own.&lt;/p&gt;
&lt;h2&gt;
  
  
  What This Project Actually Proves
&lt;/h2&gt;

&lt;p&gt;Every piece of this was provisioned as code, version controlled, and repeatable, not clicked together by hand. Five distinct, real errors were hit along the way, a provider name typo, a network connectivity drop, a Linux hostname naming rule, an invalid Azure VM size string, and a missing NSG association that silently left the firewall rules inactive despite a fully successful apply. Every one of them was diagnosed by actually reading the error message closely and understanding what it was describing, not by guessing or copying a fix blindly.&lt;/p&gt;

&lt;p&gt;The result is a real, working, secured Linux server, reachable over SSH, serving a live webpage to anyone on the internet, built entirely from four Terraform files and a lot of careful debugging.&lt;/p&gt;

&lt;p&gt;If you followed along and built this yourself, or if you hit a different error than the ones documented here, I would genuinely like to hear about it in the comments. Debugging infrastructure is a skill built entirely through hitting exactly this kind of wall and working through it, and comparing notes with someone else doing the same thing is one of the fastest ways to get better at it.&lt;/p&gt;

&lt;p&gt;Github Link:&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/highpee1991" rel="noopener noreferrer"&gt;
        highpee1991
      &lt;/a&gt; / &lt;a href="https://github.com/highpee1991/Terraform-Virtual-Machine-Deployment" rel="noopener noreferrer"&gt;
        Terraform-Virtual-Machine-Deployment
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
    </item>
    <item>
      <title>My First Real Terraform Project, Two Environments, One Storage Account, And A Few Lessons I Did Not See Coming</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Sat, 11 Jul 2026 15:52:14 +0000</pubDate>
      <link>https://dev.to/highpee1991/my-first-real-terraform-project-two-environments-one-storage-account-and-a-few-lessons-i-did-not-392k</link>
      <guid>https://dev.to/highpee1991/my-first-real-terraform-project-two-environments-one-storage-account-and-a-few-lessons-i-did-not-392k</guid>
      <description>&lt;p&gt;A few days ago I said I was starting Infrastructure as Code. Today I actually sat down and wrote my first proper Terraform project, and I want to walk you through exactly what I built, what confused me at first, and what broke along the way, because the broken parts taught me more than the parts that just worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Set Out To Build
&lt;/h2&gt;

&lt;p&gt;The idea was simple on paper. A small payments project needs two separate environments, one for development and one for staging, each represented by its own resource group in Azure. On top of that, the development environment needed a storage account with a blob container inside it, the kind of setup you would actually see on a real team, where dev and staging are kept apart on purpose.&lt;/p&gt;

&lt;p&gt;Nothing here was complicated in terms of what Azure resources were involved. What made it worth doing was doing it the Terraform way, writing it once as code, and letting Terraform figure out how to create everything in the right order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part That Actually Confused Me At First, Variables
&lt;/h2&gt;

&lt;p&gt;Before I even got to writing resources, I had to deal with variables, and this is the part I really want to talk about, because it caught me off guard in a way I was not expecting.&lt;/p&gt;

&lt;p&gt;If you come from a general programming background, even a light one, you are used to variables needing to be exported and imported somehow. You define something in one file, and if you want to use it somewhere else, you usually have to explicitly export it, then import it wherever you need it. That is just how most languages work.&lt;/p&gt;

&lt;p&gt;So naturally, when I created a separate file just for my variables, my first thought was, how do I export this so my other files can actually see it. I genuinely sat there for a moment trying to figure out the Terraform equivalent of an export statement.&lt;/p&gt;

&lt;p&gt;Turns out, there isn't one, and there does not need to be.&lt;/p&gt;

&lt;p&gt;In Terraform, every file inside the same working directory is automatically part of the same configuration. It does not matter if your variables live in &lt;code&gt;variables.tf&lt;/code&gt;, your resources live in &lt;code&gt;main.tf&lt;/code&gt;, and your outputs live in &lt;code&gt;outputs.tf&lt;/code&gt;. Terraform reads every &lt;code&gt;.tf&lt;/code&gt; file in that folder as one combined configuration. Once a variable is declared, anywhere in that folder, you can reference it anywhere else in that same folder, just by calling &lt;code&gt;var.whatever_you_named_it&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No import statement. No export keyword. No file path linking one file to another. The folder itself is the boundary, not the individual file.&lt;/p&gt;

&lt;p&gt;Once that clicked, it actually made the whole language feel simpler than I expected. Splitting files apart in Terraform is purely for organization and readability, not because the tool needs you to wire files together the way you would in something like JavaScript or Python.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting The Foundation, Provider And Variables
&lt;/h2&gt;

&lt;p&gt;Every Terraform project needs to know which provider it is talking to, in this case Azure, and which version of that provider to use. I pinned mine to a specific version rather than leaving it open ended, so the project behaves the same way every time it runs, regardless of what the latest provider release happens to be on a given day.&lt;/p&gt;

&lt;p&gt;Then came the three variables that would drive the whole project, the project name, the Azure region, and an owner tag, each with a sensible default so I would not have to retype the same values across every resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building The Actual Infrastructure
&lt;/h2&gt;

&lt;p&gt;With the foundation in place, I defined four resources. Two resource groups, one for dev and one for staging, a storage account sitting inside the dev resource group, and a storage container sitting inside that storage account.&lt;/p&gt;

&lt;p&gt;What I liked here is how naturally Terraform expresses the relationship between resources. I did not have to manually tell the storage account which resource group it belonged to using some ID I copied from somewhere. I just referenced the resource group resource directly, and Terraform understood the dependency and the order to create things in, entirely on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Actually Broke, And What Each Break Taught Me
&lt;/h2&gt;

&lt;p&gt;I want to be upfront about this part, because I think it is the most useful part of this whole post. Nothing here worked perfectly on the first attempt, and I think that is normal, and worth documenting honestly rather than pretending it all went smoothly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first error showed up right at &lt;code&gt;terraform init&lt;/code&gt;.&lt;/strong&gt; Terraform told me it could not find a provider called &lt;code&gt;hashcorp/azurerm&lt;/code&gt;. I stared at that for longer than I want to admit before I noticed the actual issue, the error was missing a letter. The real provider is &lt;code&gt;hashicorp&lt;/code&gt;, not &lt;code&gt;hashcorp&lt;/code&gt;. One missing letter somewhere in my configuration was enough to send Terraform looking for a provider that does not exist. Fixing the spelling and running init again solved it immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second error happened during &lt;code&gt;terraform plan&lt;/code&gt;.&lt;/strong&gt; This one looked scarier than it actually was. Terraform said it ran into a problem while trying to automatically register an Azure resource provider called Microsoft.Kusto, something I was not even using directly, and the process got interrupted. I simply ran the plan again, and it went through cleanly the second time. Turns out Terraform tries to register a whole set of Azure resource providers behind the scenes the first time you use certain resource types, and on a lighter subscription that step can be slow enough to get cut off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The third error was the most straightforward, but the easiest to overlook.&lt;/strong&gt; Azure rejected my storage account name because it contained a hyphen. Storage account names in Azure have to be lowercase letters and numbers only, no hyphens allowed, which is different from resource group names, where hyphens are perfectly fine. I had used the same naming pattern across both without realizing storage accounts play by different rules. Removing the hyphen fixed it right away.&lt;/p&gt;

&lt;p&gt;None of these were difficult once I understood what was actually happening, but each one forced me to slow down and actually read the error message properly instead of assuming I already knew what went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirming It Actually Worked
&lt;/h2&gt;

&lt;p&gt;Once everything applied successfully, I did not just trust the terminal output. I went into the Azure Portal directly and checked every piece, both resource groups showing up correctly under the right region, the storage account sitting inside the correct resource group, and the container present inside that storage account with the access level I had configured. Seeing it match exactly what Terraform said it created was a genuinely satisfying moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tearing It Down
&lt;/h2&gt;

&lt;p&gt;Since this was a lab and not something meant to run permanently, I finished by running a full destroy, and watched Terraform take everything down in the correct reverse order, the container first, then the storage account, then both resource groups, all cleanly removed with a single command and a typed confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Project Actually Taught Me
&lt;/h2&gt;

&lt;p&gt;Beyond the individual errors, the biggest shift for me was realizing how much Terraform handles on its own once you describe what you want clearly. I did not have to think about ordering. I did not have to manually track dependencies between resources. I described the end state, and Terraform figured out how to get there, and how to safely undo it later.&lt;/p&gt;

&lt;p&gt;I also learned something small but important while wrapping up, my working folder had files sitting there that should never end up in version control, the local state files and the provider cache directory in particular, since state files can end up holding sensitive information about the resources they track. That is going straight into a &lt;code&gt;.gitignore&lt;/code&gt; before this project touches GitHub properly.&lt;/p&gt;

&lt;p&gt;This is only the first project in what I expect will be a much longer run with Terraform. If you have worked with it longer than I have, and there is something you wish someone had told you this early on, I would genuinely like to hear it in the comments.&lt;/p&gt;

&lt;p&gt;Below is link to my github repo&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/highpee1991" rel="noopener noreferrer"&gt;
        highpee1991
      &lt;/a&gt; / &lt;a href="https://github.com/highpee1991/azure-terraform-lab" rel="noopener noreferrer"&gt;
        azure-terraform-lab
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      terraform
    &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;h2 class="heading-element"&gt;Understand the 4 Core Files&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Every Terraform project has this structure:&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;azure-terraform-lab/
├── main.tf           # Your resources live here
├── variables.tf      # Input variables (reusable values)
├── outputs.tf        # What Terraform prints after running
└── providers.tf      # Which cloud you are connecting to&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Terraform command to use daily&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;1. Download the Azure provider plugin&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;terraform init&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;2. Preview what Terraform WILL do — never skip this&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;terraform plan&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;3. Actually build the infrastructure&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;terraform apply&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;4. Destroy everything safely when done&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;terraform destroy&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/highpee1991/azure-terraform-lab" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
    </item>
    <item>
      <title>I Deployed a Live Website on Azure Using Only the CLI, No Portal, No Clicking</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Mon, 06 Jul 2026 19:46:15 +0000</pubDate>
      <link>https://dev.to/highpee1991/i-deployed-a-live-website-on-azure-using-only-the-cli-no-portal-no-clicking-4men</link>
      <guid>https://dev.to/highpee1991/i-deployed-a-live-website-on-azure-using-only-the-cli-no-portal-no-clicking-4men</guid>
      <description>&lt;p&gt;There is a certain kind of confidence you only get when you type a command into a terminal and watch an actual server come to life in another part of the world. No dashboard, no wizard, no clicking through screens. Just you, a terminal, and a set of decisions you have to make yourself.&lt;/p&gt;

&lt;p&gt;That is exactly what I set out to do. The brief was simple to read but not simple to execute: deploy a Linux server on Azure, connect to it, configure it, secure it properly, and document the whole process the way a real DevOps engineer would, as a runbook someone else could follow and trust.&lt;/p&gt;

&lt;p&gt;I want to walk you through how I did it, the mistakes I made along the way, and how you can follow the same path yourself if you are trying to build this skill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Did This the Hard Way
&lt;/h2&gt;

&lt;p&gt;It is tempting to open the Azure portal, click a few buttons, and have a server running in minutes. That works, but it does not teach you anything that scales. The moment you need to provision ten servers, or repeat the same setup for a client, clicking through a UI becomes a liability rather than a convenience.&lt;/p&gt;

&lt;p&gt;So I chose the terminal instead. Every resource group, every network, every rule was created with a command I typed myself, so that I would actually understand what was happening underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step One, Giving the Project a Home
&lt;/h2&gt;

&lt;p&gt;Every resource in Azure needs a container to live in, called a resource group. Think of it as the folder that keeps everything related to one project together, so you can manage it, monitor it, or delete it as a single unit later.&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; hagital-rg &lt;span class="nt"&gt;--location&lt;/span&gt; eastus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That one line created a logical home for everything that followed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step Two, Building the Network Before the Server
&lt;/h2&gt;

&lt;p&gt;Before you can have a server, you need a network for it to live inside. This is one of those things that feels backwards if you are used to clicking through a portal, where the network gets created for you automatically. On the CLI, you build it yourself, which forces you to actually understand how the pieces fit together.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az network vnet create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; hagital-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; provion-server-vnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--address-prefix&lt;/span&gt; 10.0.0.0/16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-name&lt;/span&gt; provison-server-subnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-prefix&lt;/span&gt; 10.0.0.0/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I then double checked everything before moving forward, because catching a mistake here is far easier than catching it after a server is already attached to the wrong network.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az network vnet show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; hagital-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; provion-server-vnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"{VNet:addressSpace.addressPrefixes, Subnets:subnets[].addressPrefix}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step Three, Bringing the Server to Life
&lt;/h2&gt;

&lt;p&gt;With the network ready, I created the virtual machine itself, an Ubuntu server, placed directly inside the network I had just built.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vm create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; hagital-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; hagital-server &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; Ubuntu2204 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--size&lt;/span&gt; Standard_B1ls &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vnet-name&lt;/span&gt; provion-server-vnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet&lt;/span&gt; provison-server-subnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--admin-username&lt;/span&gt; azureuser &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--generate-ssh-keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One detail worth mentioning here, something that genuinely surprised me. When you create a server through the Azure portal, you are usually handed a private key file to download and guard carefully. On the CLI, the &lt;code&gt;generate-ssh-keys&lt;/code&gt; flag handles that for you. Azure generates the key pair locally and wires up the public key automatically, so there is no separate file to lose track of. Small detail, but it changed how I think about secure access.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step Four, Locking the Front Door, Then Only Opening What Is Needed
&lt;/h2&gt;

&lt;p&gt;A server exposed to the entire internet on every port is a server waiting to be compromised. So the very next thing I did was open only the two ports this server actually needed, port twenty two for SSH access, and port eighty for web traffic. Nothing else.&lt;/p&gt;

&lt;p&gt;Opening the first port went smoothly.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vm open-port &lt;span class="nt"&gt;--resource-group&lt;/span&gt; hagital-rg &lt;span class="nt"&gt;--name&lt;/span&gt; hagital-server &lt;span class="nt"&gt;--port&lt;/span&gt; 22
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Opening the second one did not.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vm open-port &lt;span class="nt"&gt;--resource-group&lt;/span&gt; hagital-rg &lt;span class="nt"&gt;--name&lt;/span&gt; hagital-server &lt;span class="nt"&gt;--port&lt;/span&gt; 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Azure responded with an error I had never seen before.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SecurityRuleConflict: Security rule open-port-22 conflicts with rule open-port-80. Rules cannot have the same Priority and Direction.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the part of the process that actually taught me something. In Azure, every firewall rule, called a network security group rule, has a priority number, and two inbound rules cannot share the same one. The open port command always tries to use the same default priority, nine hundred, no matter how many times you run it. My SSH rule had already claimed that number, so the HTTP rule had nowhere to go.&lt;/p&gt;

&lt;p&gt;I checked the existing rules to see exactly what was going on.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az network nsg rule list &lt;span class="nt"&gt;--resource-group&lt;/span&gt; hagital-rg &lt;span class="nt"&gt;--nsg-name&lt;/span&gt; hagital-serverNSG &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once I understood the cause, the fix was straightforward. I created the HTTP rule manually and gave it its own priority instead of relying on the default.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az network nsg rule create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; hagital-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--nsg-name&lt;/span&gt; hagital-serverNSG &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; AllowHTTP &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--priority&lt;/span&gt; 910 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--direction&lt;/span&gt; Inbound &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--access&lt;/span&gt; Allow &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; Tcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--destination-port-ranges&lt;/span&gt; 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That single error taught me more about how Azure firewall rules work than any tutorial had, because I had to actually understand the priority system to solve it myself.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step Five, Walking In Through SSH
&lt;/h2&gt;

&lt;p&gt;With the network secured, I fetched the server's public IP address and connected directly.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vm show &lt;span class="nt"&gt;--resource-group&lt;/span&gt; hagital-rg &lt;span class="nt"&gt;--name&lt;/span&gt; hagital-server &lt;span class="nt"&gt;--show-details&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; publicIps &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh azureuser@&amp;lt;public-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No password prompt, no key file to search for, just a clean connection using the key pair Azure had already set up.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step Six, Giving the Server a Job
&lt;/h2&gt;

&lt;p&gt;A bare server does nothing on its own. I installed Nginx, a lightweight and widely used web server, so the machine could actually serve web pages to anyone who visited its address.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Running an update before the install is a habit worth keeping. It refreshes the list of available packages so you are installing the current version rather than something outdated sitting in a stale cache.&lt;/p&gt;

&lt;p&gt;A quick check confirmed everything was running as expected.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Step Seven, Getting My Own Project Onto the Server
&lt;/h2&gt;

&lt;p&gt;At this point the server could serve a web page, just not mine yet. I copied a portfolio project, built with plain HTML, CSS and JavaScript, from my local machine onto the server using secure copy.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp &lt;span class="nt"&gt;-r&lt;/span&gt; ./devops_portfolio azureuser@&amp;lt;public-ip&amp;gt;:/tmp/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The next step introduced two more mistakes, both worth sharing because they are easy to make and easy to learn from.&lt;/p&gt;

&lt;p&gt;The first attempt tried to move the entire project folder directly onto the location of the existing default web page, and Azure's server refused, since you cannot replace a single file with an entire folder.&lt;/p&gt;

&lt;p&gt;The second attempt failed for a different reason, a plain permission error, since the folder that serves web content on a Linux server is owned by the system, not by the everyday user account.&lt;/p&gt;

&lt;p&gt;The working solution handled both issues at once, moving the contents of the folder rather than the folder itself, and running the command with elevated permission.&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 mv&lt;/span&gt; /tmp/devops_portfolio/&lt;span class="k"&gt;*&lt;/span&gt; /var/www/html/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A moment later, visiting the server's public address in a browser showed my actual portfolio, live, served from a machine I had built entirely from a terminal.&lt;/p&gt;
&lt;h2&gt;
  
  
  Turning the Whole Process Into a Script
&lt;/h2&gt;

&lt;p&gt;Doing this manually taught me the mechanics, but repeating it manually every time would not scale, and would not reflect how this is actually done professionally. So I converted the entire workflow into a bash script, with each stage wrapped in its own function, resource group creation, network creation, server creation, and a reusable function for opening ports safely, one that checks whether a rule already exists before attempting to create it again.&lt;/p&gt;

&lt;p&gt;Running the whole provisioning process now takes one command and finishes in a matter of seconds, something that used to take a series of manual steps and careful attention to avoid the exact mistakes I described above.&lt;/p&gt;
&lt;h2&gt;
  
  
  What This Project Actually Demonstrates
&lt;/h2&gt;

&lt;p&gt;If you are a recruiter or a hiring manager reading this, here is what I want you to take away. This was not a copy and paste exercise. Every command here was typed, tested, and in more than one case, broken and then fixed by understanding the actual cause rather than guessing. I hit a real Azure networking conflict and resolved it by reading the error and understanding how security rule priorities work. I hit a real Linux permissions issue and resolved it the same way.&lt;/p&gt;

&lt;p&gt;I also wrote the entire process up as a runbook, the kind of documentation a real engineering team would expect, so that anyone on a team could follow these exact steps and get the same result, safely and predictably.&lt;/p&gt;

&lt;p&gt;The live result of this project is a working website, served entirely from infrastructure I provisioned, secured and configured myself, from the terminal, from the ground up.&lt;/p&gt;

&lt;p&gt;If you would like to see the full runbook, the automation script, or talk through any part of how this was built, I would genuinely welcome the conversation. This is exactly the kind of work I want to keep doing.&lt;/p&gt;

&lt;p&gt;Below is the link to my github repo&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/highpee1991" rel="noopener noreferrer"&gt;
        highpee1991
      &lt;/a&gt; / &lt;a href="https://github.com/highpee1991/azure-cli-lab" rel="noopener noreferrer"&gt;
        azure-cli-lab
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      My Azure CLI learning journey from beginner to pro
    &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;Day 01 — Azure CLI Basics&lt;/h1&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What I Learned&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Azure CLI is a tool that lets you manage Azure resources from the terminal...&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Commands I Ran&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Check version&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 --version&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Output:
azure-cli                         2.87.0&lt;/p&gt;
&lt;p&gt;core                              2.87.0
telemetry                          1.1.0&lt;/p&gt;
&lt;p&gt;Dependencies:
msal                              1.36.0
azure-mgmt-resource               24.0.0&lt;/p&gt;
&lt;p&gt;Python location 'C:\Program Files\Microsoft SDKs\Azure\CLI2\python.exe'
Config directory 'C:\Users\Relitorin Orders.azure'
Extensions directory 'C:\Users\Relitorin Orders.azure\cliextensions'&lt;/p&gt;
&lt;p&gt;Python (Windows) 3.13.13 (tags/v3.13.13:01104ce, Apr  7 2026, 19:25:48) [MSC v.1944 64 bit (AMD64)]&lt;/p&gt;
&lt;p&gt;Legal docs and information: aka.ms/AzureCliLegal&lt;/p&gt;
&lt;p&gt;Your CLI is up-to-date.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Login 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
az account show&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Output:
Retrieving tenants and subscriptions for the selection...&lt;/p&gt;
&lt;p&gt;[Tenant and subscription selection]&lt;/p&gt;
&lt;p&gt;No     Subscription name     Subscription ID                       Tenant&lt;/p&gt;

&lt;p&gt;[1] *  Azure subscription 1    Default Directory&lt;/p&gt;
&lt;p&gt;The default is marked with an *; the default tenant is 'Default Directory' and subscription is 'Azure subsc
ription 1' ().&lt;/p&gt;
&lt;p&gt;Select a subscription and tenant (Type a number or Enter for no changes):&lt;/p&gt;
&lt;p&gt;Tenant: Default Directory
Subscription: Azure subscription…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/highpee1991/azure-cli-lab" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Multi-Region VNet Peering + Global Load Balancer on Azure, A Step-by-Step Guide</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Mon, 29 Jun 2026 19:12:04 +0000</pubDate>
      <link>https://dev.to/highpee1991/multi-region-vnet-peering-global-load-balancer-on-azure-a-step-by-step-guide-45ap</link>
      <guid>https://dev.to/highpee1991/multi-region-vnet-peering-global-load-balancer-on-azure-a-step-by-step-guide-45ap</guid>
      <description>&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;A Nigerian fintech has two offices, &lt;strong&gt;Lagos&lt;/strong&gt; and &lt;strong&gt;Abuja&lt;/strong&gt; and wants:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Both offices on the &lt;strong&gt;same private network&lt;/strong&gt;, even though they sit in different parts of the world&lt;/li&gt;
&lt;li&gt;A web app running on a server in each office&lt;/li&gt;
&lt;li&gt;Traffic automatically routed to whichever office is closest to the user, with &lt;strong&gt;automatic failover&lt;/strong&gt; if one office's server goes down&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To simulate this on Azure, Lagos lives in &lt;strong&gt;East US&lt;/strong&gt; and Abuja lives in &lt;strong&gt;South Africa North&lt;/strong&gt; two genuinely different Azure regions, connected privately, with a global load balancer deciding where to send traffic.&lt;/p&gt;

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

&lt;p&gt;By the end of this guide, you'll have a working multi-region setup you can test from your own terminal.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you'll need
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;An Azure account (the free tier works fine)&lt;/li&gt;
&lt;li&gt;Basic comfort with the Azure Portal&lt;/li&gt;
&lt;li&gt;A terminal that can run &lt;code&gt;ssh&lt;/code&gt; and &lt;code&gt;curl&lt;/code&gt; (Git Bash, macOS/Linux terminal, or Cloud Shell)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Create a Resource Group
&lt;/h2&gt;

&lt;p&gt;A resource group is just a logical folder that holds everything we build, so it's easy to manage or delete later as one unit.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Azure Portal, search &lt;strong&gt;"Resource groups"&lt;/strong&gt; → &lt;strong&gt;+ Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource group name:&lt;/strong&gt; &lt;code&gt;fintech-rg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region:&lt;/strong&gt; pick any region for the resource group itself, it doesn't have to match the resources inside it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review + Create&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

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




&lt;h2&gt;
  
  
  Step 2: Create Two Virtual Networks (VNets)
&lt;/h2&gt;

&lt;p&gt;A VNet is a private network inside Azure. We need one for each branch office, in two different regions.&lt;/p&gt;

&lt;h3&gt;
  
  
  VNet for Lagos (East US)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Search &lt;strong&gt;"Virtual networks"&lt;/strong&gt; → &lt;strong&gt;+ Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Resource group: &lt;code&gt;fintech-rg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;lagos-vnet&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Region: &lt;strong&gt;East US&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Leave the default address space and subnet, or customize if you prefer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review + Create&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  VNet for Abuja (South Africa North)
&lt;/h3&gt;

&lt;p&gt;Repeat the same steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Name: &lt;code&gt;abuja-vnet&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Region: &lt;strong&gt;South Africa North&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review + Create&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

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




&lt;h2&gt;
  
  
  Step 3: Peer the Two VNets Together
&lt;/h2&gt;

&lt;p&gt;VNet Peering connects the two networks so resources inside them can talk to each other using &lt;strong&gt;private IP addresses&lt;/strong&gt;, as if they were on the same local network, no public internet involved.&lt;/p&gt;

&lt;p&gt;Peering needs to be set up from &lt;strong&gt;both sides&lt;/strong&gt; once from Lagos's VNet pointing to Abuja, and once from Abuja's VNet pointing to Lagos.&lt;/p&gt;

&lt;h3&gt;
  
  
  From the Lagos VNet
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;lagos-vnet&lt;/code&gt; → left menu → &lt;strong&gt;Peerings&lt;/strong&gt; → &lt;strong&gt;+ Add&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This virtual network:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Peering link name: &lt;code&gt;lagos-to-abuja&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Allow traffic to remote virtual network: ✅ enabled&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote virtual network:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Peering link name: &lt;code&gt;abuja-to-lagos&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Virtual network: select &lt;code&gt;abuja-vnet&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Allow traffic to remote virtual network: ✅ enabled&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This single step actually creates both sides of the peering at once.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Confirm the Peering Is Connected
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go back to &lt;code&gt;lagos-vnet&lt;/code&gt; → &lt;strong&gt;Peerings&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You should see the peering listed with status: &lt;strong&gt;Connected&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Check &lt;code&gt;abuja-vnet&lt;/code&gt; → &lt;strong&gt;Peerings&lt;/strong&gt; too — it should show &lt;strong&gt;Connected&lt;/strong&gt; from its side as well&lt;/li&gt;
&lt;/ol&gt;

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




&lt;h2&gt;
  
  
  Step 4: Create a VM in Each VNet
&lt;/h2&gt;

&lt;h3&gt;
  
  
  VM for Lagos
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Search &lt;strong&gt;"Virtual machines"&lt;/strong&gt; → &lt;strong&gt;+ Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Resource group: &lt;code&gt;fintech-rg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;VM name: &lt;code&gt;lagos-vm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Region: &lt;strong&gt;East US&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Image: &lt;strong&gt;Ubuntu Server&lt;/strong&gt; (any recent LTS version)&lt;/li&gt;
&lt;li&gt;Size: the default/smallest size is fine for this project&lt;/li&gt;
&lt;li&gt;Authentication: &lt;strong&gt;SSH public key&lt;/strong&gt; (recommended) or password&lt;/li&gt;
&lt;li&gt;Username: &lt;code&gt;lagosvm&lt;/code&gt; &lt;em&gt;(or whatever username you prefer, just stay consistent for the rest of the guide)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking tab:&lt;/strong&gt; Virtual network → select &lt;code&gt;lagos-vnet&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review + Create&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  VM for Abuja
&lt;/h3&gt;

&lt;p&gt;Repeat with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;VM name: &lt;code&gt;abuja-vm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Region: &lt;strong&gt;South Africa North&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Username: &lt;code&gt;abujavm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Virtual network: &lt;code&gt;abuja-vnet&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Once both VMs are created, &lt;strong&gt;note down&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each VM's &lt;strong&gt;public IP&lt;/strong&gt; (used to SSH in)&lt;/li&gt;
&lt;li&gt;Each VM's &lt;strong&gt;private IP&lt;/strong&gt; (used for the ping test later)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this guide, the private IPs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lagos VM private IP: &lt;code&gt;10.0.0.4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Abuja VM private IP: &lt;code&gt;10.1.0.4&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Yours may be different — always use the actual IP shown in your VM's &lt;strong&gt;Overview&lt;/strong&gt; or &lt;strong&gt;Networking&lt;/strong&gt; tab.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Secure SSH Access and Connect to Each VM
&lt;/h2&gt;

&lt;p&gt;If you're using an SSH key pair, make sure your private key file has the correct permissions before connecting, otherwise SSH will refuse to use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;700 lagosvm_key.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Do the same for the Abuja key if you're using a separate one.)&lt;/p&gt;

&lt;p&gt;Connect to the Lagos VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; lagosvm_key.pem lagosvm@&amp;lt;lagos-vm-public-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect to the Abuja VM in a separate terminal tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; abujavm_key.pem abujavm@&amp;lt;abuja-vm-public-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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




&lt;h2&gt;
  
  
  Step 6: Install and Start a Web Server on Each VM
&lt;/h2&gt;

&lt;p&gt;Run this on &lt;strong&gt;both&lt;/strong&gt; VMs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;apache2 &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start apache2
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apt update &amp;amp;&amp;amp; upgrade&lt;/code&gt; refreshes the package list and applies any pending updates&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apache2&lt;/code&gt; is the web server that will respond to HTTP requests&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enable&lt;/code&gt; makes sure Apache automatically restarts if the VM ever reboots&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Confirm Apache is running on each VM:&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 status apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;active (running)&lt;/code&gt; in green.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 7: Test the Private Network Connection (Ping Test)
&lt;/h2&gt;

&lt;p&gt;This is the moment that proves the VNet peering actually works — two VMs, in two different regions, reaching each other over a private connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From the Lagos VM&lt;/strong&gt;, ping Abuja's private IP:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;From the Abuja VM&lt;/strong&gt;, ping Lagos's private IP:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If peering is working correctly, you'll see successful replies on both sides, something like:&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;64 bytes from 10.1.0.4: icmp_seq=1 ttl=64 time=142 ms
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;Ctrl + C&lt;/code&gt; to stop the ping.&lt;/p&gt;

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

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

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Note:&lt;/strong&gt; If you don't get replies, double check that each VM's &lt;strong&gt;Network Security Group (NSG)&lt;/strong&gt; allows inbound traffic from the peered VNet (the default &lt;code&gt;AllowVnetInBound&lt;/code&gt; rule usually covers this automatically).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 8: Customize Each Web Page So You Can Tell Them Apart
&lt;/h2&gt;

&lt;p&gt;To visually confirm which server is responding later (especially once the load balancer is involved), give each VM a distinct homepage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the Lagos VM:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;Hello from Lagos Server&amp;lt;/h1&amp;gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /var/www/html/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On the Abuja VM:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;Hello from Abuja Server&amp;lt;/h1&amp;gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /var/www/html/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it directly using each VM's &lt;strong&gt;public IP&lt;/strong&gt; in a browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;lagos-vm-public-ip&amp;gt;
http://&amp;lt;abuja-vm-public-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the matching "Hello from..." message for each one.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 9: Open Port 80 on Both VMs
&lt;/h2&gt;

&lt;p&gt;Apache runs on port 80, so each VM's NSG needs to allow inbound HTTP traffic — otherwise external requests (including from the load balancer) won't be able to reach it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open each VM → &lt;strong&gt;Networking&lt;/strong&gt; tab → &lt;strong&gt;Add inbound port rule&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Settings:

&lt;ul&gt;
&lt;li&gt;Destination port: &lt;code&gt;80&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Protocol: &lt;code&gt;TCP&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Action: &lt;code&gt;Allow&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Give the rule a name (anything descriptive works, e.g. &lt;code&gt;Allow-HTTP&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Repeat for the other VM&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 10: Create the Regional Load Balancer for Lagos
&lt;/h2&gt;

&lt;p&gt;Since Lagos and Abuja are in different regions, we can't use one single load balancer to cover both — a standard Azure Load Balancer only operates within a single region. We need &lt;strong&gt;one regional load balancer per region&lt;/strong&gt;, and later, a &lt;strong&gt;Global Load Balancer&lt;/strong&gt; sitting on top of both.&lt;/p&gt;

&lt;h3&gt;
  
  
  10.1 — Create the Public IP
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Search &lt;strong&gt;"Public IP addresses"&lt;/strong&gt; → &lt;strong&gt;+ Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name (e.g. &lt;code&gt;lagos-lb-ip&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;SKU: &lt;strong&gt;Standard&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Tier: &lt;strong&gt;Regional&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Region: &lt;strong&gt;East US&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Assignment: &lt;strong&gt;Static&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review + Create&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  10.2 — Create the Load Balancer
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Search &lt;strong&gt;"Load balancers"&lt;/strong&gt; → &lt;strong&gt;+ Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name (e.g. &lt;code&gt;lagos-lb&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Region: &lt;strong&gt;East US&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;SKU: &lt;strong&gt;Standard&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Type: &lt;strong&gt;Public&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Tier: &lt;strong&gt;Regional&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend IP configuration:&lt;/strong&gt; add a frontend IP, give it a name, and select the public IP you just created (&lt;code&gt;lagos-lb-ip&lt;/code&gt;) — this becomes your regional frontend IP, in this guide &lt;code&gt;20.121.242.74&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Skip backend pools and rules for now — we'll add them after creation for a cleaner setup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review + Create&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

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

&lt;h3&gt;
  
  
  10.3 — Add the Backend Pool
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open your Lagos load balancer → &lt;strong&gt;Backend pools&lt;/strong&gt; → &lt;strong&gt;+ Add&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name&lt;/li&gt;
&lt;li&gt;Virtual network: select &lt;code&gt;lagos-vnet&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Backend pool configuration: &lt;strong&gt;NIC&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ Add&lt;/strong&gt; under the IP configurations table → select &lt;code&gt;lagos-vm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h3&gt;
  
  
  10.4 — Add a Health Probe
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Health probes&lt;/strong&gt; → &lt;strong&gt;+ Add&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name&lt;/li&gt;
&lt;li&gt;Protocol: &lt;strong&gt;HTTP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Port: &lt;code&gt;80&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Path: &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Interval: 5–15 seconds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  10.5 — Add the Load Balancing Rule
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Load balancing rules&lt;/strong&gt; → &lt;strong&gt;+ Add&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name&lt;/li&gt;
&lt;li&gt;Frontend IP: select the frontend IP you created&lt;/li&gt;
&lt;li&gt;Backend pool: select the backend pool you created&lt;/li&gt;
&lt;li&gt;Health probe: select the probe you created&lt;/li&gt;
&lt;li&gt;Protocol: &lt;strong&gt;TCP&lt;/strong&gt;, Port: &lt;code&gt;80&lt;/code&gt;, Backend port: &lt;code&gt;80&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  10.6 — Test the Lagos Load Balancer
&lt;/h3&gt;

&lt;p&gt;From your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://20.121.242.74
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(replace with your actual Lagos load balancer frontend IP)&lt;/p&gt;

&lt;p&gt;You should see the "Hello from Lagos Server" response.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 11: Create the Regional Load Balancer for Abuja
&lt;/h2&gt;

&lt;p&gt;Repeat &lt;strong&gt;all of Step 10&lt;/strong&gt;, but for the Abuja region:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public IP: give it a name, Region: &lt;strong&gt;South Africa North&lt;/strong&gt; → in this guide, the resulting frontend IP is &lt;code&gt;20.87.39.70&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Load Balancer: give it a name, Region: &lt;strong&gt;South Africa North&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Backend pool: Virtual network → &lt;code&gt;abuja-vnet&lt;/code&gt;, add &lt;code&gt;abuja-vm&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Health probe and rule: same settings as before (HTTP/80/&lt;code&gt;/&lt;/code&gt;, TCP/80→80)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test it:&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;curl http://20.87.39.70
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see "Hello from Abuja Server."&lt;/p&gt;

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

&lt;p&gt;⚠️ &lt;strong&gt;Important:&lt;/strong&gt; Don't move to the next step until both regional load balancers pass this test independently. The Global Load Balancer depends on both of these already working correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 12: Create the Global Load Balancer
&lt;/h2&gt;

&lt;p&gt;The Global Load Balancer sits above both regional load balancers and decides which region to send each user's request to, based on which one is geographically closest &lt;strong&gt;and&lt;/strong&gt; healthy.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  12.1 — Create the Global Public IP
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Search &lt;strong&gt;"Public IP addresses"&lt;/strong&gt; → &lt;strong&gt;+ Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name (e.g. &lt;code&gt;global-lb-ip&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;SKU: &lt;strong&gt;Standard&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Tier: &lt;strong&gt;Global&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Region: must be one of Azure's supported &lt;strong&gt;home regions&lt;/strong&gt; for global resources, examples include &lt;code&gt;East US 2&lt;/code&gt;, &lt;code&gt;West Europe&lt;/code&gt;, &lt;code&gt;Southeast Asia&lt;/code&gt;, &lt;code&gt;West US&lt;/code&gt;, &lt;code&gt;North Europe&lt;/code&gt;, &lt;code&gt;Central US&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Assignment: &lt;strong&gt;Static&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review + Create&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this guide, the resulting global IP is &lt;code&gt;20.15.1.61&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  12.2 — Create the Global Load Balancer
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Search &lt;strong&gt;"Load balancers"&lt;/strong&gt; → &lt;strong&gt;+ Create&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name (e.g. &lt;code&gt;global-lb&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Region: the same home region you used for the global IP (e.g. &lt;code&gt;East US 2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;SKU: &lt;strong&gt;Standard&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Tier: &lt;strong&gt;Global&lt;/strong&gt; ⚠️ this is the setting that matters most here — don't select Regional&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend IP configuration:&lt;/strong&gt; add a frontend IP, give it a name, and select your &lt;code&gt;global-lb-ip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Skip backend pools and rules in the wizard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review + Create&lt;/strong&gt; → &lt;strong&gt;Create&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  12.3 — Add the Backend Pool
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open your Global load balancer → &lt;strong&gt;Backend pools&lt;/strong&gt; → &lt;strong&gt;+ Add&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name&lt;/li&gt;
&lt;li&gt;Under the configuration table, click &lt;strong&gt;+ Add&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Type: &lt;strong&gt;Load Balancer&lt;/strong&gt; (not NIC, not IP address)&lt;/li&gt;
&lt;li&gt;Add your &lt;strong&gt;Lagos&lt;/strong&gt; load balancer&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ Add&lt;/strong&gt; again and add your &lt;strong&gt;Abuja&lt;/strong&gt; load balancer&lt;/li&gt;
&lt;li&gt;Confirm both regional load balancers now appear in the table&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Save&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h3&gt;
  
  
  12.4 — Add the Load Balancing Rule
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Load balancing rules&lt;/strong&gt; → &lt;strong&gt;+ Add&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name&lt;/li&gt;
&lt;li&gt;Frontend IP: your global frontend IP&lt;/li&gt;
&lt;li&gt;Backend pool: your global backend pool&lt;/li&gt;
&lt;li&gt;Protocol: &lt;strong&gt;TCP&lt;/strong&gt;, Port: &lt;code&gt;80&lt;/code&gt;, &lt;strong&gt;Backend port: &lt;code&gt;80&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The backend port here must match the frontend port used in both regional load balancer rules — in this case, both use port 80, so we're consistent.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Add&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 13: Test the Global Load Balancer
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://20.15.1.61
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(replace with your actual global load balancer IP)&lt;/p&gt;

&lt;p&gt;You should get back a response from whichever region is closest to you, for example, "Hello from Lagos Server."&lt;/p&gt;

&lt;p&gt;This is expected behavior: the Global Load Balancer uses &lt;strong&gt;geo-proximity routing&lt;/strong&gt;, not round robin. It always routes you to the closest healthy region, not a random alternating split.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 14: Test Automatic Failover
&lt;/h2&gt;

&lt;p&gt;To prove the system can actually fail over between regions, we'll simulate one region going down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH into the Lagos VM&lt;/strong&gt; and stop Apache:&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 stop apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now curl the global load balancer again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://20.15.1.61
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, the response should switch to &lt;strong&gt;"Hello from Abuja Server"&lt;/strong&gt; Azure detected that the Lagos region's health probe failed, removed it from rotation, and automatically routed traffic to the healthy Abuja region instead.&lt;/p&gt;

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

&lt;p&gt;Once you're done testing, restart Apache on the Lagos VM so it's back in rotation:&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 start apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Recap: What We Actually Built
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
  ↓
Global Load Balancer (home region, e.g. East US 2)
  ↓                              ↓
Regional LB - Lagos          Regional LB - Abuja
(East US)                    (South Africa North)
  ↓                              ↓
lagos-vm                     abuja-vm
  ↳ peered privately with abuja-vm via VNet Peering
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup demonstrates three real-world cloud concepts at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Private cross-region connectivity&lt;/strong&gt; (VNet Peering)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regional high availability&lt;/strong&gt; (each region's own load balancer and health probe)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global resilience and failover&lt;/strong&gt; (the Global Load Balancer routing around an unhealthy region automatically)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If you build this yourself, try the failover test a few times — watching traffic automatically reroute in real time is genuinely satisfying, and it's the same underlying pattern real fintechs use to stay online across regions.&lt;/p&gt;

&lt;p&gt;Got stuck somewhere or have a question about a specific step? Drop a comment below 👇&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Day 2 — I Stopped Being a Student and Started Acting Like a DevOps Engineer</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Sun, 28 Jun 2026 08:32:59 +0000</pubDate>
      <link>https://dev.to/highpee1991/day-2-i-stopped-being-a-student-and-started-acting-like-a-devops-engineer-4kid</link>
      <guid>https://dev.to/highpee1991/day-2-i-stopped-being-a-student-and-started-acting-like-a-devops-engineer-4kid</guid>
      <description>&lt;p&gt;This is Day 2 of my 90-day cloud engineering challenge. Day 1 was about installing tools and getting familiar with the Azure CLI. Day 2 was different — instead of just running commands to learn syntax, I gave myself a real-world scenario and worked through it the way an actual DevOps engineer would on the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scenario
&lt;/h2&gt;

&lt;p&gt;To push myself past "tutorial mode," I simulated a message from a team lead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Taiwo, we need a new environment for the payments project. Create a resource group for dev, one for staging. Tag them properly. Create a storage account in dev. List everything and send me a clean output. Then document it."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No step-by-step instructions. No exact commands handed to me — just a goal. That's closer to how real work actually shows up: as an outcome someone needs, not a tutorial to follow. My job was to translate that into the right CLI commands, tag things properly, and produce a clean, professional result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Commands I Used
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Listing and inspecting resource groups
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group list
az group list &lt;span class="nt"&gt;--output&lt;/span&gt; table
az group list &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
az group list &lt;span class="nt"&gt;--output&lt;/span&gt; jsonc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I started by getting comfortable with different output formats. In a real environment with dozens or hundreds of resources, raw JSON is hard to scan quickly — &lt;code&gt;table&lt;/code&gt; and &lt;code&gt;tsv&lt;/code&gt; make it much easier to read at a glance or pipe into other tools/scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering with &lt;code&gt;--query&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group list &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[].{Name:name, Location:location}"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
az group list &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[?location=='eastus'].name"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table
az account show &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where things started feeling like real engineering rather than just running commands. &lt;code&gt;--query&lt;/code&gt; lets you filter and reshape the JSON output before it even hits your screen — instead of scrolling through everything, you ask for exactly what you need. When you're dealing with 200+ resources, this isn't optional; it's how you stay sane.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tagging resources properly
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; taiwo-devops-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Dev &lt;span class="nv"&gt;Owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Taiwo &lt;span class="nv"&gt;Project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AzureCLILab

az group show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; taiwo-devops-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"tags"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tags seem like a small detail until you realize how much they matter in a real company. Tags answer three questions instantly, for any resource, at any scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Who owns it?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What project does it belong to?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What environment is it — dev, staging, or production?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without consistent tagging, a subscription with hundreds of resources becomes nearly impossible to manage, audit, or clean up safely.&lt;/p&gt;

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

&lt;p&gt;Rather than running each command by hand every time I needed an environment, I wrote a Bash script to provision the entire setup — dev resource group, staging resource group, and a storage account — 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="c"&gt;# ============================================&lt;/span&gt;
&lt;span class="c"&gt;# Azure Environment Provisioner&lt;/span&gt;
&lt;span class="c"&gt;# Author: Taiwo&lt;/span&gt;
&lt;span class="c"&gt;# Description: Provisions dev environment&lt;/span&gt;
&lt;span class="c"&gt;#              for any project automatically&lt;/span&gt;
&lt;span class="c"&gt;# ============================================&lt;/span&gt;

&lt;span class="c"&gt;# Variables — change these for any project&lt;/span&gt;
&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"payments"&lt;/span&gt;
&lt;span class="nv"&gt;OWNER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Taiwo"&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;
&lt;span class="nv"&gt;STORAGE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"taiwo&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;dev001"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🚀 Starting provisioning for project: &lt;/span&gt;&lt;span class="nv"&gt;$PROJECT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Step 1: Create Dev Resource Group&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"📁 Creating Dev Resource Group..."&lt;/span&gt;
az group create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-dev-rg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Dev &lt;span class="nv"&gt;Owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$OWNER&lt;/span&gt; &lt;span class="nv"&gt;Project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT&lt;/span&gt;

&lt;span class="c"&gt;# Step 2: Create Staging Resource Group&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"📁 Creating Staging Resource Group..."&lt;/span&gt;
az group create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-staging-rg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Staging &lt;span class="nv"&gt;Owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$OWNER&lt;/span&gt; &lt;span class="nv"&gt;Project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT&lt;/span&gt;

&lt;span class="c"&gt;# Step 3: Create Storage Account&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"💾 Creating Storage Account..."&lt;/span&gt;
az storage account create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STORAGE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-dev-rg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOCATION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sku&lt;/span&gt; Standard_LRS &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kind&lt;/span&gt; StorageV2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Dev &lt;span class="nv"&gt;Owner&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$OWNER&lt;/span&gt; &lt;span class="nv"&gt;Project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PROJECT&lt;/span&gt;

&lt;span class="c"&gt;# Step 4: Confirm everything&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✅ Provisioning complete. Here is your environment:"&lt;/span&gt;
az group list &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"🎉 Done! Project &lt;/span&gt;&lt;span class="nv"&gt;$PROJECT&lt;/span&gt;&lt;span class="s2"&gt; environment is ready."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script takes a few simple variables — project name, owner, and location — and provisions a complete environment from scratch, consistently, every single time. Change the &lt;code&gt;PROJECT&lt;/code&gt; variable and the same script provisions an environment for a completely different project. That's the difference between "I ran some commands" and "I built something reusable."&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: A Git Workflow Script
&lt;/h2&gt;

&lt;p&gt;Outside of the Azure work itself, I also wrote a small Bash script to speed up my Git workflow — staging, committing, and pushing in a single command instead of typing three separate ones every time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Exit if any command fails&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'echo "❌ A command failed. Exiting..."'&lt;/span&gt; ERR

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Current branch: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git branch &lt;span class="nt"&gt;--show-current&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Stage all changes&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Prompt for commit message&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &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 a commit message: "&lt;/span&gt; commitMessage
    &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;$commitMessage&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;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Commit message cannot be empty."&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Commit and push&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$commitMessage&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
git push

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✅ Changes committed and pushed successfully."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small touches here matter: &lt;code&gt;set -e&lt;/code&gt; and the &lt;code&gt;trap&lt;/code&gt; ensure the script stops cleanly the moment something fails instead of plowing ahead, and the commit-message prompt won't let me accidentally push with an empty message. It's a small script, but it's the kind of "make my own workflow faster" thinking that DevOps is built on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Taught Me About Real DevOps Work
&lt;/h2&gt;

&lt;p&gt;Today reframed how I think about the CLI. It's not just a faster way to click buttons — it's a tool for &lt;strong&gt;repeatability, filtering, and automation&lt;/strong&gt; at a scale where doing things manually simply breaks down.&lt;/p&gt;

&lt;p&gt;In a real company, this is what the day-to-day actually looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spinning up a storage account for a dev team on request&lt;/li&gt;
&lt;li&gt;Checking what resources are currently running in production&lt;/li&gt;
&lt;li&gt;Cleaning up a test environment left over from last sprint&lt;/li&gt;
&lt;li&gt;Standing up a new resource group for a new project&lt;/li&gt;
&lt;li&gt;Filtering out the handful of failed resources from a list of 200+&lt;/li&gt;
&lt;li&gt;Wrapping repeatable tasks into scripts instead of typing them out every time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that works well through a portal UI when you're dealing with real scale. It works through commands, queries, tags, and scripts — exactly what I practiced today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problems I Faced
&lt;/h2&gt;

&lt;p&gt;The biggest challenge wasn't syntax — it was genuinely understanding &lt;em&gt;why&lt;/em&gt; each piece mattered (tagging, querying, scripting) rather than just copying commands. Once I slowed down and researched the reasoning behind each one, things started clicking. I'd rather understand the "why" once than memorize the "what" repeatedly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Clicked Today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Provisioned a full dev + staging environment from a single scenario, not a tutorial&lt;/li&gt;
&lt;li&gt;Created a Storage Account via CLI inside the correct resource group&lt;/li&gt;
&lt;li&gt;Used &lt;code&gt;--query&lt;/code&gt; to filter and reshape output like an engineer working at scale&lt;/li&gt;
&lt;li&gt;Wrote a reusable Bash automation script for environment provisioning&lt;/li&gt;
&lt;li&gt;Wrote a separate Git workflow script to speed up my own commit/push process&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Day 1 was about learning the tool. Day 2 was about using it to solve a problem someone else gave me. That shift — from "following steps" to "interpreting a request and deciding the steps myself" — is the part I want to keep practicing for the rest of these 90 days.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Following along with a 90-day hands-on cloud engineering challenge. Catch up on &lt;a href="https://dev.to/highpee1991/day-01-azure-cli-basics-2fh4"&gt;Day 1&lt;/a&gt; here.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Azure #DevOps #CloudEngineering #100DaysOfCloud #LearningInPublic #BashScripting
&lt;/h1&gt;

</description>
      <category>azure</category>
      <category>career</category>
      <category>devjournal</category>
      <category>devops</category>
    </item>
    <item>
      <title>Day 01 — Azure CLI Basics</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Tue, 23 Jun 2026 16:07:37 +0000</pubDate>
      <link>https://dev.to/highpee1991/day-01-azure-cli-basics-2fh4</link>
      <guid>https://dev.to/highpee1991/day-01-azure-cli-basics-2fh4</guid>
      <description>&lt;p&gt;Welcome to Day 1 of my 90-day cloud engineering challenge! I'm documenting everything I learn as I go — partly to hold myself accountable, and partly because writing things down helps them actually stick.&lt;/p&gt;

&lt;p&gt;Today's focus: getting comfortable with the Azure CLI and creating my first resource entirely from the terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I Learned&lt;/strong&gt;&lt;br&gt;
The Azure CLI is a command-line tool that lets you manage Azure resources directly from your terminal, without needing to click through the Azure Portal. Once it's installed and you're logged in, you can create, update, and delete resources with simple commands — which is faster and more repeatable than navigating a UI.&lt;/p&gt;

&lt;p&gt;Commands I Ran&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install CLI: For me i am on window so i am using below command
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;winget&lt;/span&gt; &lt;span class="kd"&gt;install&lt;/span&gt; &lt;span class="na"&gt;--id &lt;/span&gt;&lt;span class="kd"&gt;Microsoft&lt;/span&gt;.AzureCLI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Check the installed version
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;az --version
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This confirmed the CLI was installed correctly and showed me the version, dependencies, and Python environment it's running on. Good first sanity check before doing anything else.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to Azure
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;az login
az account show
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This opens a browser-based login flow and connects the CLI to my Azure account. Once logged in, &lt;em&gt;&lt;/em&gt;&lt;/p&gt;&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;&lt;em&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="az" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;az&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/em&gt;&lt;/div&gt;&lt;em&gt;
&lt;/em&gt; confirms which subscription and tenant is currently active — useful for double-checking you're working in the right environment before creating anything.&lt;p&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create my first Resource Group
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;az group create --name taiwo-devops-rg --location eastus
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/subscriptions/&amp;lt;subscription-id&amp;gt;/resourceGroups/taiwo-devops-rg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eastus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"managedBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"taiwo-devops-rg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"provisioningState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Succeeded"&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;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Resources/resourceGroups"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Resource Group is basically a container that holds related Azure resources — useful for organizing and managing things together (and for cleaning everything up at once later by just deleting the group).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What This Taught Me&lt;/strong&gt;&lt;br&gt;
This exercise taught me how to create a resource group and control exactly where it lives (in this case, eastus). More importantly, it showed me how much faster the CLI can be compared to the Azure Portal — no waiting on pages to load, no risk of losing your place if your internet connection hiccups. Just a clean command and a clean output. I can already see why CLI/scripting skills matter so much in DevOps and cloud engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problems I Faced&lt;/strong&gt;&lt;br&gt;
Honestly, the biggest challenge today was just remembering the command syntax — this is my first real session using the Azure CLI, so nothing is muscle memory yet. But I expect that to improve quickly with repetition. The more I run these commands, the more naturally they'll come.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Matters in DevOps&lt;/strong&gt;&lt;br&gt;
This might look like a small win — installing a CLI and creating one resource group — but it ties directly into how real DevOps workflows operate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automation over manual clicks&lt;/strong&gt;: DevOps is built on repeatable, scriptable processes. A command like &lt;code&gt;az group create&lt;/code&gt; can be dropped into a CI/CD pipeline, a shell script, or an Infrastructure-as-Code workflow — something you simply can't do with portal clicks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency&lt;/strong&gt;: Running the same command always produces the same result. That predictability is exactly what you want when provisioning environments for dev, staging, and production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foundation for IaC&lt;/strong&gt;: Tools like Terraform, Bicep, and ARM templates all build on the same underlying concepts I practiced today — resource groups, regions, and subscriptions. Getting comfortable with the CLI now makes those tools click faster later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster feedback loops&lt;/strong&gt;: Typing a command and getting an immediate JSON response is a much quicker feedback loop than navigating multiple portal pages, which matters when you're iterating quickly or troubleshooting under time pressure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: today wasn't just about "I made a resource group." It was about building the muscle memory and mindset that DevOps engineers rely on every day — treating infrastructure as something you script and version, not something you click through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's Next&lt;/strong&gt;&lt;br&gt;
This is Day 1 of 90. Tomorrow I'll be building on this foundation — likely exploring more az commands and starting to provision some actual resources inside this resource group.&lt;/p&gt;

&lt;p&gt;Follow along with the rest of the series as I document 90 days of hands-on cloud engineering learning.&lt;/p&gt;

&lt;p&gt;_#Azure #CloudEngineering #DevOps #100DaysOfCloud #LearningInPublic&lt;/p&gt;

</description>
      <category>azure</category>
      <category>beginners</category>
      <category>cli</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Building a Multi-Region Load Balancer on Azure for a Nigerian Fintech (Lagos ↔ Abuja) — A Real Troubleshooting Story</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Wed, 17 Jun 2026 13:13:35 +0000</pubDate>
      <link>https://dev.to/highpee1991/building-a-multi-region-load-balancer-on-azure-for-a-nigerian-fintech-lagos-abuja-a-real-dli</link>
      <guid>https://dev.to/highpee1991/building-a-multi-region-load-balancer-on-azure-for-a-nigerian-fintech-lagos-abuja-a-real-dli</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Scenario&lt;/strong&gt;&lt;br&gt;
Imagine a Nigerian fintech — think Flutterwave-style — with offices in &lt;strong&gt;Lagos **and **Abuja&lt;/strong&gt;. Both offices need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Sit on the same private network (even though they're in different Azure&lt;br&gt;
regions)Serve a web app from two VMs, load-balanced, so if one office's&lt;br&gt;
server goes down, traffic keeps flowing through the other&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This post walks through exactly how I built this on Azure — including every error I hit along the way, because honestly, the errors taught me more than the parts that worked first try.&lt;/p&gt;&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%2Fb4wal2rk7dzfjp8t7dnj.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%2Fb4wal2rk7dzfjp8t7dnj.png" alt=" " width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're following along, you'll need an Azure account (the free tier works) and basic comfort with SSH.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Two VNets, Two Regions, One Private Network&lt;/strong&gt;&lt;br&gt;
The first task was creating two Virtual Networks (VNets) in different Azure regions and peering them together so they could talk to each other privately — no public internet involved.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VNet 1 (Lagos) → South Africa North&lt;/li&gt;
&lt;li&gt;VNet 2 (Abuja) → East US 2&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;After creating both VNets, I set up VNet Peering between them. This is what allows resources in one VNet to reach resources in the other using private IP addresses, as if they were on the same local network — even though they're physically thousands of kilometers apart.&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%2Fmy7zeugl3jrbg16wnvge.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%2Fmy7zeugl3jrbg16wnvge.PNG" alt=" " width="800" height="191"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2: One VM Per Region, Proving They Can Talk&lt;/strong&gt;&lt;br&gt;
Next, I deployed one Linux VM in each VNet:&lt;/p&gt;

&lt;p&gt;vm-lagos in South Africa North&lt;br&gt;
vm-abuja in East US 2&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%2Fw4bcodqcclrp14onyg6j.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%2Fw4bcodqcclrp14onyg6j.PNG" alt=" " width="800" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To prove the peering actually worked, I SSH'd into each VM and pinged the other one's private IP address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ping 10.1.0.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;p&gt;Getting replies back here was the moment it clicked — two VMs in completely different parts of the world, talking over a private network, no public internet hop required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3: The Load Balancer — Where Things Got Interesting&lt;/strong&gt;&lt;br&gt;
This is the part of the project that taught me the most, because almost nothing went smoothly. I'll walk through it the way it actually happened, mistakes included, because I think that's more useful than a sanitized "perfect setup" guide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My First Plan (That Didn't Work)&lt;/strong&gt;&lt;br&gt;
My first instinct was: "just create one regular Azure Load Balancer, point it at both VMs, done."&lt;br&gt;
That seemed reasonable until I remembered a key constraint: &lt;strong&gt;a standard Azure Load Balancer only works within a single region&lt;/strong&gt;. My two VMs were in two different regions. A regional load balancer physically can't span across them.&lt;br&gt;
So I had two real options:&lt;br&gt;
&lt;strong&gt;Option 1&lt;/strong&gt;&lt;br&gt;
Fake it by routing cross-region traffic through one region's LB&lt;br&gt;
&lt;strong&gt;What it means&lt;/strong&gt;&lt;br&gt;
Works in theory, but not how Azure actually supports it in production&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2&lt;/strong&gt;&lt;br&gt;
Use Azure's &lt;strong&gt;Cross-Region (Global) Load Balancer&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;What it means&lt;/strong&gt;&lt;br&gt;
The actual purpose-built solution for exactly this scenario&lt;br&gt;
I went with the proper solution: the &lt;strong&gt;Global Load Balancer&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;You can't just create one Global LB and point it straight at two VMs. The Global LB sits on top of two regional load balancers — one per region — and routes traffic between regions, while each regional LB handles traffic within its own region.&lt;br&gt;
This means I needed three load balancers total:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Regional LB in South Africa North (for Lagos)&lt;/li&gt;
&lt;li&gt;A Regional LB in East US (for Abuja)&lt;/li&gt;
&lt;li&gt;One Global LB sitting above both&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Web Server on Both VMs&lt;/strong&gt;&lt;br&gt;
Before touching any load balancer, both VMs needed something to actually serve over HTTP. I installed apache2 on each and customized the homepage so I could visually tell which VM was responding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install apache2 -y
sudo systemctl start apache2
sudo systemctl enable apache2
echo "&amp;lt;h1&amp;gt;Lagos Server Loaded&amp;lt;/h1&amp;gt;" | sudo tee /var/www/html/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(and the equivalent "Abuja Server Loaded" on the other VM)&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;Step 2: Open Port 80&lt;/strong&gt;&lt;br&gt;
Each VM's Network Security Group (NSG) needed an inbound rule allowing TCP traffic on port 80, otherwise the web server would be running but unreachable from outside.&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%2Fbhdjsn0i1jwv5mwf1xno.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%2Fbhdjsn0i1jwv5mwf1xno.PNG" alt=" " width="799" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Build the Two Regional Load Balancers&lt;/strong&gt;&lt;br&gt;
I created one Standard SKU Public Load Balancer in each region, each with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A frontend public IP&lt;/li&gt;
&lt;li&gt;A backend pool containing that region's VM&lt;/li&gt;
&lt;li&gt;A load balancing rule on port 80&lt;/li&gt;
&lt;li&gt;A health probe checking HTTP on port 80&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;At this point, hitting either regional LB's public IP directly in a browser correctly returned that region's "Hello from..." page. Good sign — the foundation was solid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4: Where I Actually Got Stuck (And How I Fixed Each Thing)&lt;/strong&gt;&lt;br&gt;
This is the part I really want to highlight, because I think showing the actual errors is more useful than pretending everything worked first try.&lt;br&gt;
&lt;strong&gt;🔴 Error 1&lt;/strong&gt;: &lt;strong&gt;"No matching inventory for the requested VIP"&lt;/strong&gt;&lt;br&gt;
When I tried to create the Global Load Balancer for the first time, Azure threw this:&lt;br&gt;
&lt;em&gt;"No matching inventory for the requested VIP of address family: IPv4, sku: Standard and availability zone: non-zonal."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What was actually happening&lt;/strong&gt;: Azure's Cross-Region/Global Load Balancer can only be deployed in specific "Home regions" — a fixed list that includes places like East US 2, West Europe, Southeast Asia, and a handful of others. I had originally tried creating it in a region that wasn't on that list.&lt;br&gt;
&lt;strong&gt;The fix&lt;/strong&gt;: I deleted the failed Public IP and Load Balancer, and recreated both in &lt;strong&gt;East US 2&lt;/strong&gt; specifically, with the Public IP's tier set to &lt;strong&gt;Global&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🤔 Question I had to clear up: "Does everything need to move to a Home region?"&lt;/strong&gt;&lt;br&gt;
Once I learned about the Home region restriction, I panicked a little — did my Resource Group, VNets, and VMs all need to be in one of these special regions too? That would've defeated the entire point of the assignment (having Lagos and Abuja in genuinely different regions).&lt;br&gt;
Turns out: no. Only the Global Load Balancer itself and its Public IP need to be in a Home region. Everything else — the Resource Group, both VNets, both VMs, and both Regional Load Balancers — can stay exactly where they logically belong. The Home region is just where the Global LB's control plane lives; it doesn't dictate where your actual infrastructure sits.&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%2Fnmhcwyyo388z9vuq5ldi.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%2Fnmhcwyyo388z9vuq5ldi.PNG" alt=" " width="731" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔴 Error 2&lt;/strong&gt;: Global LB deployed, but curl couldn't connect at all&lt;br&gt;
After fixing the region issue, the Global LB deployed successfully. But testing it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl http:/20.15.2.121
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...just hung and timed out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What was actually happening&lt;/strong&gt;: When I went to check the regional backend pool configuration, I found the &lt;strong&gt;Virtual Network field was empty&lt;/strong&gt; — meaning the backend pool existed and was attached to a load balancing rule, but had &lt;strong&gt;zero actual VMs inside&lt;/strong&gt; it. It looked configured, but it was an empty shell.&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%2Faamgoajuwhm30btvvif6.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%2Faamgoajuwhm30btvvif6.PNG" alt=" " width="799" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix: Selecting the correct VNet from the dropdown revealed an "+ Add" button I hadn't seen before, which let me actually add the VM's NIC to the pool.&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%2F6zrbt2oruewqay7bmmmm.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%2F6zrbt2oruewqay7bmmmm.PNG" alt=" " width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Lesson learned&lt;/strong&gt;: a backend pool being "&lt;strong&gt;attached to a rule" doesn't mean it has members in it&lt;/strong&gt;. Always double check the actual &lt;strong&gt;VM/IP table&lt;/strong&gt;, not just whether the pool exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final puzzle&lt;/strong&gt;: "Why does my Global LB always show the same region?"&lt;br&gt;
Everything was technically working now — both regional LBs responded correctly, and the Global LB no longer timed out. But running curl against the Global LB's IP over and over only ever returned the Abuja server, never Lagos, no matter how many times I refreshed or switched browsers.&lt;/p&gt;

&lt;p&gt;My first thought: something's broken, it should be alternating like a normal load balancer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What was actually happening&lt;/strong&gt;: This is expected behavior, not a bug. A regional load balancer round-robins between backend VMs. A &lt;strong&gt;Global Load Balancer&lt;/strong&gt; works completely differently — it uses geo-proximity routing, meaning it looks at where the request is physically coming from and always sends it to the closest healthy region. Since I was testing from one physical location every time, it consistently picked the same region — exactly as designed.&lt;/p&gt;

&lt;p&gt;This actually makes total sense for the original use case: a Lagos-based customer should get routed to the Lagos backend, and a US-based customer should get routed to the US backend, automatically, without anyone configuring anything manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I proved both regions actually worked&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Curl each regional LB's IP directly — both returned their correct, distinct pages.&lt;/li&gt;
&lt;li&gt;Simulated a regional failure by stopping apache2 on the Abuja VM, then curled the Global LB IP again — it correctly failed over and served the Lagos page instead.&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%2Fi55rbrcqzslqejtwuyd2.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%2Fi55rbrcqzslqejtwuyd2.PNG" alt=" " width="800" height="268"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# On the Abuja VM
sudo systemctl stop apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# From my laptop
curl http:/20.15.2.121
# Now returns "Lagos Server Loaded" instead
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Don't forget to restart apache2 afterward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl start apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What I'd Tell Someone Starting This Project&lt;/strong&gt;&lt;br&gt;
If I were doing this again, here's what I'd want to know upfront:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A regional Load Balancer cannot span two regions, full stop&lt;/strong&gt;. If your VMs are in different regions, you need the Global tier sitting on top of two regional LBs underneath it — not one LB trying to do everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Global LB has region restrictions ("Home regions") that your other resources don't&lt;/strong&gt;. Don't panic and try to move your whole architecture — only the Global LB and its Public IP care about this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always verify backend pools actually have members, not just that they exist&lt;/strong&gt;. An empty backend pool with a rule attached looks deceptively "configured."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Load Balancer ≠ round robin&lt;/strong&gt;. It's geo-proximity based. If you're testing from one location and only ever see one region respond, that's correct behavior, not a failure — test failover instead to prove redundancy.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Final Result&lt;/strong&gt;&lt;br&gt;
By the end, hitting the Global Load Balancer's public IP correctly served traffic from the geographically closest healthy region, and killing nginx on one VM correctly triggered failover to the other — proving genuine multi-region high availability, the same pattern a real fintech would rely on to keep Lagos and Abuja customers online even if one region had issues.&lt;br&gt;
If you're working through something similar, I hope walking through the actual errors (not just the final clean steps) saves you some of the head-scratching it cost me.&lt;br&gt;
Questions or got stuck somewhere different? Drop a comment below 👇&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Creating a Custom RBAC Role in Azure for VM Operations</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Tue, 12 May 2026 04:01:41 +0000</pubDate>
      <link>https://dev.to/highpee1991/creating-a-custom-rbac-role-in-azure-for-vm-operations-ha4</link>
      <guid>https://dev.to/highpee1991/creating-a-custom-rbac-role-in-azure-for-vm-operations-ha4</guid>
      <description>&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;br&gt;
In cloud environments, giving users excessive permissions can introduce major security risks. One of the key principles of cloud security is the principle of least privilege, where users receive only the permissions required to perform their tasks.&lt;/p&gt;

&lt;p&gt;In this hands-on project, I implemented a custom Role-Based Access Control (RBAC) role in Microsoft Azure to allow a user manage virtual machines without granting full administrative access.&lt;/p&gt;

&lt;p&gt;The custom role allowed the user to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start virtual machines&lt;/li&gt;
&lt;li&gt;Stop virtual machines&lt;/li&gt;
&lt;li&gt;View virtual machines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While restricting the user from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deleting virtual machines&lt;/li&gt;
&lt;li&gt;Modifying networking resources&lt;/li&gt;
&lt;li&gt;Assigning IAM roles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project helped me gain practical experience with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure IAM and RBAC&lt;/li&gt;
&lt;li&gt;Custom role creation&lt;/li&gt;
&lt;li&gt;Least privilege security&lt;/li&gt;
&lt;li&gt;Resource Group scoped permissions&lt;/li&gt;
&lt;li&gt;Permission testing and validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scenario&lt;/strong&gt;&lt;br&gt;
The DevOps team required a user who could operate virtual machines for daily operational tasks without having permission to delete resources or manage access control settings.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Create a custom RBAC role&lt;/li&gt;
&lt;li&gt;Assign VM-specific permissions&lt;/li&gt;
&lt;li&gt;Restrict unnecessary privileges&lt;/li&gt;
&lt;li&gt;Assign role at Resource Group level&lt;/li&gt;
&lt;li&gt;Validate access using a test user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Environment Setup&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resource Group&lt;/li&gt;
&lt;li&gt;Virtual Machine&lt;/li&gt;
&lt;li&gt;Test user (ops-user)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First we need to create our resource groups, to see guide on RG click the link below on one of our article on creating resource groups&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/highpee1991/how-to-create-a-resource-group-in-azure-step-by-step-guide-4cle" class="crayons-story__hidden-navigation-link"&gt;How to Create a Resource Group in Azure (Step-by-Step Guide)&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="/highpee1991" 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%2F1894849%2F48eadcb7-7cf7-4299-8728-58c535f1e6bd.jpeg" alt="highpee1991 profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/highpee1991" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Ipadeola Taiwo
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Ipadeola Taiwo
                
              
              &lt;div id="story-author-preview-content-3363141" 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="/highpee1991" 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%2F1894849%2F48eadcb7-7cf7-4299-8728-58c535f1e6bd.jpeg" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Ipadeola Taiwo&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/highpee1991/how-to-create-a-resource-group-in-azure-step-by-step-guide-4cle" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 17&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/highpee1991/how-to-create-a-resource-group-in-azure-step-by-step-guide-4cle" id="article-link-3363141"&gt;
          How to Create a Resource Group in Azure (Step-by-Step Guide)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/azure"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;azure&lt;/a&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/microsoft365"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;microsoft365&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/highpee1991/how-to-create-a-resource-group-in-azure-step-by-step-guide-4cle" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/highpee1991/how-to-create-a-resource-group-in-azure-step-by-step-guide-4cle#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;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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;Next is to create our user&lt;br&gt;
we would be brief with this step&lt;br&gt;
first search on the console search bar miscrosoft entra ID&lt;br&gt;
Click on the results &lt;br&gt;
Go to manage and click on users, create users&lt;br&gt;
to see step by step guide on creating a user, click on the link below to see full details on creating a user&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/highpee1991/implementing-rbac-in-azure-for-secure-resource-group-access-39n" class="crayons-story__hidden-navigation-link"&gt;Implementing RBAC in Azure for Secure Resource Group Access&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="/highpee1991" 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%2F1894849%2F48eadcb7-7cf7-4299-8728-58c535f1e6bd.jpeg" alt="highpee1991 profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/highpee1991" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Ipadeola Taiwo
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Ipadeola Taiwo
                
              
              &lt;div id="story-author-preview-content-3630217" 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="/highpee1991" 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%2F1894849%2F48eadcb7-7cf7-4299-8728-58c535f1e6bd.jpeg" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Ipadeola Taiwo&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/highpee1991/implementing-rbac-in-azure-for-secure-resource-group-access-39n" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 8&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/highpee1991/implementing-rbac-in-azure-for-secure-resource-group-access-39n" id="article-link-3630217"&gt;
          Implementing RBAC in Azure for Secure Resource Group Access
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/azure"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;azure&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cloud"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cloud&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tutorial"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tutorial&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/highpee1991/implementing-rbac-in-azure-for-secure-resource-group-access-39n#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;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&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;Next step is to Create the Custom Role:&lt;br&gt;
To create the custome role follow below step&lt;br&gt;
first go to the resource group created, click on access control (IAM), click on add, add custome role&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%2Fkkg3lpr8z264ff2sc22c.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%2Fkkg3lpr8z264ff2sc22c.PNG" alt=" " width="800" height="653"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give the custom role a name, and description&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%2F0h0pom1kj4r4maa65rrz.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%2F0h0pom1kj4r4maa65rrz.PNG" alt=" " width="800" height="331"&gt;&lt;/a&gt;&lt;br&gt;
Click on next&lt;br&gt;
Click on add permission&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%2Fmbbtt0ee1ejdkoon94mm.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%2Fmbbtt0ee1ejdkoon94mm.PNG" alt=" " width="800" height="598"&gt;&lt;/a&gt;&lt;br&gt;
since the role we want to assign is read (view), start and stop (deallocate)&lt;br&gt;
we would would input this in the search for permission Microsoft.Compute/virtualMachines this allow us to have access to virtual machine control, click on the result microsoft compute then go further by adding /read to the path after clicking on the microsoft compute, select permission and click on add&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fste9rh9jsnjtv4vv2qzf.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%2Fste9rh9jsnjtv4vv2qzf.PNG" alt=" " width="800" height="598"&gt;&lt;/a&gt;&lt;br&gt;
The read permision will be added&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%2Fwxt04msqq5mn9raod2l8.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%2Fwxt04msqq5mn9raod2l8.PNG" alt=" " width="800" height="278"&gt;&lt;/a&gt;&lt;br&gt;
we would do the same thing for start&lt;br&gt;
we would click on add permision again, where we have search for a permission we would input this again Microsoft.Compute/virtualMachines and click on microsoft compute after then add /start to the path click on the permission start vm and add &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpkvg4yqczl9nx49ytyxv.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%2Fpkvg4yqczl9nx49ytyxv.PNG" alt=" " width="800" height="497"&gt;&lt;/a&gt;&lt;br&gt;
this will add the start permission to the list of our created custom role&lt;/p&gt;

&lt;p&gt;next is to create custom role to stop vm in azure it is not called stop but deallocate so we would do the same thing as we have been doing before click on add permison, paste Microsoft.Compute/virtualMachines in the in the search bar and click on miscrosoft compute add /deallocate to the path and select the permission, so this give it the power to stop the vm &lt;br&gt;
Our permission should look like this &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%2F24pjr84lu8mhx1potg6n.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%2F24pjr84lu8mhx1potg6n.PNG" alt=" " width="800" height="507"&gt;&lt;/a&gt;&lt;br&gt;
this add the read, start and stop power custome role, click next, asignable scope we leave it as defaults, then we have our json created&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{    "properties": {        "roleName": "VM Operator",        "description": "The custom role allowed the user to:\n- Start virtual machines\n- Stop virtual machines\n- View virtual machines",        "assignableScopes": [            "/subscriptions/732a7227-baa8-4458-8e10-4c85a2615397/resourceGroups/vm-Operator-rg"        ],        "permissions": [            {                "actions": [                    "Microsoft.Compute/virtualMachines/read",                    "Microsoft.Compute/virtualMachines/start/action",                    "Microsoft.Compute/virtualMachines/deallocate/action"                ],                "notActions": [],                "dataActions": [],                "notDataActions": []            }        ]    }}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Click next and create&lt;/p&gt;

&lt;p&gt;for security restriction, i intenstionally did not include &lt;br&gt;
Microsoft.Authorization/*&lt;br&gt;
Microsoft.Network/*&lt;br&gt;
Microsoft.Compute/virtualMachines/delete&lt;/p&gt;

&lt;p&gt;this prevented&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IAM modifications&lt;/li&gt;
&lt;li&gt;Network changes&lt;/li&gt;
&lt;li&gt;VM deletion 
we would now assign this role to our user ops-user, that we created at the reourse group level, this give the user power to start, stop and view vms's in this resource group
For us to assign the role to our user we would go to our resource group vm-operator-rg, that we created 
click on add&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fce3vzbjxjbcq2d3tekot.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%2Fce3vzbjxjbcq2d3tekot.PNG" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;br&gt;
click on add role assigment&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%2Fb2v5lzx5yeuwjqhffjo0.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%2Fb2v5lzx5yeuwjqhffjo0.PNG" alt=" " width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;on the role search for the custome role we created by typing the name we gave to it, vm operator and select the custome role &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgs85f847f9m4va9q3hqz.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%2Fgs85f847f9m4va9q3hqz.PNG" alt=" " width="800" height="378"&gt;&lt;/a&gt;&lt;br&gt;
click on next &lt;br&gt;
click on select member to add the user we want to give this permision to &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%2Fp6xr5yvc8afxmskzx34h.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%2Fp6xr5yvc8afxmskzx34h.PNG" alt=" " width="800" height="403"&gt;&lt;/a&gt;&lt;br&gt;
select the user and click on select&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%2Fbqqoj7f1v7cxs703ehwu.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%2Fbqqoj7f1v7cxs703ehwu.PNG" alt=" " width="550" height="386"&gt;&lt;/a&gt;&lt;br&gt;
click on next, then review + assign &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%2Fzia0p7a581yarquyldol.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%2Fzia0p7a581yarquyldol.PNG" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;br&gt;
we have succesfully created our custome role and assign it to a user&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%2Ffa72sq9jyesl2iocf6s1.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%2Ffa72sq9jyesl2iocf6s1.PNG" alt=" " width="550" height="386"&gt;&lt;/a&gt;&lt;br&gt;
our user can start vm, view vm stop vm, but cannot delete vm or modify IAM&lt;/p&gt;

&lt;p&gt;for us to practicalise some of the action we can log in to user ops-user account&lt;br&gt;
first we try assign IAM to the vm from the ops-user account we can see that the add button is greyed there is no permission for the user to do this operation&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8y5p6eh3tnmb893wq3lf.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%2F8y5p6eh3tnmb893wq3lf.PNG" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;br&gt;
also let us try and delete the vm &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqx9cph2d06tdybnbh1rt.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%2Fqx9cph2d06tdybnbh1rt.PNG" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;
we can see we got an error trying to delete the vm &lt;/p&gt;

&lt;p&gt;now let us try stop our running vm&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhuekq5er28y4h4ptf669.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%2Fhuekq5er28y4h4ptf669.PNG" alt=" " width="800" height="453"&gt;&lt;/a&gt;&lt;br&gt;
It was a success because we gave the user the power to stop the vm&lt;/p&gt;

&lt;p&gt;let us start our vm, we click on start &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%2F0tin2ng62wxwuklfdhyw.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%2F0tin2ng62wxwuklfdhyw.PNG" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;
we succesfully started our vm &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Security Lessons&lt;/strong&gt;&lt;br&gt;
Least privilege reduces security risk&lt;br&gt;
Custom roles provide granular control&lt;br&gt;
Resource Group scope limits exposure&lt;br&gt;
Built-in roles may provide excessive permissions&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
This project demonstrated how custom RBAC roles can be used to enforce least privilege access in Azure environments. Instead of assigning broad built-in roles, custom permissions allowed secure delegation of operational tasks while protecting critical infrastructure and access control configurations.&lt;/p&gt;

&lt;p&gt;Thanks for reading&lt;/p&gt;

</description>
      <category>azure</category>
      <category>beginners</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Implementing RBAC in Azure for Secure Resource Group Access</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Fri, 08 May 2026 03:14:09 +0000</pubDate>
      <link>https://dev.to/highpee1991/implementing-rbac-in-azure-for-secure-resource-group-access-39n</link>
      <guid>https://dev.to/highpee1991/implementing-rbac-in-azure-for-secure-resource-group-access-39n</guid>
      <description>&lt;p&gt;In modern cloud environments, not every team member should have full access to resources. Proper access control is important for security, accountability, and preventing accidental changes.&lt;/p&gt;

&lt;p&gt;In this project, I implemented Role-Based Access Control (RBAC) in Microsoft Azure to manage access to a Resource Group. I created multiple users with different permission levels and ensured that only authorized users could create or modify resources, while others had view-only access.&lt;/p&gt;

&lt;p&gt;This hands-on project helped me understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure IAM and RBAC&lt;/li&gt;
&lt;li&gt;Role assignments&lt;/li&gt;
&lt;li&gt;Least privilege principle&lt;/li&gt;
&lt;li&gt;Resource Group level access control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Project Scenario&lt;/strong&gt;&lt;br&gt;
A company project is hosted in Azure, and multiple team members require access to the environment. However, not everyone should have full control over the resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objectives&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Resource Group&lt;/li&gt;
&lt;li&gt;Create users&lt;/li&gt;
&lt;li&gt;Assign RBAC roles&lt;/li&gt;
&lt;li&gt;Implement least privilege access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Implementation Steps&lt;/strong&gt;&lt;br&gt;
First create a resource group, this act as logical container for our resoucrces and help us manage our resources easily&lt;/p&gt;

&lt;p&gt;To create a resource group click on the search bar in azure console and type resource groups&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%2Fyng2w5d9knoopkthxnip.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%2Fyng2w5d9knoopkthxnip.PNG" alt=" " width="800" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the resource groups result shown &lt;/p&gt;

&lt;p&gt;Nest step is to click on create as shown in the image below&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%2F2dld5ou4i2zktsncrrqc.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%2F2dld5ou4i2zktsncrrqc.PNG" alt=" " width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;on clicking on the create button it will take you to the next page where you will fill in the following information, information such as selecting subscription, by default azure create subscription1 for us and that is what we would be using in this mini project&lt;br&gt;
next give it a resource group name and also select a region, for this project we would go with the South Africa North (as i am based in Nigeria and is the closed region to my company, choose a region closest to your company)&lt;br&gt;
then click on next&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%2Fa71ovf6suleaxbcvdmqu.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%2Fa71ovf6suleaxbcvdmqu.PNG" alt=" " width="800" height="685"&gt;&lt;/a&gt;&lt;br&gt;
Next you are to select a tag name and value for the resource group to make it easier to search, but this is optional for this project we would leave it blank&lt;br&gt;
Next is to click on review and create after confirmation click on create&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%2Fugz030340pl9iks7n7nn.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%2Fugz030340pl9iks7n7nn.PNG" alt=" " width="445" height="703"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;we have succesfully created our resource groups&lt;/p&gt;

&lt;p&gt;next let us create a user &lt;br&gt;
for this project we would create 3 users&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;dev-user -&amp;gt; role: Contributor -&amp;gt; Access Level: Create/Edit Resources&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;tester-user -&amp;gt; role: Reader -&amp;gt; Access Level: View Only&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;auditor-user -&amp;gt; role: Reader -&amp;gt; Access Level: View Only&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To create a user click on the search bar and type microsoft entra ID &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%2F2zc0f4ck279tawaprl9z.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%2F2zc0f4ck279tawaprl9z.PNG" alt=" " width="800" height="183"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on microsoft entra ID from the results&lt;br&gt;
Go to manage and click on results&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%2F9drh52bsj8gnjrppm8qn.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%2F9drh52bsj8gnjrppm8qn.PNG" alt=" " width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on new user, create new user&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%2Faveblbsfebmytx6m6l27.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%2Faveblbsfebmytx6m6l27.PNG" alt=" " width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;fill all the information and click on next &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%2F0spzxts798m2xmyakzyd.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%2F0spzxts798m2xmyakzyd.PNG" alt=" " width="724" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since we want to gie it role from the resource group level we wonrt give it role at this point we would click on review and create and this would create our dev-user&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%2Fj359zsb693vewk5d3cp6.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%2Fj359zsb693vewk5d3cp6.PNG" alt=" " width="636" height="690"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;we would create a new user again to do the same thing we did ealier to create our tester-user and our auditor-user&lt;/p&gt;

&lt;p&gt;at this state we have our three new users created&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%2Fi8bxtqfuq3x4y7s8ifjh.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%2Fi8bxtqfuq3x4y7s8ifjh.PNG" alt=" " width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let us give each users role to what they can do so we dont just give them unnecessary power they do not need to have, for protections and securities reasons&lt;/p&gt;

&lt;p&gt;for us to give them role we would go our resource group we created ealier, by typing resource group in the search bar and click on jake-projects-rg we created ealier&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%2Fwvetvo90nkn4u9qychv1.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%2Fwvetvo90nkn4u9qychv1.PNG" alt=" " width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank click on access control (IAM)&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%2Fx36ptrgc1x17aleb4qwe.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%2Fx36ptrgc1x17aleb4qwe.PNG" alt=" " width="800" height="550"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We would click on add -&amp;gt; assign role&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%2F6g289k7p7qaiz6tmkc7j.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%2F6g289k7p7qaiz6tmkc7j.PNG" alt=" " width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;let us start assigning role to our dev-user first, since we want to give contributor access control to our dev-user we would role we wouls click on priviledge administrator and search for contributor&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%2Fdf3s2lzt8yvu77k398ij.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%2Fdf3s2lzt8yvu77k398ij.PNG" alt=" " width="800" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the next button &lt;br&gt;
click on User, group, or service principal at the select access to&lt;br&gt;
on the members click on select member in order to add we we want to give this permision to which is our dev-user&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%2Fv0qmg1k4zmvm69oyxkit.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%2Fv0qmg1k4zmvm69oyxkit.PNG" alt=" " width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;now our dev-user have been selected &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%2Ffyu12pvdd3qe3i4irvv0.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%2Ffyu12pvdd3qe3i4irvv0.PNG" alt=" " width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next click on review and assign&lt;br&gt;
we have succesfully given role based accessed control to our dev-user&lt;/p&gt;

&lt;p&gt;now let us do same thing to our remaining 2 users we want to give read access to &lt;/p&gt;

&lt;p&gt;we would do the same thing as above &lt;br&gt;
we would click on add, add role assignment &lt;br&gt;
since the access we want to give to both users are read access only we would click on the job function and select the reader access this restrict the user from creating resources or accidentally deleting the resources but can only view&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%2Fl897mca8dz0qak8fyvhr.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%2Fl897mca8dz0qak8fyvhr.PNG" alt=" " width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We would click on the add member and select the member we want to assign this role to in our case is the tester-user, and the auditor-user&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%2Fk17ksm8w41i6o9de0dro.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%2Fk17ksm8w41i6o9de0dro.PNG" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;we would click on review and assign to complete this &lt;br&gt;
we have succesfully created and assign reader role to both users&lt;/p&gt;

&lt;p&gt;When we click on role assigment inside the access control (IAM) in our resource group we can see we have succesfully &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%2Fa8xic5p9hayw4zxm9qop.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%2Fa8xic5p9hayw4zxm9qop.PNG" alt=" " width="569" height="393"&gt;&lt;/a&gt;&lt;br&gt;
now this users can now sign in to their created accounts and do only what they are gicen power to do&lt;/p&gt;

&lt;p&gt;The Dev-user can create resources, modify resources work on resources but  delete resources and delete the resource group, mind you if the dev user mistakenly delete the resource group it can not be undone&lt;/p&gt;

&lt;p&gt;auditor user and tester user can only read but can not create resources can not modify resources and can not delete resources&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security Improvement&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;During testing, I observed that a user with the Contributor role could delete the entire Resource Group. To improve protection against accidental or unauthorized deletion, Azure Resource Locks can be implemented to restrict deletion operations even for users with elevated resource permissions.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Deploying an Azure Virtual Machine: Networking, SSH, and Apache Setup</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Sat, 18 Apr 2026 16:36:19 +0000</pubDate>
      <link>https://dev.to/highpee1991/deploying-an-azure-virtual-machine-networking-ssh-and-apache-setup-5e2c</link>
      <guid>https://dev.to/highpee1991/deploying-an-azure-virtual-machine-networking-ssh-and-apache-setup-5e2c</guid>
      <description>&lt;p&gt;Before deploying a virtual machine, we need to create a Resource Group. In Azure, a Resource Group acts as a container that holds all related resources such as virtual machines, networking components, and storage.&lt;br&gt;
&lt;strong&gt;Step 1: Create a Resource Group&lt;/strong&gt;: To create a resource group, type on search box to acces resource group, and create a resource group&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr077jfbdznxe52xu7w3w.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%2Fr077jfbdznxe52xu7w3w.png" alt=" " width="767" height="597"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;After creating the Resource Group, the next step is to configure networking. In Azure, this is done using a Virtual Network (VNet), which provides a private network for your virtual machine.&lt;/p&gt;

&lt;p&gt;Before deploying a virtual machine, we need to set up the required networking. In Azure, this is done by creating a Virtual Network (VNet), which allows our VM to communicate securely with other resources.&lt;/p&gt;

&lt;p&gt;This leads us to the next step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Create a Virtual Network (VNet)&lt;/strong&gt;: In the Azure portal, search for Virtual Networks using the search bar. Select it, then click on Create to begin setting up a new virtual network.&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%2F0wtqogcq8lwp1z8wnvig.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%2F0wtqogcq8lwp1z8wnvig.png" alt=" " width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Select your existing subscription, then choose the Resource Group you created earlier.&lt;/p&gt;

&lt;p&gt;Click Next to proceed to the Security tab and leave the default settings unchanged. Continue by clicking Next to the IP Addresses section.&lt;/p&gt;

&lt;p&gt;Here, keep the default address space of 10.0.0.0/16, which provides up to 65,536 IP addresses for your virtual network. Delete the default subnet and click next, you can leave the tag blank depends on your choice, review and create your vnet&lt;/p&gt;

&lt;p&gt;Step 3: After creating the Virtual Network, open it from the Azure portal. In the left-hand menu, navigate to Settings and select Subnets.&lt;/p&gt;

&lt;p&gt;Click on + Subnet to create a new subnet within the virtual network.&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%2Fn0u1rwbmk63uft9o98gv.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%2Fn0u1rwbmk63uft9o98gv.png" alt=" " width="268" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on +subnet&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%2F9ndnyujlwerbzcubc9tf.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%2F9ndnyujlwerbzcubc9tf.png" alt=" " width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give the subnet a name, then set the address range to 10.0.0.0/24.&lt;br&gt;
This subnet mask provides 256 IP addresses.&lt;br&gt;
Finally, click Add to create the subnet.&lt;/p&gt;

&lt;p&gt;After setting up the Resource Group, Virtual Network, and Subnet, we are now ready to deploy the Virtual Machine.&lt;br&gt;
These networking components ensure that the VM can communicate securely within the Azure environment&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Create a Virtual Machine&lt;/strong&gt;: To deploy our vm we click on the search box in the azure console and click on the virtual machine in the resultof the search&lt;/p&gt;

&lt;p&gt;click on create&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%2Fowuczf62d8bwmek9i5i7.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%2Fowuczf62d8bwmek9i5i7.png" alt=" " width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;click on virtual machine&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%2Fx8mdudr3p8g5vklthtq2.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%2Fx8mdudr3p8g5vklthtq2.png" alt=" " width="553" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;select the created resource group, give it a name, select the region of your choice, (best region closer to the clients)&lt;/p&gt;

&lt;p&gt;For Availability Zone, you can select Zone 1 for testing purposes.&lt;/p&gt;

&lt;p&gt;Next, choose the operating system for your virtual machine. You can select either Windows or Linux, depending on your use case. For this tutorial, we will proceed with Ubuntu Server 24.04 LTS.&lt;/p&gt;

&lt;p&gt;Select the VM size based on your requirements (for training purposes, a smaller size is recommended).&lt;/p&gt;

&lt;p&gt;For authentication type, select SSH Public Key for secure access. Then, provide a username for the VM.&lt;/p&gt;

&lt;p&gt;Under Inbound port rules, allow public inbound access and select the required ports:&lt;/p&gt;

&lt;p&gt;Port 22 (SSH) – for connecting to your VM&lt;br&gt;
Port 80 (HTTP) – for web traffic once Apache is installed, click on next to Disk&lt;/p&gt;

&lt;p&gt;Os size should be by choice for training purpose choose 30 gig to incure little or no cost&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F98onjpgc126vzbjjgegs.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%2F98onjpgc126vzbjjgegs.png" alt=" " width="788" height="462"&gt;&lt;/a&gt;&lt;br&gt;
For the Disk type, you can choose between SSD and HDD.&lt;br&gt;
For cost efficiency during training, select HDD, then click Next.&lt;/p&gt;

&lt;p&gt;Networking&lt;/p&gt;

&lt;p&gt;Select the previously created Virtual Network (VNet) and Subnet.&lt;br&gt;
Allow public inbound access for required ports, then proceed to the next step.&lt;/p&gt;

&lt;p&gt;Management&lt;/p&gt;

&lt;p&gt;For simplicity, leave all settings at their default values.&lt;/p&gt;

&lt;p&gt;Monitoring&lt;/p&gt;

&lt;p&gt;Also leave the monitoring settings as default.&lt;/p&gt;

&lt;p&gt;Advanced&lt;/p&gt;

&lt;p&gt;Leave all advanced options unchanged.&lt;/p&gt;

&lt;p&gt;Tags&lt;/p&gt;

&lt;p&gt;You may leave tags blank for this tutorial.&lt;/p&gt;

&lt;p&gt;click on create, download key pair from the prompts&lt;/p&gt;

&lt;p&gt;Well done our virtual machine have been deployed&lt;/p&gt;

&lt;p&gt;Next, we will connect to our Virtual Machine using SSH and install Apache2 on the server.&lt;/p&gt;

&lt;p&gt;To begin, open Git Bash or any Linux-based command-line interface (CLI) on your local machine.&lt;/p&gt;

&lt;p&gt;SSH into the Virtual Machine&lt;/p&gt;

&lt;p&gt;To SSH into our VM, we will use the following steps:&lt;/p&gt;

&lt;p&gt;First, locate your .pem file, which is usually in your Downloads folder.&lt;/p&gt;

&lt;p&gt;Navigate to the Downloads directory using the command:&lt;/p&gt;

&lt;p&gt;cd Downloads&lt;/p&gt;

&lt;p&gt;Next, set the correct permissions for your private key file:&lt;/p&gt;

&lt;p&gt;chmod 400 file.pem&lt;/p&gt;

&lt;p&gt;This ensures that only you have read access to the key file.&lt;/p&gt;

&lt;p&gt;Now, connect to your Virtual Machine using the SSH command:&lt;/p&gt;

&lt;p&gt;ssh -i file.pem username@publicIP&lt;/p&gt;

&lt;p&gt;When prompted, type yes to accept the host key and establish the connection.&lt;/p&gt;

&lt;p&gt;welcome to our vm&lt;br&gt;
first before we do anything we need to update and upgrade our virtual machine using below command line&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;





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

&lt;/div&gt;



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

&lt;p&gt;now let us install our apache2 by following below command&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;to start our apache 2 use the below command line &lt;br&gt;
sudo systemctl start apache2&lt;/p&gt;

&lt;p&gt;and yesss we just Connect to the VM using SSH. Install Apache2 on the VM.&lt;/p&gt;

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

</description>
      <category>azure</category>
      <category>beginners</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Create a Resource Group in Azure (Step-by-Step Guide)</title>
      <dc:creator>Ipadeola Taiwo</dc:creator>
      <pubDate>Tue, 17 Mar 2026 13:26:41 +0000</pubDate>
      <link>https://dev.to/highpee1991/how-to-create-a-resource-group-in-azure-step-by-step-guide-4cle</link>
      <guid>https://dev.to/highpee1991/how-to-create-a-resource-group-in-azure-step-by-step-guide-4cle</guid>
      <description>&lt;h2&gt;
  
  
  What is resource group and why should you care
&lt;/h2&gt;

&lt;p&gt;In Microsoft Azure, a Resource Group is a logical container used to organize and manage related resources. It is one of the first things you need to create before deploying any service in Azure.&lt;/p&gt;

&lt;p&gt;Whether you're deploying a virtual machine, database, or web app, Resource Groups help you manage everything efficiently, control access, and reduce costs.&lt;/p&gt;

&lt;p&gt;In this guide, you’ll learn how to create a Resource Group step-by-step using the Azure Portal.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: Log in to the Azure Portal by visiting portal.azure.com and signing in to your account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;: In the Azure Portal, use the search bar at the top and type “Resource groups,” then select it from the results.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 3&lt;/strong&gt;: Click the “Create” button to start creating a new Resource Group.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 4&lt;/strong&gt;: Configure your Resource Group settings.
By default, your Azure subscription (e.g., “Subscription 1”) will be selected. If you have multiple subscriptions, choose the one you want to use.
Next, enter a &lt;strong&gt;name&lt;/strong&gt; for your Resource Group—usually the name of your project works best.
Then, select a &lt;strong&gt;region&lt;/strong&gt; close to where your project or users are located.
Once done, click the &lt;strong&gt;Next&lt;/strong&gt; button to continue.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 5&lt;/strong&gt;: Add tags (optional)
Tags help you organize resources so you can find them easily later.
You can leave this section blank if you want. It won’t affect your Resource Group.
In this tutorial, we’ll skip adding tags.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 6&lt;/strong&gt;: Review and create your Resource Group
After clicking Next in the previous step, you will be taken to the Review + Create page.
Carefully check that all the information is correct, including your subscription, Resource Group name, and region.
Once everything looks good, click Create. Congratulations—you have successfully created your Resource Group! 🎉&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step 7&lt;/strong&gt;: Confirm your Resource Group creation
After clicking Create, look at the bell icon at the top of the Azure Portal. You should see a notification saying “Resource Group created”.
You can then choose to either Go to resource group to view it immediately or Pin to dashboard for quick access later.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Congratulations! You have successfully created your first Resource Group in Azure. By organizing your resources in a Resource Group, you can manage, monitor, and deploy your projects efficiently.&lt;/p&gt;

&lt;p&gt;Next, you can start adding resources like Virtual Machines, Databases, or Storage accounts to your Resource Group and explore how Azure manages them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practices&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Use meaningful names for your Resource Groups to easily identify projects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose regions close to your users to improve performance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use tags for better organization, especially if you manage many projects.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now that you know how to create a Resource Group, you’re ready to start building and managing your cloud projects in Azure. Happy learning! 🚀&lt;/p&gt;

&lt;p&gt;Did you find this guide helpful? 💡&lt;br&gt;
I’d love to hear about your experience creating your first Resource Group in Azure!&lt;br&gt;
Share your thoughts or questions in the comments below, and stay tuned for more tutorials on Azure, Cloud Computing, and DevOps.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloudcomputing</category>
      <category>devops</category>
      <category>microsoft365</category>
    </item>
  </channel>
</rss>
