<?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: David Cletus</title>
    <description>The latest articles on DEV Community by David Cletus (@4thman).</description>
    <link>https://dev.to/4thman</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%2F3927892%2F43aac6d7-df74-4a90-bdbe-8ba55edf1a70.jpeg</url>
      <title>DEV Community: David Cletus</title>
      <link>https://dev.to/4thman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/4thman"/>
    <language>en</language>
    <item>
      <title>Dockerizing a Node App &amp; Shipping It to Azure: Build, Break, Fix &amp; Ship (3 Errors, Zero Regrets) 🐳</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Sat, 26 Sep 2026 00:33:05 +0000</pubDate>
      <link>https://dev.to/4thman/-catching-container-vibes-dockerizing-a-node-app-shipping-it-to-azure-3-errors-zero-regrets-p02</link>
      <guid>https://dev.to/4thman/-catching-container-vibes-dockerizing-a-node-app-shipping-it-to-azure-3-errors-zero-regrets-p02</guid>
      <description>&lt;p&gt;I wanted a hands-on project to actually cement what I'd been learning in Docker and Azure, build something small, break it, fix it, and ship it for real. So I built &lt;strong&gt;Container Vibes&lt;/strong&gt;: a tiny Express app that hands you a random "vibe" (a color theme + a little quote) every time you click a button. It's deliberately simple. The point was never the app it was everything around it: containerizing it properly, pushing it to a registry, and getting it running live on Azure Container Instances (ACI).&lt;/p&gt;

&lt;p&gt;This post walks through the entire build, step by step, in the exact order I actually did it including the three errors I ran into along the way and exactly how I diagnosed and fixed each one. &lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&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%2Funx1yjo84gyadnjoik9f.jpg" 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%2Funx1yjo84gyadnjoik9f.jpg" alt=" " width="642" height="648"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small Node.js/Express app with three routes: a homepage, a &lt;code&gt;/health&lt;/code&gt; check, and a &lt;code&gt;/api/vibe&lt;/code&gt; endpoint that returns a random theme&lt;/li&gt;
&lt;li&gt;A Docker image for it, tested locally&lt;/li&gt;
&lt;li&gt;The image pushed up to Docker Hub as the deployable artifact&lt;/li&gt;
&lt;li&gt;The container running live on Azure Container Instances, reachable at a real URL&lt;/li&gt;
&lt;li&gt;The source code version-controlled and pushed to GitHub&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 — Laying the groundwork
&lt;/h2&gt;

&lt;p&gt;First thing, I set up a clean folder structure so everything had a home:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;docker-project-1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;docker-project-1
code &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That opens the empty folder straight into VS Code. From there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;hagital &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;hagital
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;touch &lt;/span&gt;Dockerfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm init -y&lt;/code&gt; scaffolds a &lt;code&gt;package.json&lt;/code&gt; with all the defaults accepted automatically no need to answer the usual prompts one by one. This file is what tells both Node and, later, Docker's &lt;code&gt;npm install&lt;/code&gt; step exactly which dependencies the project needs, so getting it created early matters. I also touched an empty &lt;code&gt;Dockerfile&lt;/code&gt; right away, just as a placeholder for what was coming.&lt;/p&gt;

&lt;p&gt;Then the actual app files:&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;touch &lt;/span&gt;server.js .dockerignore
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapcrxefzdqblfy35dnw2.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%2Fapcrxefzdqblfy35dnw2.png" alt=" " width="786" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Step 2 — Writing the app
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;server.js&lt;/code&gt; is where all the actual logic lives. It's a small Express server holding a roster of "vibes" each one a name, a pair of gradient colors, and a short quote plus two routes: one that picks a vibe at random, and a health check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// A little roster of "container vibes" — color themes + quotes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;VIBES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sunset Deploy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FF6B6B&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FFD93D&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Life is good and could be challenging — ship it anyway.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ocean Rollout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#4ECDC4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#556270&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Every container has a bad day. Restart policy: always.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Neon Nightshift&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#8E2DE2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#4A00E0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Logs don't lie. Neither do good tests.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forest Uptime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#11998e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#38ef7d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Healthy pods, happy engineers.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Candy CI/CD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f857a6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ff5858&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Green pipeline, good vibes.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Golden Hour Git&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f7971e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffd200&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Commit small, dream big.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cosmic Cluster&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0f0c29&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#302b63&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#24243e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Somewhere out there, a pod is scaling for you.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;static&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/vibe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vibe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;VIBES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;VIBES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;vibe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;uptime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uptime&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Life is good and could be challenging. But we are up.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✨ Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; — go visit http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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 few things worth pointing out: &lt;code&gt;express.static&lt;/code&gt; serves everything in the &lt;code&gt;public&lt;/code&gt; folder (that's how the HTML page gets delivered), &lt;code&gt;/api/vibe&lt;/code&gt; picks a random entry from the &lt;code&gt;VIBES&lt;/code&gt; array and tacks on a live timestamp and the server's uptime so every response feels fresh, and &lt;code&gt;/health&lt;/code&gt; exists purely as a lightweight endpoint to confirm the server is alive which turned out to be genuinely useful later when checking on the container from Azure's side.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 3 — Building the frontend
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;public &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;public
&lt;span class="nb"&gt;touch &lt;/span&gt;index.html
&lt;span class="nb"&gt;cd&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhfnnlhpaori95qihz3g4.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%2Fhfnnlhpaori95qihz3g4.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;public/index.html&lt;/code&gt; is a single self-contained page: a gradient background that slowly animates through the current vibe's colors, a frosted-glass "card" in the center holding the vibe's name and quote, a small uptime readout, and a "New Vibe" button. Clicking that button calls &lt;code&gt;/api/vibe&lt;/code&gt;, gets back a fresh random theme as JSON, and updates the card's colors and text on the page without a reload.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 4 — Writing the Dockerfile
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# pull the official Node.js image from the Docker Hub&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;

&lt;span class="c"&gt;# set the working directory in the container&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# copy package.json and package-lock.json to the working directory&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;

&lt;span class="c"&gt;# install dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# copy the rest of the application code to the working directory&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# expose the port that the application will run on&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="c"&gt;# define the command to run the application&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; [ "node", "server.js" ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking through it line by line: &lt;code&gt;FROM node:20-alpine&lt;/code&gt; grabs a minimal, lightweight Node 20 base image (the Alpine variant strips out everything not strictly needed, keeping the image small). &lt;code&gt;WORKDIR /app&lt;/code&gt; sets where all the following commands run inside the container. &lt;code&gt;COPY package*.json ./&lt;/code&gt; copies over just &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt; first — deliberately, before the rest of the code so that &lt;code&gt;RUN npm install&lt;/code&gt; becomes its own cached layer. That means if I only change &lt;code&gt;server.js&lt;/code&gt; later and rebuild, Docker can reuse the already-installed &lt;code&gt;node_modules&lt;/code&gt; layer instead of reinstalling everything from scratch every single time. &lt;code&gt;COPY . .&lt;/code&gt; then brings in the rest of the app. &lt;code&gt;EXPOSE 3000&lt;/code&gt; documents which port the app listens on. And &lt;code&gt;CMD [ "node", "server.js" ]&lt;/code&gt; is what actually runs when a container starts from this image.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 5 — First build, first surprise
&lt;/h2&gt;

&lt;p&gt;With the Dockerfile in place, I moved back up to the project root and kicked off the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ..
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; 4thman/hagital:v1 &lt;span class="nb"&gt;.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzpmiva1cdzlraffh9dmm.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%2Fzpmiva1cdzlraffh9dmm.png" alt=" " width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It built clean — all 11 steps finished, image tagged as &lt;code&gt;4thman/hagital:v1&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Feeling good, I ran it and checked the images list first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker images
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="nt"&gt;--name&lt;/span&gt; hagital-app 4thman/hagital:v1
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdp7l9jy91qpxrll0bqxk.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%2Fdp7l9jy91qpxrll0bqxk.png" alt=" " width="799" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then opened &lt;code&gt;localhost:3000&lt;/code&gt; in the browser, fully expecting my little vibes card to show up. Instead: &lt;strong&gt;"This site can't be reached localhost refused to connect."&lt;/strong&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Debugging: the case of the missing module
&lt;/h2&gt;

&lt;p&gt;A clean &lt;code&gt;docker build&lt;/code&gt; doesn't guarantee a working app that's the lesson this error taught me. First move was checking whether the container was even still alive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It showed status &lt;code&gt;Exited (1)&lt;/code&gt;, meaning the container had started and then immediately crashed. Time to check why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs hagital-app
&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;Error: Cannot find module 'express'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there it was. I had written &lt;code&gt;require('express')&lt;/code&gt; in &lt;code&gt;server.js&lt;/code&gt;, but I had never actually run &lt;code&gt;npm install express&lt;/code&gt; on my own machine so &lt;code&gt;package.json&lt;/code&gt; had zero dependencies listed. When the Dockerfile's &lt;code&gt;RUN npm install&lt;/code&gt; step ran during the build, there was simply nothing in &lt;code&gt;package.json&lt;/code&gt; for it to install. The image built successfully because Docker didn't know anything was missing it only found out at runtime, the moment Node tried to &lt;code&gt;require()&lt;/code&gt; a package that was never there.&lt;/p&gt;

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

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That updates &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt; with the real dependency. Then it was just a matter of rebuilding the image (this time you could see only the &lt;code&gt;WORKDIR&lt;/code&gt; layer got reused from cache everything from &lt;code&gt;COPY package*.json&lt;/code&gt; onward had to rerun, since the package files had actually changed) and swapping the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; 4thman/hagital:v1 &lt;span class="nb"&gt;.&lt;/span&gt;
docker &lt;span class="nb"&gt;rm &lt;/span&gt;hagital-app
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="nt"&gt;--name&lt;/span&gt; hagital-app 4thman/hagital:v1
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjqia8zblaicih51otiu0.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%2Fjqia8zblaicih51otiu0.png" alt=" " width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Success — locally, at last
&lt;/h2&gt;

&lt;p&gt;Refreshed the browser and there it was: Container Vibes, running from inside a container, gradient background and all.&lt;/p&gt;

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

&lt;p&gt;I went through all three routes to be thorough the homepage, the health check, and the vibe API directly:&lt;/p&gt;

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

&lt;p&gt;(homepage) · &lt;/p&gt;

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

&lt;p&gt;(&lt;code&gt;/health&lt;/code&gt; returning &lt;code&gt;{"status":"ok",...}&lt;/code&gt;) · &lt;/p&gt;

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

&lt;p&gt;(&lt;code&gt;/api/vibe&lt;/code&gt; returning a random theme as JSON)*&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick concept check: EXPOSE vs -p vs --dns-name-label
&lt;/h2&gt;

&lt;p&gt;These three get mixed up a lot, but they each solve a different problem. &lt;code&gt;EXPOSE&lt;/code&gt; in the Dockerfile is really just documentation it tells anyone reading the image which port the app inside expects traffic on, but it doesn't actually open anything to the outside world by itself; that's exactly why the app could build fine and still be unreachable. The &lt;code&gt;-p&lt;/code&gt; flag on &lt;code&gt;docker run&lt;/code&gt; is the one doing the real work locally: it's the bridge that maps a port on your own machine to a port inside the container (&lt;code&gt;-p 3000:3000&lt;/code&gt; means "traffic hitting port 3000 on my laptop gets forwarded into the container's port 3000"), and without it your app can be running perfectly inside the container and you'd still get "connection refused" on the host. &lt;code&gt;--dns-name-label&lt;/code&gt;, which comes in later once we're on Azure, is a different beast entirely since Azure Container Instances hands your container a public IP that's a pain to remember and isn't guaranteed to stay the same, this flag lets you attach a friendly, stable subdomain to it instead (like &lt;code&gt;david-hagital.eastus.azurecontainer.io&lt;/code&gt;), so you're never hunting down a raw IP address to share a link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Cleaning up locally
&lt;/h2&gt;

&lt;p&gt;Before moving on, I ran through the basic container lifecycle commands just to get comfortable with them checking status, reading logs, stopping, and removing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
docker logs hagital-app
docker stop hagital-app
docker &lt;span class="nb"&gt;rm &lt;/span&gt;hagital-app
docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last &lt;code&gt;docker ps&lt;/code&gt; came back empty, confirming the container was fully gone.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 7 — Logging into Docker Hub
&lt;/h2&gt;

&lt;p&gt;Next was getting the image somewhere Azure could actually reach it. Logged in, and while I was at it, ran &lt;code&gt;git init&lt;/code&gt; on the project too (even though I wouldn't actually commit anything until much later) and double-checked my &lt;code&gt;.dockerignore&lt;/code&gt; set to keep &lt;code&gt;.git&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; out of the build context entirely, so neither ever ends up baked into the image.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.&lt;span class="n"&gt;dockerignore&lt;/span&gt;
.&lt;span class="n"&gt;git&lt;/span&gt;
.&lt;span class="n"&gt;env&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqlutecsga1pg11tf3nbo.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%2Fqlutecsga1pg11tf3nbo.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8 — Pushing to Docker Hub
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker push 4thman/hagital:v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watched each layer push up one by one until it finished with a digest and total size the image was now sitting on Docker Hub, ready to be pulled from anywhere, including Azure.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 9 — Logging into Azure
&lt;/h2&gt;



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

&lt;/div&gt;



&lt;p&gt;This opens a browser sign-in flow and, once authenticated, lists the subscriptions tied to the account so you can confirm which one the CLI is about to operate on.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 10 — Creating the resource group (and hitting error #1)
&lt;/h2&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; david-docker-rg &lt;span class="nt"&gt;--location&lt;/span&gt; eastus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That part succeeded instantly — a resource group is just a logical folder in Azure to keep everything for this project together. Then I tried to actually create the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-app-container &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; 4thman/hagital:v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dns-name-label&lt;/span&gt; david-hagital &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And hit the first Azure error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(MissingSubscriptionRegistration) The subscription is not registered to use namespace 'Microsoft.ContainerInstance'
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89svqvmx5whc4gexvhif.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%2F89svqvmx5whc4gexvhif.png" alt=" " width="799" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing error #1: the provider that wasn't switched on
&lt;/h2&gt;

&lt;p&gt;Every Azure subscription has to explicitly "register" the individual resource providers it wants to use before you're allowed to create resources of that type. Mine had simply never touched Container Instances before, so the &lt;code&gt;Microsoft.ContainerInstance&lt;/code&gt; namespace wasn't switched on yet. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; Microsoft.ContainerInstance
az provider show &lt;span class="nt"&gt;-n&lt;/span&gt; Microsoft.ContainerInstance &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"registrationState"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran that last command a couple of times, watching the state move from &lt;code&gt;"Registering"&lt;/code&gt; to &lt;code&gt;"Registered"&lt;/code&gt; before trying again.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Error #2: pick an OS, any OS
&lt;/h2&gt;

&lt;p&gt;With the provider registered, I ran the exact same &lt;code&gt;az container create&lt;/code&gt; command again and hit a second, different error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(InvalidOsType) The 'osType' for container group '&amp;lt;null&amp;gt;' is invalid. The value must be one of 'Windows,Linux'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It turns out &lt;code&gt;az container create&lt;/code&gt; doesn't assume an operating system for you, you have to say so explicitly, every time. Adding &lt;code&gt;--os-type Linux&lt;/code&gt; to the command fixed it immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-app-container &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; 4thman/hagital:v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 3000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dns-name-label&lt;/span&gt; david-hagital &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--os-type&lt;/span&gt; Linux &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 1
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmij3ye5onfqsp7rkfm7t.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%2Fmij3ye5onfqsp7rkfm7t.png" alt=" " width="799" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It worked — container created
&lt;/h2&gt;

&lt;p&gt;This time the command ran all the way through, returning the full container group JSON with &lt;code&gt;"provisioningState": "Succeeded"&lt;/code&gt;.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 11 — Grabbing the live URL
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-app-container &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; ipAddress.fqdn &lt;span class="se"&gt;\&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;That returned &lt;code&gt;david-hagital.eastus.azurecontainer.io&lt;/code&gt; — the friendly subdomain the &lt;code&gt;--dns-name-label&lt;/code&gt; flag from earlier had reserved.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  It's alive on the internet 🎉
&lt;/h2&gt;

&lt;p&gt;Visited &lt;code&gt;david-hagital.eastus.azurecontainer.io:3000&lt;/code&gt; in the browser, and there it was Container Vibes, live, running the Forest Uptime theme.&lt;/p&gt;

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

&lt;p&gt;Clicked "New Vibe" a few times to make sure the randomization actually worked over a real network call, not just locally and it did, cycling through themes like Golden Hour Git exactly as expected.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 12 — Checking logs straight from Azure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container logs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-app-container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pulled back the exact same startup message the app prints locally, confirming the container had booted cleanly on Azure's side too.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 13 — Tearing it down
&lt;/h2&gt;

&lt;p&gt;Azure Container Instances bills for the time a container is actually running, so once I'd confirmed everything worked, I cleaned up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az container delete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-app-container &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--yes&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fioovmcx8to4wyxwmmfh6.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%2Fioovmcx8to4wyxwmmfh6.png" alt=" " width="800" height="354"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group delete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; david-docker-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--yes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--no-wait&lt;/code&gt; just means the CLI hands control back to me immediately instead of sitting there until Azure finishes deleting everything in the background.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 14 — Finally, version control
&lt;/h2&gt;

&lt;p&gt;With the app built, tested, containerized, pushed to Docker Hub, and successfully deployed and torn down on Azure, I went back and properly set up Git since none of the actual deployment steps above needed GitHub at all (Azure was pulling the image straight from Docker Hub), but I still wanted the source code backed up and version-controlled.&lt;/p&gt;

&lt;p&gt;Added a proper &lt;code&gt;.gitignore&lt;/code&gt; to keep &lt;code&gt;node_modules&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; out of the repo, then checked status and staged everything:&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;touch&lt;/span&gt; .gitignore
git status
git add &lt;span class="nb"&gt;.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fioylw4fqefl1if1vbag2.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%2Fioylw4fqefl1if1vbag2.png" alt=" " width="799" height="414"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Hagital Docker App"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven files went into that first commit: the &lt;code&gt;.dockerignore&lt;/code&gt;, &lt;code&gt;.gitignore&lt;/code&gt;, &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, &lt;code&gt;package-lock.json&lt;/code&gt;, &lt;code&gt;public/index.html&lt;/code&gt;, and &lt;code&gt;server.js&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Created a new private repository on GitHub named &lt;strong&gt;Hagital-Docker-App&lt;/strong&gt;, with a short 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg738qkuir8t5mqro68bw.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%2Fg738qkuir8t5mqro68bw.png" alt=" " width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then connected the local repo to it and pushed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch main
git switch main
git remote add origin https://github.com/4thman/Hagital-Docker-App.git
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frdca1j0hpsff0mxldrw2.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%2Frdca1j0hpsff0mxldrw2.png" alt=" " width="800" height="418"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdl0rl67lv5nc1y89sl57.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%2Fdl0rl67lv5nc1y89sl57.png" alt=" " width="799" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And confirmed everything landed correctly by checking the repo on GitHub,  all seven files present, matching exactly what was committed locally.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  What I'd tell past-me
&lt;/h2&gt;

&lt;p&gt;None of the three errors here were Docker or Azure being difficult — they were small gaps in my own checklist. Forgetting to run &lt;code&gt;npm install&lt;/code&gt; before building meant a "successful" build that shipped a broken app, and the fix was realizing that a clean build log means nothing if the container crashes the moment it actually starts. Forgetting that a fresh Azure subscription needs its resource providers registered cost me a re-run, and forgetting to specify an OS type cost me another. None of it was complicated once I actually read the error message properly — which, honestly, is most of DevOps in a nutshell: read the log, trust what it's telling you, fix that one specific thing, and try again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub repo:&lt;/strong&gt; &lt;a href="https://github.com/4thman/Hagital-Docker-App" rel="noopener noreferrer"&gt;github.com/4thman/Hagital-Docker-App&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Hub image:&lt;/strong&gt; &lt;a href="https://hub.docker.com/repository/docker/4thman/hagital/general" rel="noopener noreferrer"&gt;hub.docker.com/repository/docker/4thman/hagital&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you enjoyed following along, drop a ❤️, leave a comment, and share this with someone. I'd also love to know: what would you add or change in this project if you were building it yourself?&lt;/p&gt;

&lt;p&gt;Until the next build keep learning, keep breaking things, keep fixing them, and keep shipping. &lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>containers</category>
      <category>github</category>
    </item>
    <item>
      <title>I Built a One-Click Azure CI/CD Pipeline with Terraform &amp; GitHub Actions From git push to a Live Website</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Sun, 20 Sep 2026 01:33:32 +0000</pubDate>
      <link>https://dev.to/4thman/i-built-a-one-click-azure-cicd-pipeline-with-terraform-github-actions-from-git-push-to-a-live-1pj9</link>
      <guid>https://dev.to/4thman/i-built-a-one-click-azure-cicd-pipeline-with-terraform-github-actions-from-git-push-to-a-live-1pj9</guid>
      <description>&lt;p&gt;A few weeks ago I built an Azure VM with Terraform from my laptop. It worked, but I still had to SSH in, install nginx and copy my website over by hand.&lt;/p&gt;

&lt;p&gt;This time I wanted all of that to happen on its own. I run &lt;code&gt;git push&lt;/code&gt; from Git Bash, and a live website shows up on a brand new VM. The website is a copy of a public GitHub project called Omnifood, and the workflow clones it onto the server for me. No portal clicking, no SSH from me.&lt;/p&gt;

&lt;p&gt;It took me a few hours and a lot of red error messages. In this post I'll show you what worked, what broke, and how I destroyed everything at the end so Azure doesn't charge me for a VM I forgot about.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(The name "Hagital" comes from my school, Hagital Tech Institute. Use your own names when you follow along.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&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%2F3k2wmhse081bhnwz9vpp.jpg" 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%2F3k2wmhse081bhnwz9vpp.jpg" alt=" " width="800" height="248"&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;git push from my laptop
   -&amp;gt; GitHub Actions wakes up
   -&amp;gt; Terraform builds the VM on Azure
   -&amp;gt; GitHub logs into the VM over SSH
   -&amp;gt; nginx is installed, then the website is cloned from GitHub over nginx's default page
   -&amp;gt; You open the IP address and see a live site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/4thman/Hagital-project.git" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.shields.io%2Fbadge%2FMy%2520GitHub%2520Repo-181717%3Fstyle%3Dflat%26logo%3Dgithub%26logoColor%3Dwhite" alt="My GitHub repo" width="117" height="20"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And there's a second path for cleanup: click &lt;strong&gt;Run workflow&lt;/strong&gt;, and a destroy job deletes everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Words I'll use
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt;: a tool where you describe your cloud stuff in files, and it builds it for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State file&lt;/strong&gt;: Terraform's notebook of what it has built so far.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service principal&lt;/strong&gt;: a robot account. GitHub uses it to log into Azure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secret&lt;/strong&gt;: a hidden value in GitHub (passwords, keys) that your workflow can use but nobody can read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow&lt;/strong&gt;: a YAML file that tells GitHub Actions what to run and when.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An Azure account and the Azure CLI&lt;/li&gt;
&lt;li&gt;Terraform installed&lt;/li&gt;
&lt;li&gt;VS Code and Git Bash (I'm on Windows)&lt;/li&gt;
&lt;li&gt;A GitHub account&lt;/li&gt;
&lt;li&gt;An SSH key pair&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My GitHub repo&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Build the Terraform files on my laptop
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Make a folder and open it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;Hagital-Project1
&lt;span class="nb"&gt;cd &lt;/span&gt;Hagital-Project1
code &lt;span class="nb"&gt;.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5xssj4k5y9aqpgtmgloq.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%2F5xssj4k5y9aqpgtmgloq.png" alt=" " width="800" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I made an empty &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 shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;main.tf
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F74cj60seuwvi2sju7sgb.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%2F74cj60seuwvi2sju7sgb.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Give Terraform a safe place for its notebook
&lt;/h3&gt;

&lt;p&gt;If the state file sits on my laptop, GitHub's computer can't see it. Then it wouldn't know what already exists and couldn't delete it later. So the state lives in an Azure storage account. I made three things with the CLI:&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="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; terraform-state-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; eastus

az storage account create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; hagitaldavid2026 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; terraform-state-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sku&lt;/span&gt; Standard_LRS

az storage container create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; tfstate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account-name&lt;/span&gt; hagitaldavid2026 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auth-mode&lt;/span&gt; login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Storage account names must be unique across all of Azure, so yours can't be &lt;code&gt;hagitaldavid2026&lt;/code&gt;. When you see &lt;code&gt;"created": true&lt;/code&gt;, you're good.&lt;/p&gt;

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

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

&lt;h3&gt;
  
  
  Provider and backend
&lt;/h3&gt;

&lt;p&gt;This goes at the top of &lt;code&gt;main.tf&lt;/code&gt;. The backend block tells Terraform where the notebook lives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&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;"~&amp;gt; 5.4.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="nx"&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="nx"&gt;subscription_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YOUR-SUBSCRIPTION-ID"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&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;resource_group_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-state-rg"&lt;/span&gt;
    &lt;span class="nx"&gt;storage_account_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hagitaldavid2026"&lt;/span&gt;
    &lt;span class="nx"&gt;container_name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-project.tfstate"&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;&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%2F9vru7g3xioeki79l71et.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%2F9vru7g3xioeki79l71et.png" alt=" " width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual resources
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Resource group, network, subnet and public IP:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&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;"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="s2"&gt;"East US"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&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;"hagital-vnet"&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;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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&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;"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.0.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_public_ip"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_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;"hagital-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;sku&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard"&lt;/span&gt;
&lt;span class="p"&gt;}&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0vk5accxje1ixssieu25.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%2F0vk5accxje1ixssieu25.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network security group.&lt;/strong&gt; Think of it as a door guard. I opened port 22 for SSH and port 80 for the website:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_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;"hagital-nsg"&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;security_rule&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;"Allow-SSH"&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;"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="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;security_rule&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;"Allow-HTTP"&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;"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="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;&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%2F37aa300vq160fyl7jb13.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%2F37aa300vq160fyl7jb13.png" alt=" " width="799" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network card, tied to the NSG:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_interface"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_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;"hagital-nic"&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;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;hagital_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;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_interface_security_group_association"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_nic_nsg"&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;hagital_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;hagital_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;&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%2Fio2gbmw9ef99e9jwifr3.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%2Fio2gbmw9ef99e9jwifr3.png" alt=" " width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Linux VM&lt;/strong&gt; (Ubuntu 22.04, small and cheap):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_linux_virtual_machine"&lt;/span&gt; &lt;span class="s2"&gt;"hagital_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;"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="s2"&gt;"Standard_B1s"&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;"adminuser"&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;hagital_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="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="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;"adminuser"&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="p"&gt;}&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ficb0d61ub12nhioxlwgp.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%2Ficb0d61ub12nhioxlwgp.png" alt=" " width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at that last line, &lt;code&gt;file("~/.ssh/id_rsa.pub")&lt;/code&gt;. It reads the key from a path on my laptop. Remember it, because it causes trouble later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Init and plan
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; set up the Azure backend and downloaded the provider. It also created a &lt;code&gt;.terraform&lt;/code&gt; folder and a &lt;code&gt;.terraform.lock.hcl&lt;/code&gt; file. The plan said &lt;strong&gt;8 to add, 0 to change, 0 to destroy&lt;/strong&gt;.&lt;/p&gt;

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

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

&lt;h3&gt;
  
  
  Variables and outputs
&lt;/h3&gt;

&lt;p&gt;Hard-coded names are fine for a first try, but I wanted them in a &lt;code&gt;variables.tf&lt;/code&gt; file:&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;touch &lt;/span&gt;variables.tf
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjwuj17yqg85kefu4sakr.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%2Fjwuj17yqg85kefu4sakr.png" alt=" " width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every value got its own variable block, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"resource_group_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;"hagital-rg"&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-rg"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I did the same for location, network name, subnet name, public IP name, NSG name, NIC name and VM name. Two variables have no default on purpose, so they must be given a value when Terraform runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"subscription_id"&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 subscription ID"&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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"public_key"&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;"admin user ssh public key"&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="p"&gt;}&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2yyytl7twa1i0p4vi77k.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%2F2yyytl7twa1i0p4vi77k.png" alt=" " width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Then I changed &lt;code&gt;main.tf&lt;/code&gt; to use them, for example &lt;code&gt;subscription_id = var.subscription_id&lt;/code&gt; and &lt;code&gt;name = var.resource_group_name&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Next, &lt;code&gt;outputs.tf&lt;/code&gt;. It prints the VM's public IP and username after the build:&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;touch &lt;/span&gt;outputs.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"hagital_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;"Public IP address of the Hagital virtual machine"&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;hagital_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip_address&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"hagital_vm_username"&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;"Username of the Hagital virtual machine"&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;hagital_vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;admin_username&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran &lt;code&gt;terraform fmt&lt;/code&gt; to tidy the files. It listed the two files it fixed.&lt;/p&gt;

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

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

&lt;p&gt;Then I ran &lt;code&gt;terraform plan&lt;/code&gt; again. Because &lt;code&gt;subscription_id&lt;/code&gt; and &lt;code&gt;public_key&lt;/code&gt; have no defaults, Terraform stopped and &lt;strong&gt;asked me to type them in&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;I typed the subscription ID and got another clean plan: 8 to add, plus my two outputs.&lt;/p&gt;

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

&lt;p&gt;Keep that typing prompt in mind. It comes back later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Let GitHub talk to Azure
&lt;/h2&gt;

&lt;p&gt;GitHub's computer needs permission to build things in my Azure account. I made a service principal (the robot account) with the Contributor role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create-for-rbac&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hagital-cicd"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;contributor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--scopes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/subscriptions/YOUR-SUBSCRIPTION-ID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--sdk-auth&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It printed a JSON block with the login details. Azure warned me that &lt;code&gt;--sdk-auth&lt;/code&gt; is deprecated. It still worked for me.&lt;/p&gt;

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

&lt;p&gt;Then I made a &lt;strong&gt;private&lt;/strong&gt; repo on GitHub with a README:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Adding the secrets
&lt;/h3&gt;

&lt;p&gt;In the repo I went to &lt;strong&gt;Settings &amp;gt; Secrets and variables &amp;gt; Actions &amp;gt; New repository secret&lt;/strong&gt; and added four secrets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret name&lt;/th&gt;
&lt;th&gt;What I pasted in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HAGITAL_AZURE_CREDENTIALS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The whole JSON from the command above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HAGITAL_SUBSCRIPTION_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;My subscription ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HAGITAL_SSH_PUBLIC_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Output of &lt;code&gt;cat ~/.ssh/id_rsa.pub&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HAGITAL_SSH_PRIVATE_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Output of &lt;code&gt;cat ~/.ssh/id_rsa&lt;/code&gt;, including the BEGIN and END lines&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you don't have a key pair yet, &lt;code&gt;ssh-keygen -t rsa -b 4096&lt;/code&gt; makes one.&lt;/p&gt;

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

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

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

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

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

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

&lt;p&gt;Last, I made an environment for the workflow to deploy into:&lt;/p&gt;

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

&lt;p&gt;One small thing I noticed later. I named it "Hagital Production" here, but my YAML says &lt;code&gt;hagital-production&lt;/code&gt;. GitHub doesn't complain, it just makes a new environment from the YAML. Keep the names identical so you don't end up with a stray one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: Git and GitHub (where the fun started)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Error 1: "not a valid object name: 'master'"
&lt;/h3&gt;

&lt;p&gt;I ran &lt;code&gt;git init&lt;/code&gt;, and Git put me on a branch called &lt;code&gt;master&lt;/code&gt;. I wanted &lt;code&gt;main&lt;/code&gt;, so I typed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git said no:&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;fatal: not a valid object name: 'master'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is simple. A branch is a pointer to a commit, and I had zero commits. There was nothing to point at. So I did &lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;git add .&lt;/code&gt; and &lt;code&gt;git commit&lt;/code&gt; first.&lt;/p&gt;

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

&lt;p&gt;After that commit, the same commands worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch main
git switch main
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv41dtxczfc0808f3iqgn.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%2Fv41dtxczfc0808f3iqgn.png" alt=" " width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Notice what was inside that first commit. It matters in Part 5.)&lt;/p&gt;

&lt;h3&gt;
  
  
  .gitignore
&lt;/h3&gt;

&lt;p&gt;I made a &lt;code&gt;.gitignore&lt;/code&gt; and added the &lt;code&gt;.terraform/&lt;/code&gt; folder to 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;touch&lt;/span&gt; .gitignore
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fooutzfvijc50c6ndvd1u.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%2Fooutzfvijc50c6ndvd1u.png" alt=" " width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the workflow file on GitHub
&lt;/h3&gt;

&lt;p&gt;To make the workflow file, I used the GitHub website. &lt;strong&gt;Add file &amp;gt; Create new file&lt;/strong&gt;. When I typed &lt;code&gt;.github/workflows/deploy.yaml&lt;/code&gt; in the name box, GitHub made both folders for me.&lt;/p&gt;

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

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

&lt;p&gt;Then I copied the repo's HTTPS link:&lt;/p&gt;

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

&lt;p&gt;And connected my laptop folder to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin https://github.com/4thman/Hagital-project.git
git pull origin main &lt;span class="nt"&gt;--allow-unrelated-histories&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--allow-unrelated-histories&lt;/code&gt; part is there because GitHub already had its own first commit (the README and my new YAML file), and my laptop had a totally separate history. Git refuses to join two histories that don't share a parent unless you say it's okay.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Part 4: Write the workflow
&lt;/h2&gt;

&lt;p&gt;The workflow has two jobs. &lt;code&gt;provision&lt;/code&gt; builds the VM. &lt;code&gt;configure&lt;/code&gt; waits for &lt;code&gt;provision&lt;/code&gt;, then sets up the website.&lt;/p&gt;

&lt;h3&gt;
  
  
  Job 1: provision
&lt;/h3&gt;



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

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;provision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hagital-production&lt;/span&gt;

    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;vm_ip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.tf_output.outputs.vm_ip }}&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;creds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.HAGITAL_AZURE_CREDENTIALS }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prepare SSH Public Key for Terraform&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p ~/.ssh&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.HAGITAL_SSH_PUBLIC_KEY }}" &amp;gt; ~/.ssh/id_rsa.pub&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Init&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Plan&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform plan \&lt;/span&gt;
            &lt;span class="s"&gt;-var="subscription_id=${{ secrets.HAGITAL_SUBSCRIPTION_ID }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-out=hagital.tfplan&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Apply&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply hagital.tfplan&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get VM Public IP&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tf_output&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "vm_ip=$(terraform output -raw hagital_vm_public_ip)" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;on:&lt;/code&gt; says run when I push to &lt;code&gt;main&lt;/code&gt;, or when I click the button myself (&lt;code&gt;workflow_dispatch&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;azure/login&lt;/code&gt; uses my robot account secret.&lt;/li&gt;
&lt;li&gt;The last step reads the VM's IP from Terraform and saves it as a job output. That's how the second job learns where the VM is.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  Job 2: configure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;configure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;provision&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prepare SSH Private Key&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p ~/.ssh&lt;/span&gt;
          &lt;span class="s"&gt;echo "${{ secrets.HAGITAL_SSH_PRIVATE_KEY }}" &amp;gt; ~/.ssh/id_rsa&lt;/span&gt;
          &lt;span class="s"&gt;chmod 600 ~/.ssh/id_rsa&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for SSH to become Available&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;for i in $(seq 1 10); do&lt;/span&gt;
            &lt;span class="s"&gt;ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa \&lt;/span&gt;
              &lt;span class="s"&gt;adminuser@${{ needs.provision.outputs.vm_ip }} echo ready &amp;amp;&amp;amp; break&lt;/span&gt;
            &lt;span class="s"&gt;echo "VM not Ready Yet, Waiting 15 Seconds..."&lt;/span&gt;
            &lt;span class="s"&gt;sleep 15&lt;/span&gt;
          &lt;span class="s"&gt;done&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install and configure the website&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa adminuser@${{ needs.provision.outputs.vm_ip }} '&lt;/span&gt;
            &lt;span class="s"&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;/span&gt;
            &lt;span class="s"&gt;sudo apt install nginx git -y&lt;/span&gt;
            &lt;span class="s"&gt;cd /var/www/html&lt;/span&gt;
            &lt;span class="s"&gt;sudo rm -f index.nginx-debian.html&lt;/span&gt;
            &lt;span class="s"&gt;sudo git clone https://github.com/MettaSurendhar/omnifood.git .&lt;/span&gt;
            &lt;span class="s"&gt;sudo chown -R www-data:www-data /var/www/html&lt;/span&gt;
            &lt;span class="s"&gt;sudo systemctl restart nginx&lt;/span&gt;
          &lt;span class="s"&gt;'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Show the live Website URL&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Hagital Website is Live and was deployed using CI/CD." &amp;gt;&amp;gt; "$GITHUB_STEP_SUMMARY"&lt;/span&gt;
          &lt;span class="s"&gt;echo "Visit: http://${{ needs.provision.outputs.vm_ip }}" &amp;gt;&amp;gt; "$GITHUB_STEP_SUMMARY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain words:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;needs: provision&lt;/code&gt; means "don't start until the VM exists."&lt;/li&gt;
&lt;li&gt;A brand new VM isn't ready for SSH right away. So the wait step tries to connect up to 10 times, 15 seconds apart.&lt;/li&gt;
&lt;li&gt;Once in, the script installs nginx and git, then swaps nginx's default page for the website. More on that just below.&lt;/li&gt;
&lt;li&gt;The last step writes the site's URL into the run summary, so I don't have to dig for the IP.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where the website comes from
&lt;/h3&gt;

&lt;p&gt;I didn't build the website myself. I used a public GitHub project called Omnifood (&lt;code&gt;https://github.com/MettaSurendhar/omnifood&lt;/code&gt;), and the workflow copies it onto the VM. Here's how that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;nginx is a web server. It shows whatever files sit in &lt;code&gt;/var/www/html&lt;/code&gt;. Right after you install it, that folder only holds nginx's default "Welcome to nginx" page (&lt;code&gt;index.nginx-debian.html&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The script deletes that page with &lt;code&gt;sudo rm -f index.nginx-debian.html&lt;/code&gt;. Git can only clone into an empty folder, so the default page has to go first.&lt;/li&gt;
&lt;li&gt;Then &lt;code&gt;sudo git clone https://github.com/MettaSurendhar/omnifood.git .&lt;/code&gt; downloads the Omnifood files straight into that folder. The dot at the end means "put the files here, don't make a new folder."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chown&lt;/code&gt; gives nginx permission to read the files, and &lt;code&gt;systemctl restart nginx&lt;/code&gt; makes it pick them up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So when someone opens the VM's IP address, they see Omnifood, not the nginx welcome page. Want to show your own site? Change that one clone URL.&lt;/p&gt;

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

&lt;p&gt;Back in VS Code, I saved the file and committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"wrote the Hagital deploy &amp;amp; configure script on the deploy.yaml file"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb8c5umlo8qdejq7r5v88.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%2Fb8c5umlo8qdejq7r5v88.png" alt=" " width="799" height="319"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Push day (three errors in a row)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Error 2: GitHub rejected my push
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&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="gp"&gt;remote: error: File .terraform/providers/.../terraform-provider-azurerm_v5.4.0_x5.exe is 219.00 MB;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;this exceeds GitHub's file size limit of 100.00 MB
remote: error: GH001: Large files detected.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider file that Terraform downloads is 219 MB, and GitHub's limit is 100 MB. I had put &lt;code&gt;.terraform/&lt;/code&gt; in &lt;code&gt;.gitignore&lt;/code&gt;, so what happened?&lt;/p&gt;

&lt;p&gt;Remember my very first commit in Part 3? I ran &lt;code&gt;git add .&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; I made the &lt;code&gt;.gitignore&lt;/code&gt;. Git had already saved that giant file. A &lt;code&gt;.gitignore&lt;/code&gt; only stops Git from picking up new files. It does nothing about files that were already committed.&lt;/p&gt;

&lt;p&gt;I added the state files to &lt;code&gt;.gitignore&lt;/code&gt; too, then committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.terraform/
*.tfstate
*tfstate.*
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81us3itxvygw21asuvgt.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%2F81us3itxvygw21asuvgt.png" alt=" " width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The push failed again, with the same error. Deleting the file in a new commit doesn't help, because the old commit still carries it. The big file lives in my history.&lt;/p&gt;

&lt;p&gt;While I was fixing things, I also changed the VM's key line in &lt;code&gt;main.tf&lt;/code&gt;. The path &lt;code&gt;~/.ssh/id_rsa.pub&lt;/code&gt; only exists on my laptop, so I switched to the variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;public_key&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_key&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faw9nthdz8lfslimd9tjq.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%2Faw9nthdz8lfslimd9tjq.png" alt=" " width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix that worked:&lt;/strong&gt; this repo was brand new and had nothing worth saving, so I deleted Git's history and started over:&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;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; .git
git init
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"removed git and readded git then re staged an commit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time &lt;code&gt;.terraform&lt;/code&gt; wasn't in the commit. Seven small files went in.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Don't do this on a real project.&lt;/strong&gt; &lt;code&gt;rm -rf .git&lt;/code&gt; wipes your whole history. I could do it because I had exactly two commits and both were disposable.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Error 3: the merge conflict
&lt;/h3&gt;

&lt;p&gt;Same trick as before: &lt;code&gt;git branch main&lt;/code&gt; failed until after the first commit. Once I had a commit, I made &lt;code&gt;main&lt;/code&gt; and tried to join my history with GitHub's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge origin/main &lt;span class="nt"&gt;--allow-unrelated-histories&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;Automatic merge failed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fix conflicts and &lt;span class="k"&gt;then &lt;/span&gt;commit the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sides had a &lt;code&gt;deploy.yaml&lt;/code&gt;, and Git couldn't pick one. My prompt changed to &lt;code&gt;(main|MERGING)&lt;/code&gt;. I wanted my local version, so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;--ours&lt;/span&gt; .github/workflows/deploy.yaml
git add .github/workflows/deploy.yaml
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Merge GitHub main into local main"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt went back to &lt;code&gt;(main)&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;)&lt;/p&gt;

&lt;p&gt;Then the push finally went through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbn7g5f87qvs9lrgfv6iq.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%2Fbn7g5f87qvs9lrgfv6iq.png" alt=" " width="799" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Your turn:&lt;/em&gt; look at your own repo's file list right now. Is anything in there that shouldn't be? Check before you push, not after.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 6: Watching GitHub Actions do the work
&lt;/h2&gt;

&lt;p&gt;I opened the &lt;strong&gt;Actions&lt;/strong&gt; tab and my workflow was already running.&lt;/p&gt;

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

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

&lt;h3&gt;
  
  
  Error 4: the workflow that waited forever
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;provision&lt;/code&gt; job sat on &lt;strong&gt;Terraform Plan&lt;/strong&gt;. Two minutes. Five minutes. Twenty minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Quick guess before you scroll: what do you think it was waiting for?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Remember the prompt from Part 1, where Terraform asked me to type the public key? That's what happened. My &lt;code&gt;main.tf&lt;/code&gt; now used &lt;code&gt;var.public_key&lt;/code&gt;, but my workflow only passed &lt;code&gt;subscription_id&lt;/code&gt;. So Terraform asked for the key and waited for someone to type it. Nobody types in GitHub Actions. It just sat there until the run gave up after 19 minutes and 50 seconds.&lt;/p&gt;

&lt;p&gt;The fix is one extra line in the plan step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Plan&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform plan \&lt;/span&gt;
            &lt;span class="s"&gt;-var="subscription_id=${{ secrets.HAGITAL_SUBSCRIPTION_ID }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-var="public_key=${{ secrets.HAGITAL_SSH_PUBLIC_KEY }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-out=hagital.tfplan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also changed the apply line to &lt;code&gt;terraform apply -auto-approve hagital.tfplan&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 5: my commit message broke Bash
&lt;/h3&gt;

&lt;p&gt;When I committed the fix, I wrote a commit message that had &lt;code&gt;${{ secrets... }}&lt;/code&gt; inside double quotes. Bash tried to read it as a command and said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash: ... : bad substitution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing got committed. I wrote a normal message instead (&lt;code&gt;Add public key variable to Terraform plan&lt;/code&gt;) and it worked. Keep commit messages plain.&lt;/p&gt;

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

&lt;p&gt;Then I pushed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push origin main
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72jjnfcdev3xq5zohe7c.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%2F72jjnfcdev3xq5zohe7c.png" alt=" " width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  It works
&lt;/h3&gt;

&lt;p&gt;That &lt;code&gt;git push&lt;/code&gt; is the trigger. The &lt;code&gt;on: push&lt;/code&gt; lines in the YAML tell GitHub to start the workflow every time new code lands on &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A new run started. &lt;code&gt;provision&lt;/code&gt; finished in 35 seconds. &lt;code&gt;configure&lt;/code&gt; began.&lt;/p&gt;

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

&lt;p&gt;The "Install and configure the website" step took a while, because apt was updating a whole Ubuntu 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwwcmeequezlinny5p8s7.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%2Fwwcmeequezlinny5p8s7.png" alt=" " width="799" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The log gets long and ends with warnings about a pending kernel upgrade and services to restart. That's normal. Ubuntu is saying "a reboot would be nice." The job still finished green.&lt;/p&gt;

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

&lt;p&gt;Here's the run list. Run #4 is green (4 minutes 19 seconds). Run #2 is the stuck one from before.&lt;/p&gt;

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

&lt;p&gt;And here's my favourite part, the summary page, with the link my last step wrote:&lt;/p&gt;

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

&lt;p&gt;I clicked the IP address. No "Welcome to nginx" page. It was the Omnifood website, cloned from GitHub onto a server that didn't exist 5 minutes earlier:&lt;/p&gt;

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

&lt;p&gt;I did nothing by hand except push code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 7: Delete everything with one click
&lt;/h2&gt;

&lt;p&gt;A VM that runs forever keeps costing money. I wanted a way to tear it all down from GitHub, so I added a third job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;destroy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'workflow_dispatch'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hagital-production&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure Login&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;creds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.HAGITAL_AZURE_CREDENTIALS }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Terraform&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hashicorp/setup-terraform@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Init&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Terraform Destroy&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;terraform destroy \&lt;/span&gt;
            &lt;span class="s"&gt;-auto-approve \&lt;/span&gt;
            &lt;span class="s"&gt;-var="subscription_id=${{ secrets.HAGITAL_SUBSCRIPTION_ID }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-var="public_key=${{ secrets.HAGITAL_SSH_PUBLIC_KEY }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;if&lt;/code&gt; line is the switch. This job only runs when I click the button (&lt;code&gt;workflow_dispatch&lt;/code&gt;), never on a normal push. So my code pushes can't accidentally delete my server.&lt;/p&gt;

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

&lt;p&gt;But then the other two jobs needed the opposite switch. Without it, clicking the button would try to build the VM and delete it in the same run. So I added this line to both &lt;code&gt;provision&lt;/code&gt; and &lt;code&gt;configure&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'push'&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjspsinbp9n4xk42ixgfl.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%2Fjspsinbp9n4xk42ixgfl.png" alt=" " width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the usual routine, with commit message "Added Manual Terraform Destroy Workflow":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status
git add .github/workflows/deploy.yaml
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Added Manual Terraform Destroy Workflow"&lt;/span&gt;
git push origin main
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19hjhqw605zgzqc5gm36.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%2F19hjhqw605zgzqc5gm36.png" alt=" " width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That push counts as a normal push, so &lt;code&gt;provision&lt;/code&gt; and &lt;code&gt;configure&lt;/code&gt; ran again and &lt;code&gt;destroy&lt;/code&gt; was skipped (the grey circle). The site stayed up.&lt;/p&gt;

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

&lt;p&gt;Proof the site was still alive:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Pulling the plug
&lt;/h3&gt;

&lt;p&gt;Then I went to &lt;strong&gt;Actions &amp;gt; Hagital VM Deploy &amp;gt; Run workflow&lt;/strong&gt;, picked the &lt;code&gt;main&lt;/code&gt; branch, and clicked the green button.&lt;/p&gt;

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

&lt;p&gt;This time &lt;code&gt;provision&lt;/code&gt; and &lt;code&gt;configure&lt;/code&gt; were skipped and only &lt;code&gt;destroy&lt;/code&gt; ran. The log ends with the line I wanted to see:&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;Destroy complete! Resources: 8 destroyed.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight things built, eight things gone.&lt;/p&gt;

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

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

&lt;p&gt;Why does this work on a fresh GitHub computer that has never seen my VM? Because of the remote state from Part 1. Terraform reads its notebook from Azure storage, sees what it built, and deletes exactly that.&lt;/p&gt;

&lt;p&gt;One thing is still there. The state storage account (&lt;code&gt;terraform-state-rg&lt;/code&gt;) was made by hand with the CLI, not by this Terraform code. If you want a zero bill, delete it yourself when you're done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Every mistake in one place
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;What went wrong&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git branch main&lt;/code&gt; said "not a valid object name"&lt;/td&gt;
&lt;td&gt;No commits yet, so the branch had nothing to point at&lt;/td&gt;
&lt;td&gt;Make a commit first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;GitHub rejected my push (219 MB file)&lt;/td&gt;
&lt;td&gt;I ran &lt;code&gt;git add .&lt;/code&gt; before making &lt;code&gt;.gitignore&lt;/code&gt;, so the provider file was already in history&lt;/td&gt;
&lt;td&gt;Create &lt;code&gt;.gitignore&lt;/code&gt; first; I restarted the repo with &lt;code&gt;rm -rf .git&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Merge failed on &lt;code&gt;deploy.yaml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;GitHub and my laptop each had a different &lt;code&gt;deploy.yaml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git checkout --ours&lt;/code&gt;, then add and commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Terraform plan hung for 19+ minutes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;public_key&lt;/code&gt; had no value and nobody can type in CI&lt;/td&gt;
&lt;td&gt;Pass it with &lt;code&gt;-var&lt;/code&gt; from a secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bad substitution&lt;/code&gt; on commit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;${{ }}&lt;/code&gt; inside a double-quoted commit message&lt;/td&gt;
&lt;td&gt;Use a plain message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Things I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make &lt;code&gt;.gitignore&lt;/code&gt; before the first &lt;code&gt;git add .&lt;/code&gt;&lt;/strong&gt; and list &lt;code&gt;.terraform/&lt;/code&gt; in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't leave SSH open to the world.&lt;/strong&gt; My NSG allows port 22 from anywhere. I tried limiting SSH to GitHub's IP ranges at one point and backed it out, so this lab stays open. That's okay for something I destroy in a day. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;And that’s it from a simple git push to a live website running on Azure, with Terraform and GitHub Actions handling the heavy lifting.&lt;/p&gt;

&lt;p&gt;The biggest lesson for me wasn’t just learning Terraform or GitHub Actions. It was learning how to troubleshoot, fix mistakes, and keep going when things don’t work the first time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git push → GitHub Actions → Terraform → Azure VM → Live Website
                                                          ↓
                                                One-click Destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📦 Full source code at my github repo: github.com/4thman/Hagital-project&lt;/p&gt;

&lt;p&gt;Now I want to hear from you:&lt;/p&gt;

&lt;p&gt;Where should the journey go next Docker containers, secure HTTPS, custom domains, OIDC authentication, or a fully automated production CI/CD pipeline?&lt;/p&gt;

&lt;p&gt;And most importantly, what’s the biggest DevOps mistake you’ve ever made? 😄&lt;/p&gt;

&lt;p&gt;Drop it in the comments. Let’s learn from each other!&lt;/p&gt;

&lt;p&gt;One git push at a time. 🚀&lt;/p&gt;

</description>
      <category>azure</category>
      <category>terraform</category>
      <category>githubactions</category>
      <category>devops</category>
    </item>
    <item>
      <title>Three Days, Four Errors, One Working Azure VM: Deploying with Terraform and Remote State</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Wed, 09 Sep 2026 12:31:36 +0000</pubDate>
      <link>https://dev.to/4thman/three-days-four-errors-one-working-azure-vm-deploying-with-terraform-and-remote-state-2kbp</link>
      <guid>https://dev.to/4thman/three-days-four-errors-one-working-azure-vm-deploying-with-terraform-and-remote-state-2kbp</guid>
      <description>&lt;p&gt;This was my first real hands-on Terraform assignment: deploy a working Azure VM, wire up a remote backend for the state file, and document the whole process. On paper it looked like a weekend task. It took me close to three days, mostly because I kept running into errors I'd never seen before and had to actually understand each one instead of working around it.&lt;/p&gt;

&lt;p&gt;This post walks through what I built, the order I built it in, the four errors that stopped me at different points, how I got past each one, and the proof that it all worked in the end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/4thman/cloud-cohort" rel="noopener noreferrer"&gt;github.com/4thman/cloud-cohort&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;At a glance, the stack looks 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpd2stn6jfaono69ios1w.jpg" 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%2Fpd2stn6jfaono69ios1w.jpg" alt=" " width="600" height="482"&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;Internet
   │
   ▼
Public IP (cohort-pip)
   │
   ▼
NIC (cohort-nic)  ──secured by──  NSG (cohort-nsg: allow 22, allow 80)
   │
   ▼
VM: cohort-vm (Ubuntu 18.04 LTS, Standard_B1s)
   │
   ▼
Subnet (cohort-subnet, 10.0.0.0/24)
   │
   ▼
VNet (cohort-vnet, 10.0.0.0/16)
   │
   ▼
Resource Group: cohort-rg (East US)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything above lives in Terraform. The state file for all of it lives in an Azure Storage Account, not on my laptop. That was the part of the brief I hadn't done before, and it ended up being the most useful thing I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the stage
&lt;/h2&gt;

&lt;p&gt;Nothing complicated here, just a folder and an editor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;Downloads
&lt;span class="nb"&gt;mkdir &lt;/span&gt;cloud-cohort
&lt;span class="nb"&gt;cd &lt;/span&gt;cloud-cohort
code &lt;span class="nb"&gt;.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0v1qom1f95i0ack3vgsr.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%2F0v1qom1f95i0ack3vgsr.png" alt=" " width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;First file: &lt;code&gt;main.tf&lt;/code&gt;, empty.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Wiring up the provider
&lt;/h2&gt;

&lt;p&gt;Every Terraform project starts with telling it which provider to talk to and which version to pin. I went with &lt;code&gt;azurerm&lt;/code&gt; version &lt;code&gt;~&amp;gt; 5.4.0&lt;/code&gt; since am working on Azure Platform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&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;"~&amp;gt; 5.4.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="nx"&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="nx"&gt;subscription_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"&lt;/span&gt;
&lt;span class="p"&gt;}&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsyepx2g4pyq3vwnmuk02.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%2Fsyepx2g4pyq3vwnmuk02.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then the command that downloads the provider plugin and sets everything up:&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;&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%2Fku1hnkq53u5nhmo5v47h.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%2Fku1hnkq53u5nhmo5v47h.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the network layer
&lt;/h2&gt;

&lt;p&gt;I built this resource by resource, running &lt;code&gt;terraform plan&lt;/code&gt; after almost every block just to see what was about to happen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a resource group&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_resource_group"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-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;"cohort-rg"&lt;/span&gt;
  &lt;span class="nx"&gt;location&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="c1"&gt;# Create a virtual network&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_network"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-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;"cohort-vnet"&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;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;cohort-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;cohort-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="c1"&gt;# Create a subnet&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-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;"cohort-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;cohort-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;cohort-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.0.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;&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%2F5pm759eb5d9z9ga8ey7r.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%2F5pm759eb5d9z9ga8ey7r.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Locking it down with an NSG
&lt;/h2&gt;

&lt;p&gt;I configured a Network Security Group (NSG) to control inbound traffic. I added two essential rules: SSH (port 22) to allow secure remote administration and HTTP (port 80) to enable web-server access for testing and demonstration purposes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a network security group &amp;amp; NSG rules&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-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;"cohort-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;cohort-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;cohort-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;security_rule&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;"AllowSSH"&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;1000&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="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;security_rule&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;"AllowHTTP"&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;1001&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="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;&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%2Fga6k79ars60pbtnrk9mm.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%2Fga6k79ars60pbtnrk9mm.png" alt=" " width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The NIC and a formatting habit
&lt;/h2&gt;

&lt;p&gt;The network interface is what actually connects the VM to the subnet and, through the public IP, to the internet. I also started running &lt;code&gt;terraform fmt&lt;/code&gt; before every &lt;code&gt;plan&lt;/code&gt; around here. It costs nothing, and it catches the indentation mess before it becomes a diff you have to think about.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a network interface&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_interface"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-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;"cohort-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;cohort-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;cohort-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;cohort-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;cohort-pip&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;span class="c1"&gt;# Associate the NIC with the NSG&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_network_interface_security_group_association"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-nic-nsg"&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;cohort-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;cohort-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;&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%2Fdnelr9vod7p9r1283wfg.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%2Fdnelr9vod7p9r1283wfg.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual VM
&lt;/h2&gt;

&lt;p&gt;This is the main resource that everything else is built around: an Ubuntu 18.04 LTS virtual machine using the Standard_B1s size and authenticated with an SSH key instead of a password. I chose Ubuntu rather than a Microsoft Windows Server VM because Ubuntu is lightweight, open-source, and commonly used for hosting web servers and working with Linux-based cloud environments. It also provides a straightforward environment for installing and managing web-server software through the command line, making it a suitable choice for this learning project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a virtual machine&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_linux_virtual_machine"&lt;/span&gt; &lt;span class="s2"&gt;"cohort-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;"cohort-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;cohort-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;cohort-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="s2"&gt;"Standard_B1s"&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;"cohortuser1"&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;cohort-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="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;"cohortuser1"&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;"UbuntuServer"&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;"18.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;&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%2F0a1tsb78ecljsldswthj.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%2F0a1tsb78ecljsldswthj.png" alt=" " width="799" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And two outputs so I don't have to go hunting through the Azure portal every time I need the IP or the username:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Cleaning up: variables and outputs
&lt;/h2&gt;

&lt;p&gt;At this point, everything was hardcoded directly into main.tf. While this works, it makes the configuration harder to maintain, reuse, and modify for different environments. To improve the structure, I moved the repeated and changeable values into variables.tf and placed the two outputs into a separate outputs.tf file.&lt;/p&gt;

&lt;p&gt;I created variables.tf to make the Terraform configuration more flexible and reusable. Instead of changing values directly inside main.tf, I can define them as variables and provide different values when needed. This makes it easier to reuse the same configuration for another environment without modifying the main infrastructure code.&lt;/p&gt;

&lt;p&gt;I created outputs.tf to keep the information Terraform needs to display after deployment separate from the resource configuration. This makes the project more organised and allows important details, such as the VM's public IP address, to be easily accessed after the infrastructure has been 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz631fdi1empvgim49nub.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%2Fz631fdi1empvgim49nub.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

&lt;p&gt;&lt;code&gt;main.tf&lt;/code&gt; then got rewritten to reference &lt;code&gt;var.*&lt;/code&gt; instead of literal strings:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;location = "East US"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;location = var.location&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;name = "cohort-rg"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;name = var.resource_group_name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;address_prefixes = ["10.0.0.0/24"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;still literal (left this one alone on purpose)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frtth1ldxhublqpqe2lb5.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%2Frtth1ldxhublqpqe2lb5.png" alt=" " width="799" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One small mistake I made was in my variables.tf file. I used the description field to store the default value instead of explaining what the variable is for. For example, I used description = "cohort-rg" instead of description = "Name of the resource group".&lt;/p&gt;

&lt;p&gt;This did not cause any problems because Terraform still works with it, but it makes the file less clear and harder to understand later. It is a small mistake, but it is something I would correct to keep the code clean and easy to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  A duplicate-output gotcha
&lt;/h2&gt;

&lt;p&gt;Ran &lt;code&gt;terraform fmt&lt;/code&gt; and &lt;code&gt;terraform plan&lt;/code&gt; right after the refactor, expecting a clean run. Instead, Terraform stopped me with this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Error: Duplicate output definition&lt;br&gt;
An output named "vm_username" was already defined at main.tf:134,1-21. Output names must be unique within a module.&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%2F8a8cphds4mx53akr54j3.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%2F8a8cphds4mx53akr54j3.png" alt=" " width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once I looked closer it made sense: I'd created &lt;code&gt;outputs.tf&lt;/code&gt; with the two outputs, but never went back and deleted the original output blocks still sitting at the bottom of &lt;code&gt;main.tf&lt;/code&gt;. Terraform was looking at two files both trying to define &lt;code&gt;vm_username&lt;/code&gt;, and correctly refused to guess which one I meant. Deleted the leftover blocks from &lt;code&gt;main.tf&lt;/code&gt;, re-ran &lt;code&gt;plan&lt;/code&gt;, and got a clean 8-to-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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq55ee6fgj5kwrutqcb9e.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%2Fq55ee6fgj5kwrutqcb9e.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Running terraform apply
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ssh_Public_key&lt;/code&gt; and &lt;code&gt;subscription_id&lt;/code&gt; don't have defaults in &lt;code&gt;variables.tf&lt;/code&gt; on purpose. They're marked &lt;code&gt;sensitive&lt;/code&gt;, and I didn't want them sitting in a file that might end up on GitHub. Terraform prompts for them at apply time instead.&lt;br&gt;
&lt;/p&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;&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%2Ft68qvvw0wh8xhlunmzcc.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%2Ft68qvvw0wh8xhlunmzcc.png" alt=" " width="800" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Configuring the remote backend
&lt;/h2&gt;

&lt;p&gt;This was the part of the brief I'd never actually done before, so it got its own careful pass.&lt;/p&gt;

&lt;p&gt;The idea: instead of &lt;code&gt;terraform.tfstate&lt;/code&gt; sitting on my machine, where it can get lost, overwritten, or just never seen by anyone else on a team, it lives in an Azure Storage Account, and Terraform reads and writes it there on every command.&lt;/p&gt;

&lt;p&gt;First, log in and create the storage account and a container inside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az login
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft5y7e5lhlipvv7vwnoa7.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%2Ft5y7e5lhlipvv7vwnoa7.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cohorttfstate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--resource-group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cohort-rg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;eastus&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--sku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Standard_LRS&lt;/span&gt;&lt;span class="w"&gt;
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fek7qm89ziomvaz4ejyur.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%2Fek7qm89ziomvaz4ejyur.png" alt=" " width="799" height="395"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tfstate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--account-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cohorttfstate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--auth-mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="w"&gt;
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb8x1d6fokgsosuut4140.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%2Fb8x1d6fokgsosuut4140.png" alt=" " width="800" height="392"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then tell Terraform where to find it, right inside the &lt;code&gt;terraform {}&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&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;"~&amp;gt; 5.4.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Backend configuration for storing the Terraform state in Azure Blob Storage&lt;/span&gt;
  &lt;span class="nx"&gt;backend&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;resource_group_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohort-rg"&lt;/span&gt;
    &lt;span class="nx"&gt;storage_account_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cohorttfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;container_name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform.tfstate"&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;&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%2Fw353n4zi6xmomj30p0ud.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%2Fw353n4zi6xmomj30p0ud.png" alt=" " width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;terraform init&lt;/code&gt; again picks up the new backend and asks whether to migrate the state that's currently sitting locally:&lt;/p&gt;

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

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

&lt;p&gt;From this point forward, every &lt;code&gt;terraform plan&lt;/code&gt; reads the state from &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; instead of a local file. This also means the Terraform state file, which contains information such as my resource IDs, is no longer stored on my laptop.&lt;/p&gt;

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

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

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

&lt;p&gt;"No changes" here is the whole point: it means the remote state and the real infrastructure agree with each other, from somewhere that isn't tied to my one laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting onto the box
&lt;/h2&gt;

&lt;p&gt;Time to prove there's an actual, usable server behind all this config.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh cohortuser1@4.157.245.198
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopxi50plqgr8c0py0l15.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%2Fopxi50plqgr8c0py0l15.png" alt=" " width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Then I tried to update and upgrade in one line and typed &lt;code&gt;&amp;amp;&lt;/code&gt; where I meant &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&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;sudo &lt;/span&gt;apt upgrade &amp;amp; update &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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh5p6s2vyklzkbk2e37bb.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%2Fh5p6s2vyklzkbk2e37bb.png" alt=" " width="799" height="538"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;amp;&lt;/code&gt; backgrounds the first command and immediately tries to run &lt;code&gt;update&lt;/code&gt; as its own program, which doesn't exist as a standalone command, hence &lt;code&gt;Command 'update' not found&lt;/code&gt;. &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; chains two commands so the second only runs after the first succeeds: one character, very different behaviour. Ran it properly afterward:&lt;/p&gt;

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

&lt;p&gt;Then installed the two things I actually needed: nginx to prove a web server can be reached from outside, and git to push the code.&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;nginx &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git &lt;span class="nt"&gt;-y&lt;/span&gt;
git &lt;span class="nt"&gt;--version&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcv18y5c45uupvki0ttru.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%2Fcv18y5c45uupvki0ttru.png" alt=" " width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;And in a browser, hitting the VM's public IP directly:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Proof in the portal
&lt;/h2&gt;

&lt;p&gt;Terraform reported that it successfully created &lt;strong&gt;eight resources&lt;/strong&gt;, and I verified this in the &lt;strong&gt;Azure portal&lt;/strong&gt;, which also shows the same resources. This confirmed that the Terraform configuration was applied successfully and that the infrastructure was created as expected.&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Tearing it down
&lt;/h2&gt;

&lt;p&gt;Once everything was verified and screenshotted, I destroyed the whole stack. It's a learning VM, not a production workload, and there's no reason to let a &lt;code&gt;Standard_B1s&lt;/code&gt; keep billing for the rest of the month for something I've already proven works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform destroy
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3fyq605vtgopihdi27hi.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%2F3fyq605vtgopihdi27hi.png" alt=" " width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the state lived remotely, destroying it was as clean as creating it. Terraform read the real state from Blob Storage, worked out exactly what it had made, and took it all back down in one pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting this into GitHub
&lt;/h2&gt;

&lt;p&gt;This is the section where I learned more about Git than about Terraform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl05347427oaptglui4ew.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%2Fl05347427oaptglui4ew.png" alt=" " width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I tried &lt;code&gt;git branch main&lt;/code&gt; before making a single commit, which fails because there's no commit yet for a branch to point at (&lt;code&gt;fatal: not a valid object name 'master'&lt;/code&gt;). Staged everything instead:&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"first commit on terraform cloud cohort from master branch"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F763t1nd38boyg7yv540l.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%2F763t1nd38boyg7yv540l.png" alt=" " width="799" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Renamed the default branch from &lt;code&gt;master&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt; to match what GitHub expects by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch main
git switch main
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbkvve2marllpfva53hym.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%2Fbkvve2marllpfva53hym.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Created the repo on GitHub, private, with a README ticked on (a decision that comes back to bite me two steps from now):&lt;/p&gt;

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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin https://github.com/4thman/cloud-cohort.git
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm3nku1rd93zjzoo4mc3x.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%2Fm3nku1rd93zjzoo4mc3x.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first push got rejected:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;remote: error: File .terraform/providers/registry.terraform.io/hashicorp/azurerm/5.4.0/windows_amd64/terraform-provider-azurerm_v5.4.0_x5.exe is 219.00 MB; this exceeds GitHub's file size limit of 100.00 MB&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%2F9moytit1h8vojvlm4qhy.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%2F9moytit1h8vojvlm4qhy.png" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Honestly, I didn't know GitHub even had a 100MB file size limit until this error told me. I'd just run &lt;code&gt;git add .&lt;/code&gt; and pushed everything in the folder without thinking about what was actually sitting inside &lt;code&gt;.terraform/&lt;/code&gt;. That folder is a local cache of provider plugins, genuinely huge (that one provider binary alone was 219MB), and it's something nobody needs in version control anyway since &lt;code&gt;terraform init&lt;/code&gt; rebuilds it from scratch on any machine. So it needed to go in &lt;code&gt;.gitignore&lt;/code&gt; instead of getting committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.&lt;span class="n"&gt;terraform&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F03kqtcq8z0p4iopvjfq0.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%2F03kqtcq8z0p4iopvjfq0.png" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Staged it, committed it, and pushed again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"added .terraform folder to .gitignore due to push error"&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rejected a second time, this time with &lt;code&gt;[rejected] main -&amp;gt; main (fetch first)&lt;/code&gt;, because the README GitHub generated when I created the repo existed as a commit on the remote that my local history knew nothing about. Two histories, no shared ancestor Git could reconcile on its own.&lt;/p&gt;

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

&lt;p&gt;Pulled the remote's history in, merged it with mine, and pushed one more time:&lt;/p&gt;

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

&lt;p&gt;And the repo, live, with all three commits and the actual project files:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/4thman/cloud-cohort" rel="noopener noreferrer"&gt;github.com/4thman/cloud-cohort&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Add &lt;strong&gt;.gitignore&lt;/strong&gt; before the first commit, rather than after the push. When I created the GitHub repository, I left the &lt;strong&gt;ADD README&lt;/strong&gt; option enabled, which meant the local and remote repositories had different histories. I fixed this by running git pull to bring the README into my local repository and merge the changes before pushing my files.&lt;/p&gt;

&lt;p&gt;Write proper descriptions in** variables.tf** instead of putting the default value in the description field. I should also add the &lt;strong&gt;.tfstate&lt;/strong&gt; files to .gitignore, not just &lt;strong&gt;.terraform/&lt;/strong&gt;. My state file ended up in the first commit, and although there was nothing sensitive in this particular file, Terraform state files can contain sensitive information and should not normally be committed to GitHub.&lt;/p&gt;

&lt;p&gt;None of these broke the deployment. All of them are the kind of thing you only really learn by hitting them once.&lt;/p&gt;




&lt;p&gt;If you made it this far, thanks for reading! 🙌 I’d love to hear from you what’s one Terraform, Azure, or Git mistake you’ve made while learning, and what did it teach you? Drop it in the comments; your experience might save someone else a few hours of frustration. If you found this walkthrough useful, like ❤️, comment 💬, and share 🔄 it with someone else learning Terraform or Azure. Let’s learn from the mistakes, not hide them! &lt;/p&gt;

</description>
      <category>terraform</category>
      <category>azure</category>
      <category>git</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>From Zero to Auto-Scaling Hero: Deploying a VMSS on Azure 🚀</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Wed, 08 Jul 2026 00:03:12 +0000</pubDate>
      <link>https://dev.to/4thman/from-zero-to-auto-scaling-hero-deploying-a-vmss-on-azure-48g5</link>
      <guid>https://dev.to/4thman/from-zero-to-auto-scaling-hero-deploying-a-vmss-on-azure-48g5</guid>
      <description>&lt;h1&gt;
  
  
  From Zero to Auto-Scaling Hero: Deploying a VMSS on Azure 🚀
&lt;/h1&gt;

&lt;p&gt;Ever wondered how big websites survive a sudden rush of visitors without crashing? The secret is &lt;strong&gt;auto-scaling&lt;/strong&gt; — the server army quietly grows when traffic spikes, and shrinks back down when things calm down, so nobody pays for computers they don't need.&lt;/p&gt;

&lt;p&gt;In this post, I'm walking you through &lt;strong&gt;exactly&lt;/strong&gt; how I built that on Microsoft Azure — from a completely empty subscription to a live webpage served by &lt;strong&gt;5 auto-scaling virtual machines&lt;/strong&gt; sitting behind a load balancer. &lt;/p&gt;

&lt;p&gt;By the end, you'll have your own mini traffic-proof web app running in the cloud, and you'll know how to tear it all down without leaving a single unpaid bill behind.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;This is part of a series!&lt;/strong&gt; I've already covered the foundational pieces in earlier posts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔗 &lt;a href="https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4"&gt;https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4&lt;/a&gt; — The "land and street" this project is built on&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔗 &lt;a href="https://dev.to/4thman/i-built-my-first-azure-vm-from-scratch-and-hosted-a-live-website-1ob0"&gt;https://dev.to/4thman/i-built-my-first-azure-vm-from-scratch-and-hosted-a-live-website-1ob0&lt;/a&gt; — The basics of a single server, before we multiply it into a scale set&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of the early steps below feel unfamiliar, those posts break them down in full detail. Here, I'll recap them quickly and then dive deep into the part that's new: &lt;strong&gt;auto-scaling with VMSS&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧩 What We're Actually Building
&lt;/h2&gt;

&lt;p&gt;Think of it like building a house, street by street:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resource Group&lt;/strong&gt; – an empty folder that holds everything we create (so we can delete it all in one click later)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Network (VNet) + Subnet&lt;/strong&gt; – the private street our servers will live on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Machine Scale Set (VMSS)&lt;/strong&gt; – a &lt;em&gt;group&lt;/em&gt; of identical servers that can grow or shrink automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load Balancer&lt;/strong&gt; – the traffic cop that spreads visitors evenly across all the servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tiny Apache web page&lt;/strong&gt; – so we have something to actually see working in the browser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the flow in one picture (in your head, for now): &lt;strong&gt;Resource Group → Network → Subnet → Scale Set → Load Balancer → 🌍 Live Website&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Before You Start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An active Azure subscription (Pay-As-You-Go or free trial both work)&lt;/li&gt;
&lt;li&gt;Nothing else! Azure gives you everything else through the portal — no local software needed for this tutorial&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;(Optional but helpful)&lt;/em&gt; A quick read of my earlier posts on &lt;a href="https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4"&gt;https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4&lt;/a&gt; — This post builds directly on top of those concepts&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Say Hello to the Azure Portal 👋
&lt;/h2&gt;

&lt;p&gt;Everything starts here — &lt;a href="https://portal.azure.com" rel="noopener noreferrer"&gt;portal.azure.com&lt;/a&gt;. This is mission control for every Azure resource you'll ever 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fepsdl1idimlczddzxsoq.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%2Fepsdl1idimlczddzxsoq.png" alt=" " width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Azure homepage — our home base for the whole build.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Quick Recap — Resource Group, VNet &amp;amp; Subnet 📁🛣️🏘️
&lt;/h2&gt;

&lt;p&gt;I've already walked through this part step-by-step in an earlier post, so here's the short version — &lt;strong&gt;check out Setting up a Resource Group, Virtual Network &amp;amp; Subnet on Azure for the full click-by-click guide.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what happened, in a nutshell:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Created a Resource Group&lt;/strong&gt; called &lt;strong&gt;Smyles-RG&lt;/strong&gt; — the folder that holds everything in this project, so it can all be deleted in one click later.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk1lz4devqnd8px7uof9m.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%2Fk1lz4devqnd8px7uof9m.png" alt=" " width="800" height="330"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Built a Virtual Network&lt;/strong&gt; called &lt;strong&gt;Smyles-VN&lt;/strong&gt; inside that resource group — a private, isolated network, like a gated estate for our resources.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frqfnh0jb0jd7nw5tztl3.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%2Frqfnh0jb0jd7nw5tztl3.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Carved out a Subnet&lt;/strong&gt; called &lt;strong&gt;Smyles-Subnet&lt;/strong&gt; inside the VNet — a smaller slice of that estate where our servers will actually live.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gzgscrhb2nfn2itwy88.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%2F9gzgscrhb2nfn2itwy88.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Simple English tip:&lt;/strong&gt; Resource Group = the folder. VNet = the private estate. Subnet = the specific street inside it. Our servers move in next.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the land prepped, we're ready for the main event.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Create the Virtual Machine Scale Set (VMSS) 🖥️🖥️🖥️
&lt;/h2&gt;

&lt;p&gt;This is the star of the show. A &lt;strong&gt;VMSS&lt;/strong&gt; doesn't create just &lt;em&gt;one&lt;/em&gt; server — it creates a &lt;em&gt;template&lt;/em&gt; for identical servers that Azure can multiply or shrink automatically based on demand.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 — Find and open VMSS
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Search &lt;strong&gt;"vmss"&lt;/strong&gt; in the top search bar and select &lt;strong&gt;Virtual machine scale sets&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa6lt9dixkw5ex1x9v1iv.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%2Fa6lt9dixkw5ex1x9v1iv.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg0ydcaieagisn1pqtf0q.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%2Fg0ydcaieagisn1pqtf0q.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 — Basics: name, region, and zones
&lt;/h3&gt;

&lt;p&gt;Fill in the resource group (&lt;strong&gt;Smyles-RG&lt;/strong&gt;), give the scale set a name (&lt;strong&gt;Smyles-vmms&lt;/strong&gt;), pick your region, and choose &lt;strong&gt;Zones 1, 2, 3&lt;/strong&gt; for high availability (this spreads your servers across different physical data centers, so one outage doesn't take everything down).&lt;/p&gt;

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

&lt;h3&gt;
  
  
  3.3 — Orchestration and scaling mode
&lt;/h3&gt;

&lt;p&gt;Choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration mode: Flexible&lt;/strong&gt; — the modern, more flexible way Azure manages your servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling mode: Autoscaling&lt;/strong&gt; — this is the whole point! Let Azure decide how many servers we need based on CPU usage&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  3.4 — Configure the scaling rules
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Configure&lt;/strong&gt; to fine-tune exactly &lt;em&gt;how&lt;/em&gt; the auto-scaling should behave.&lt;/p&gt;

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

&lt;p&gt;Click &lt;strong&gt;Add a scaling condition&lt;/strong&gt;, name it (&lt;strong&gt;Smyles-scaling-condition&lt;/strong&gt;), and set the instance limits — I chose a &lt;strong&gt;minimum of 2&lt;/strong&gt; and a &lt;strong&gt;maximum of 5&lt;/strong&gt; servers.&lt;/p&gt;

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

&lt;p&gt;Then define the actual trigger rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scale out&lt;/strong&gt; (add a server) when CPU usage goes &lt;strong&gt;above 80%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale in&lt;/strong&gt; (remove a server) when CPU usage drops &lt;strong&gt;below 20%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Save it, and you'll see both the &lt;strong&gt;Default condition&lt;/strong&gt; and our new &lt;strong&gt;Smyles-scaling-condition&lt;/strong&gt; sitting side by side.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;In plain English:&lt;/strong&gt; "If the servers are working too hard (above 80% CPU), add a helper. If they're bored (below 20% CPU), send a helper home. Never go below 2 or above 5 helpers."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.5 — Instance details: OS, size, and count
&lt;/h3&gt;

&lt;p&gt;Set the instance count to &lt;strong&gt;5&lt;/strong&gt;, choose the image — I picked &lt;strong&gt;Ubuntu Server 24.04 LTS&lt;/strong&gt; — and select a size. I went with the budget-friendly &lt;strong&gt;Standard B1ms&lt;/strong&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  3.6 — Admin account
&lt;/h3&gt;

&lt;p&gt;Create a login for the servers. I used the username &lt;strong&gt;smyles&lt;/strong&gt; with a password (SSH keys work too, if you prefer).&lt;/p&gt;

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

&lt;h3&gt;
  
  
  3.7 — Networking: plug into our VNet
&lt;/h3&gt;

&lt;p&gt;Head to the &lt;strong&gt;Networking&lt;/strong&gt; tab. Select  &lt;strong&gt;Smyles-VN&lt;/strong&gt; and &lt;strong&gt;Smyles-Subnet&lt;/strong&gt;. Click &lt;strong&gt;Create new nic&lt;/strong&gt; to set up the network card for our servers.&lt;/p&gt;

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

&lt;p&gt;Name the NIC (&lt;strong&gt;Smyles-Networkinterface&lt;/strong&gt;), keep the network security group on &lt;strong&gt;Basic&lt;/strong&gt;, and allow &lt;strong&gt;HTTP (port 80)&lt;/strong&gt; so the outside world can actually reach our website.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  3.8 — Add a Load Balancer
&lt;/h3&gt;

&lt;p&gt;Still on the Networking tab, choose &lt;strong&gt;Azure load balancer&lt;/strong&gt;, then click &lt;strong&gt;Create a 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F36rmi0crjhcu2thucc64.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%2F36rmi0crjhcu2thucc64.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Name it &lt;strong&gt;Smyles-loadbalancer&lt;/strong&gt;, set the type to &lt;strong&gt;Public&lt;/strong&gt; (so it has an internet-facing address), and the protocol to &lt;strong&gt;TCP&lt;/strong&gt;. Click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Simple English tip:&lt;/strong&gt; A load balancer is like a receptionist. Visitors don't talk to individual servers — they talk to the receptionist, who quietly sends them to whichever server is free.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3.9 — Health monitoring
&lt;/h3&gt;

&lt;p&gt;Switch to the &lt;strong&gt;Health&lt;/strong&gt; tab and enable &lt;strong&gt;Application health monitoring&lt;/strong&gt;. This lets Azure automatically detect and replace any server that stops responding.&lt;/p&gt;

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

&lt;p&gt;Click &lt;strong&gt;Configure&lt;/strong&gt; and set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protocol: &lt;strong&gt;HTTP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Port: &lt;strong&gt;80&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Request path: &lt;strong&gt;/&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;h3&gt;
  
  
  3.10 — Custom Data (the magic script)
&lt;/h3&gt;

&lt;p&gt;This is where the servers get their personality. In the &lt;strong&gt;Advanced&lt;/strong&gt; tab, under &lt;strong&gt;Custom data and cloud-init&lt;/strong&gt;, I pasted a small Bash script that automatically installs a web server and creates a webpage — the moment each server boots up, it's already ready to greet visitors.&lt;/p&gt;

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

&lt;p&gt;Here's the exact script I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &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 &lt;span class="nb"&gt;enable &lt;/span&gt;apache2
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start apache2
&lt;span class="nv"&gt;HOSTNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; | sudo tee /var/www/html/index.html
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;body style="font-family:Arial;text-align:center;padding-top:100px;background:#f4f4f4"&amp;gt;
&amp;lt;h1 style="color:#0078D4"&amp;gt;Hello from &lt;/span&gt;&lt;span class="nv"&gt;$HOSTNAME&lt;/span&gt;&lt;span class="sh"&gt;&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Azure VMSS Instance&amp;lt;/p&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this script does, in plain English:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Line&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apt-get update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Refreshes the list of available software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;apt-get install apache2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Installs Apache — a popular web server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl enable/start apache2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Turns the web server on, and keeps it on after reboots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;HOSTNAME=$(hostname)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Grabs the unique name of &lt;em&gt;this particular&lt;/em&gt; server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF ... EOF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes a simple HTML webpage that greets visitors with the server's own name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;systemctl restart apache2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restarts the web server so the new page shows up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cool part? Because this script runs on &lt;strong&gt;every single instance&lt;/strong&gt; in the scale set, each server will proudly display its &lt;strong&gt;own unique name&lt;/strong&gt; — so when we later refresh the load balancer's page, we can literally watch traffic bounce between different servers!&lt;/p&gt;

&lt;h3&gt;
  
  
  3.11 — Review and create
&lt;/h3&gt;

&lt;p&gt;With everything configured, click &lt;strong&gt;Review + create&lt;/strong&gt;. Wait for the green &lt;strong&gt;"Validation passed"&lt;/strong&gt; banner, then click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;Sit back and relax while Azure spins up &lt;strong&gt;5 identical virtual machines&lt;/strong&gt; for you. ☕&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 4: See It Come Alive 🌍
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 — Grab the load balancer's address
&lt;/h3&gt;

&lt;p&gt;Open your new scale set (&lt;strong&gt;Smyles-vmss&lt;/strong&gt;) and go to &lt;strong&gt;Networking → Load balancing&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;Copy the &lt;strong&gt;Frontend IP address&lt;/strong&gt; — this is the single public address that represents &lt;em&gt;all five&lt;/em&gt; of your servers.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  4.2 — Paste it in your browser
&lt;/h3&gt;

&lt;p&gt;Paste that IP address into a new browser tab and hit enter...&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;"Hello from smyles-vmR4TV8G"&lt;/strong&gt; — a real, working, auto-scaling web app, built entirely from the ground up! Refresh the page a few times and watch the hostname change as the load balancer sends you to different servers behind the scenes. That's auto-scaling infrastructure in action. 🎉&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Clean Up — Don't Leave the Lights On! 💡
&lt;/h2&gt;

&lt;p&gt;Cloud resources cost money for every hour they exist, even if nobody's visiting your website. Once you're done experimenting, tear it all down — and thanks to our Resource Group trick from Step 2 (or see the full explanation in my earlier Resource Group post), this takes seconds.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go back to &lt;strong&gt;Resource groups&lt;/strong&gt; and open &lt;strong&gt;Smyles-RG&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4gxazdjd6d4lzfj0dwsf.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%2F4gxazdjd6d4lzfj0dwsf.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Delete resource group&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnninyxawcuexe3i5vsfm.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%2Fnninyxawcuexe3i5vsfm.png" alt=" " width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Type the resource group name to confirm (Azure double-checks so you don't delete anything by accident), then click &lt;strong&gt;Delete&lt;/strong&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fionxvzgalnjdl26k2ys0.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%2Fionxvzgalnjdl26k2ys0.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And just like that — the VMs, load balancer, network, subnet, and everything else vanish, and your billing stops. One folder in, one folder out. Also do the same for &lt;strong&gt;&lt;em&gt;NetworkWatcherRG&lt;/em&gt;&lt;/strong&gt; 🧹&lt;/p&gt;

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




&lt;h2&gt;
  
  
  🎯 What We Actually Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource Groups&lt;/strong&gt; keep your projects tidy and easy to delete&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Networks &amp;amp; Subnets&lt;/strong&gt; give your servers a private, controlled space to live in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VMSS with Autoscaling&lt;/strong&gt; means your infrastructure grows and shrinks with real demand — you stop paying for idle servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load Balancers&lt;/strong&gt; spread traffic so no single server gets overwhelmed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Data / cloud-init&lt;/strong&gt; lets you automate server setup completely — no manual SSH-ing into five different machines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health monitoring&lt;/strong&gt; keeps your app resilient by auto-replacing unhealthy instances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the entire journey — from an empty Azure subscription to a self-healing, self-scaling website, torn down responsibly at the end. If you followed along, you didn't just read about cloud infrastructure — you built it. 🙌&lt;/p&gt;

&lt;p&gt;Got questions or stuck somewhere? &lt;strong&gt;Drop a comment below&lt;/strong&gt; — happy to help troubleshoot!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow me for more hands-on Azure and cloud engineering walkthroughs, straight from my own learning journey.&lt;/em&gt; ☁️&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>developer</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Built My First Azure Virtual Machine From Scratch and Hosted a Website Using Apache2 🌐</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Sat, 20 Jun 2026 15:15:00 +0000</pubDate>
      <link>https://dev.to/4thman/i-built-my-first-azure-vm-from-scratch-and-hosted-a-live-website-1ob0</link>
      <guid>https://dev.to/4thman/i-built-my-first-azure-vm-from-scratch-and-hosted-a-live-website-1ob0</guid>
      <description>&lt;h1&gt;
  
  
  Building My First Azure Virtual Machine From the Ground Up 🚀
&lt;/h1&gt;

&lt;p&gt;In this post, I'll walk you through &lt;strong&gt;exactly&lt;/strong&gt; how I went from an empty Azure subscription to a live Ubuntu VM running Apache, complete with every screen I clicked through. No skipped steps, no "and then magic happens."&lt;/p&gt;

&lt;p&gt;By the end, you'll be able to do this yourself in under 20 minutes. Let's go! 👇&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💬 &lt;strong&gt;Heads up:&lt;/strong&gt; This is a follow-along tutorial. Grab a coffee, open a second tab with your Azure portal, and build alongside me.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  ✅ What You'll Build
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[x] A &lt;strong&gt;Resource Group&lt;/strong&gt; to hold everything together&lt;/li&gt;
&lt;li&gt;[x] A &lt;strong&gt;Virtual Network (VNet)&lt;/strong&gt; with a custom address space&lt;/li&gt;
&lt;li&gt;[x] A &lt;strong&gt;Subnet&lt;/strong&gt; inside that VNet&lt;/li&gt;
&lt;li&gt;[x] A &lt;strong&gt;Virtual Machine&lt;/strong&gt; (Ubuntu 24.04 LTS) deployed into that subnet&lt;/li&gt;
&lt;li&gt;[x] SSH access into the VM from VS Code's terminal&lt;/li&gt;
&lt;li&gt;[x] A live &lt;strong&gt;Apache2 web server&lt;/strong&gt; you can visit in your browser&lt;/li&gt;
&lt;li&gt;[x] A clean teardown so you don't get billed for forgetting it 💸&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;An Azure account (free tier works fine)&lt;/li&gt;
&lt;li&gt;A terminal — I used &lt;strong&gt;VS Code's integrated terminal (bash/Git Bash)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;15–20 minutes and a bit of curiosity&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🗺️ The Architecture (in plain English)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Resource Group  (the folder that holds everything)
   └── Virtual Network   (10.0.0.0/16)
         └── Subnet      (10.0.0.0/24)
               └── Virtual Machine  (Ubuntu 24.04, public IP, SSH + HTTP open)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resource Group first, then network, then subnet, then the actual VM goes &lt;em&gt;into&lt;/em&gt; that subnet. Let's build it top-down.&lt;/p&gt;




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

&lt;p&gt;A Resource Group is basically a folder in Azure — it keeps every resource for this project (network, VM, disks, IP) organized together so you can manage or delete them as one unit.&lt;/p&gt;

&lt;p&gt;Firstly I login into the Azure Homepage via &lt;a href="https://portal.azure.com/" rel="noopener noreferrer"&gt;https://portal.azure.com/&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;From the Azure homepage, I searched for &lt;strong&gt;Resource Groups&lt;/strong&gt; and hit &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

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

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

&lt;p&gt;I named mine &lt;code&gt;VM-RG-Test&lt;/code&gt; and picked &lt;strong&gt;France Central&lt;/strong&gt; as my region — pick whatever region is closest to you.&lt;/p&gt;

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

&lt;p&gt;While on the Review + Create page, I clicked Create after reviewing the details of the Resource Group.&lt;/p&gt;

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

&lt;p&gt;A few seconds later — done! ✅&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Tip:&lt;/strong&gt; Whatever region you pick here is the one you should use for &lt;em&gt;every&lt;/em&gt; resource in this tutorial. Mixing regions adds latency and (sometimes) extra cost.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2: Create a Virtual Network (VNet) 🌐
&lt;/h2&gt;

&lt;p&gt;Next, your VM needs a network to live in. I searched for &lt;strong&gt;Virtual networks&lt;/strong&gt; &lt;/p&gt;

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

&lt;p&gt;and clicked &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;I assigned it to my &lt;code&gt;VM-RG-Test&lt;/code&gt; resource group and named it &lt;code&gt;VM-Virtual-Network&lt;/code&gt;, same region as before.&lt;/p&gt;

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

&lt;p&gt;On the &lt;strong&gt;Address space&lt;/strong&gt; tab, Azure gives you a default &lt;code&gt;10.0.0.0/16&lt;/code&gt; range with a default subnet already attached. I deleted that default subnet — I want to create my own with a name that actually makes sense — then hit &lt;strong&gt;Review + create&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;Validation passed, so I clicked &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;Once the deployment finished, I clicked &lt;strong&gt;Go to resource&lt;/strong&gt;.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 3: Add a Subnet 🧩
&lt;/h2&gt;

&lt;p&gt;A VNet without a subnet is just an empty container — the subnet is where your VM's network card actually attaches.&lt;/p&gt;

&lt;p&gt;Inside the VNet, I went to &lt;strong&gt;Settings → Subnets&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;And clicked &lt;strong&gt;Create&lt;/strong&gt; on the &lt;strong&gt;subnet&lt;/strong&gt; page.&lt;/p&gt;

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

&lt;p&gt;I named it &lt;code&gt;VM-Sub-Net&lt;/code&gt;, left the default &lt;code&gt;10.0.0.0/24&lt;/code&gt; range (251 usable IPs — plenty for one VM), and clicked &lt;strong&gt;Add&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;And there it is — one ready-to-use 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr6wvthba96v5rsr7uq38.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%2Fr6wvthba96v5rsr7uq38.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Create the Virtual Machine 🖥️
&lt;/h2&gt;

&lt;p&gt;This is the main event. I searched for &lt;strong&gt;Virtual machines&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;And clicked &lt;strong&gt;Create&lt;/strong&gt;, then selected the first option, which is &lt;strong&gt;Virtual Machine.&lt;/strong&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  4.1 — Basics: name, region, resource group
&lt;/h3&gt;

&lt;p&gt;I dropped this VM into my existing &lt;code&gt;VM-RG-Test&lt;/code&gt; resource group and named it &lt;code&gt;VM-Test&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  4.2 — Image, size, and authentication
&lt;/h3&gt;

&lt;p&gt;For the image, I went with &lt;strong&gt;Ubuntu Server 24.04 LTS - x64 Gen2&lt;/strong&gt;, kept the size small (&lt;code&gt;Standard_B1ls&lt;/code&gt;) to keep costs minimal, and chose &lt;strong&gt;SSH public key&lt;/strong&gt; as the authentication method instead of a password — much safer.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  4.3 — Username, SSH key, and inbound ports
&lt;/h3&gt;

&lt;p&gt;I set my username to &lt;code&gt;VM-David&lt;/code&gt;, let Azure &lt;strong&gt;generate a new SSH key pair&lt;/strong&gt; for me, named the key &lt;code&gt;VM-Test_key&lt;/code&gt;, and allowed inbound traffic on &lt;strong&gt;HTTP (80)&lt;/strong&gt; and &lt;strong&gt;SSH (22)&lt;/strong&gt; — HTTP so I can later view my website, SSH so I can connect and manage the server.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;⚠️ Allowing SSH from "any IP" is fine for a quick test like this, but for production you'd lock it down to your own IP address using the Advanced networking controls.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4.4 — Disks
&lt;/h3&gt;

&lt;p&gt;I kept the default &lt;strong&gt;Standard SSD&lt;/strong&gt; for the OS disk — good enough for a test server, no need for Premium SSD here.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  4.5 — Networking: plugging it into our VNet/Subnet
&lt;/h3&gt;

&lt;p&gt;This is where everything we built earlier comes together — I selected my &lt;code&gt;VM-Virtual-Network&lt;/code&gt; and &lt;code&gt;VM-Sub-Net&lt;/code&gt;, let Azure create a new public IP, and chose &lt;strong&gt;HTTP (80), SSH (22)&lt;/strong&gt; as the allowed inbound ports.&lt;/p&gt;

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

&lt;p&gt;I also checked &lt;strong&gt;"Delete public IP and NIC when VM is deleted"&lt;/strong&gt; — a small checkbox that saves you from leaving orphaned (and billable!) resources behind later.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  4.6 — Review + Create
&lt;/h3&gt;

&lt;p&gt;Validation passed (estimated cost: under $8/month for this tiny config 🙌), so I hit &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;Azure immediately prompted me to &lt;strong&gt;download the private key&lt;/strong&gt; — this is your &lt;em&gt;only&lt;/em&gt; chance to grab it, since Azure never stores the private half of the key pair.&lt;/p&gt;

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

&lt;p&gt;And there's my &lt;code&gt;.pem&lt;/code&gt; key, safely sitting in my Downloads folder.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;🔐 &lt;strong&gt;Don't lose this file.&lt;/strong&gt; Without it, you can't SSH into your VM. Treat it like a password.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A minute or two later, the deployment finished.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 5: Confirm the VM Is Alive ✅
&lt;/h2&gt;

&lt;p&gt;On the VM's overview page, I could see its public IP, private IP, region, and size — and the status read &lt;strong&gt;Running&lt;/strong&gt;.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 6: SSH In and Install Apache 🔧
&lt;/h2&gt;

&lt;p&gt;Time to actually log into the server. I opened VS Code's terminal then swicthed to Bash from Powershell(you can use Git-bash also), navigated to my Downloads folder (where the key lives), locked down the key's permissions, and connected.&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;# 1. Go to where the key was downloaded&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;Downloads

&lt;span class="c"&gt;# 2. Restrict permissions on the key (required by SSH)&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;400 VM-Test_key.pem
&lt;span class="c"&gt;# (chmod 600 also works on most systems)&lt;/span&gt;

&lt;span class="c"&gt;# 3. Connect to the VM using the key, username, and public IP&lt;/span&gt;
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; VM-Test_key.pem VM-David@98.66.137.84

&lt;span class="c"&gt;# 4. Update package lists&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update

&lt;span class="c"&gt;# 5. Install Apache2 web server&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;apache2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time you connect, SSH will ask if you trust the host's fingerprint — type &lt;code&gt;yes&lt;/code&gt; to continue. Running &lt;code&gt;whoami&lt;/code&gt; afterward confirmed I was logged in successfully.&lt;/p&gt;

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

&lt;p&gt;After updating, I installed Apache2:&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 7: See It Live in the Browser 🌍
&lt;/h2&gt;

&lt;p&gt;With Apache installed and running, I opened a browser and typed in the VM's &lt;strong&gt;public IP address&lt;/strong&gt;. And there it was — the Apache2 default "It works!" page, served straight from my own Azure VM.&lt;/p&gt;

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

&lt;p&gt;This was honestly the most satisfying part — seeing a page load from infrastructure I built myself, end to end. 🎉&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Clean Up — Delete the Resource Group 🧹
&lt;/h2&gt;

&lt;p&gt;Azure bills you for running resources, so once I was done testing, I deleted the whole resource group. Because everything (VM, disk, NIC, public IP, SSH key, VNet) was inside &lt;code&gt;VM-RG-Test&lt;/code&gt;, &lt;strong&gt;one deletion removes it all&lt;/strong&gt;.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;👀 Spot the extra one? There's also a NetworkWatcherRG sitting in that list. Azure creates this automatically the first time you use any network diagnostics feature in a region — I didn't make it on purpose. It doesn't cost anything by itself, but since I was cleaning house, I deleted it too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Azure shows you exactly what's about to be deleted — 7 resources in my case — and makes you type the resource group's name to confirm. No accidental deletions here.&lt;/p&gt;

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

&lt;p&gt;A couple minutes later, it was gone — clean slate, zero ongoing cost.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  🎯 Recap
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What Happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Created a Resource Group (&lt;code&gt;VM-RG-Test&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Created a Virtual Network (&lt;code&gt;VM-Virtual-Network&lt;/code&gt;, &lt;code&gt;10.0.0.0/16&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Added a Subnet (&lt;code&gt;VM-Sub-Net&lt;/code&gt;, &lt;code&gt;10.0.0.0/24&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Deployed an Ubuntu VM into that subnet with an SSH key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Connected via SSH from VS Code's terminal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Installed and ran Apache2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Visited the live page in a browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Deleted everything to avoid surprise charges&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  🗣️ Over to you
&lt;/h3&gt;

&lt;p&gt;Drop a comment below 👇 — and if this helped you, a like/follow goes a long way! 🙌&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Inside Azure Networking: How I Created a Virtual Network with Custom Subnets</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Thu, 04 Jun 2026 23:54:09 +0000</pubDate>
      <link>https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4</link>
      <guid>https://dev.to/4thman/inside-azure-networking-how-i-created-a-virtual-network-with-custom-subnets-35g4</guid>
      <description>&lt;p&gt;If you've ever stared at the Azure portal wondering where to even begin with networking, you're not alone. I was right there a few weeks ago. In this post, I'm walking you through exactly what I did — from creating a resource group to building a virtual network with three custom subnets. No fluff, just real steps with real screenshots.&lt;br&gt;
Let's get into it.&lt;/p&gt;

&lt;p&gt;Head over to portal.azure.com&lt;br&gt;
Once logged in,&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%2F7hq3gbn9sc4ccouc6ddd.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%2F7hq3gbn9sc4ccouc6ddd.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Search for Resource Groups&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first thing you want to do is head to the Azure portal and use the search bar at the top to look up "resource group". You'll see it pop up under Services — click on 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%2F0ehdhzvsvwh82om5ixty.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%2F0ehdhzvsvwh82om5ixty.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💡 Quick note: &lt;em&gt;I covered resource group creation in a previous blog too, but I'm including the steps here again so this post is fully self-contained. No jumping around needed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Create a New Resource Group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you're on the Resource Groups page, you'll see a list of any existing groups or an empty list with a create button visible on the page. To create a new one, click the + Create button at the top left.&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%2Fugt25ehdlmhw5ni9zesc.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%2Fugt25ehdlmhw5ni9zesc.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Fill in the Resource Group Details&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the creation form, you need to fill in three things:&lt;/p&gt;

&lt;p&gt;Subscription — select your active subscription (mine is Azure subscription 1)&lt;br&gt;
Resource group name — I named mine Netaccess&lt;br&gt;
Region — I chose (Africa) South Africa North since that's closest to me&lt;/p&gt;

&lt;p&gt;Once filled in, click Review + 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%2Ft3nt44ao99c0qvbzvcrd.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%2Ft3nt44ao99c0qvbzvcrd.png" alt=" " width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Review and Create the Resource Group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You'll be taken to a summary screen. Confirm the details look right — subscription, name, and region. Then hit the Create button at the bottom.&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%2Fmbvxamym9bwrtp5qdps6.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%2Fmbvxamym9bwrtp5qdps6.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5 — Resource Group Created!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once Azure finishes provisioning (it's pretty fast — usually a few seconds), you'll see a notification on the right confirming that the resource group was created successfully. Your new Netaccess group will now appear in the list.&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%2Fxconcxza0wj7kfmjtbw6.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%2Fxconcxza0wj7kfmjtbw6.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6 — Search for Virtual Networks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now that our resource group is ready, it's time to create a Virtual Network inside it. Use the top search bar again and type "virtual networks". Click on Virtual networks from the Services dropdown.&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%2Fm3i1chajv1rvyu86qhpo.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%2Fm3i1chajv1rvyu86qhpo.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7 — Create a Virtual Network&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You'll land on the Virtual Networks overview page. Since we don't have any yet, it'll say "No virtual networks to display." Click the + Create button (either at the top or the one in the centre of the page).&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Step 8 — Fill in the Virtual Network Basics&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the Basics tab of the creation wizard, configure the following:&lt;/p&gt;

&lt;p&gt;Subscription — Azure subscription 1&lt;br&gt;
Resource group — Select the Netaccess group we just created&lt;br&gt;
Virtual network name — I named it Netaccess-VM&lt;br&gt;
Region — (Africa) South Africa North&lt;/p&gt;

&lt;p&gt;Then click Next to move to the next tab.&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%2Ftxhndetaowch5f2i18ab.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%2Ftxhndetaowch5f2i18ab.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 9 — Configure Address Space and Delete the Default Subnet&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Skip past the Security tab (leave everything as default) and head to the Address space tab. Here Azure has already set up a default address space of 10.0.0.0/16 — that gives us 65,536 IP addresses, which is way more than enough.&lt;br&gt;
Now here's the important part: Azure also pre-creates a default subnet with the range 10.0.0.0 - 10.0.0.255 (/24). We're going to delete this because we want to create our own subnets with custom names and IP ranges.&lt;br&gt;
Click the trash/delete icon on the right side of the default subnet row to remove it.&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%2Ffvm925y995eb51lox0e2.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%2Ffvm925y995eb51lox0e2.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why delete the default subnet? Because accepting defaults means Azure names and organises things on your behalf. When you create your own subnets from scratch, you control the naming convention, IP ranges, and how traffic is logically segmented. Clean slate = cleaner architecture.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 10 — Review and Create the Virtual Network&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click through to the Review + create tab. You'll see a green "Validation passed" banner at the top — Azure confirming everything looks good. Take a moment to review the summary:&lt;/p&gt;

&lt;p&gt;Subscription: Azure subscription 1 ✅&lt;br&gt;
Resource Group: Netaccess ✅&lt;br&gt;
Name: Netaccess-VM ✅&lt;br&gt;
Region: South Africa North ✅&lt;br&gt;
Security (Bastion, Firewall, DDoS): all Disabled ✅&lt;/p&gt;

&lt;p&gt;Hit the Create button at the bottom.&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%2Fbbp18o2z0lgi7efks97g.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%2Fbbp18o2z0lgi7efks97g.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 11 — Deployment Complete — Click "Go to Resource"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Azure will now provision your virtual network. It usually takes just a minute or less. Once done, you'll land on the deployment overview showing "Your deployment is complete" with a green checkmark.&lt;br&gt;
Click Go to resource to open your newly created Netaccess-VM 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%2Fi07578ekvm5g13vqer3z.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%2Fi07578ekvm5g13vqer3z.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 12 — Explore the VNet Overview and Navigate to Subnets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You're now inside the Netaccess-VM virtual network overview page. You can see all the essentials — resource group, location, address space (10.0.0.0/16), and notice it currently shows 0 subnets (because we deleted the default one earlier).&lt;br&gt;
&lt;em&gt;&lt;strong&gt;In the left sidebar under Settings, click on Subnets.&lt;/strong&gt;&lt;/em&gt; This is where we'll build all three of our custom subnets.&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%2F3t3f7vlyqxidbjyh3kep.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%2F3t3f7vlyqxidbjyh3kep.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 13 — Click "+ Subnet" to Start Adding Subnets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once you're on the Subnets page, you'll see "No items found" — exactly as expected. Click the + Subnet button at the top of the page to open the "Add a subnet" panel on the right side.&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%2Fx7dh1mpss08bfzbs5ixc.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%2Fx7dh1mpss08bfzbs5ixc.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 14 — Add Subnet 1 (Netaccess-subnet1)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The "Add a subnet" panel slides in from the right. Fill in the following:&lt;/p&gt;

&lt;p&gt;Subnet purpose — Default&lt;br&gt;
Name — Netaccess-subnet1&lt;br&gt;
IPv4 address range — 10.0.0.0/16 (this is auto-populated from your VNet address space)&lt;br&gt;
Starting address — 10.0.0.0&lt;br&gt;
Size — /24 (256 addresses)&lt;/p&gt;

&lt;p&gt;This creates a subnet range of 10.0.0.0 - 10.0.0.255. Leave IPv6 unchecked and scroll down to click Add.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Step 15 — Add Subnet 2 (Netaccess-subnet2)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click + Subnet again to add the second subnet. You'll notice the panel now shows the existing subnet1 listed on the left — that's just confirmation it was saved. Fill in the new details:&lt;/p&gt;

&lt;p&gt;Name — Netaccess-subnet2&lt;br&gt;
Starting address — 10.0.1.0&lt;br&gt;
Size — /24 (256 addresses)&lt;/p&gt;

&lt;p&gt;Subnet range: 10.0.1.0 - 10.0.1.255. Click Save.&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%2Fpno3tel2g0g36fe213op.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%2Fpno3tel2g0g36fe213op.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice how we moved the third octet from .0.0 to .1.0 for the second subnet — that's how we keep each subnet in its own separate IP block within the same /16 space. No overlap, no conflict, no headaches so we do the same to create Netaccess-subnet3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 16 — All Three Subnets Created Successfully&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Repeat the same process one final time for the third subnet:&lt;/p&gt;

&lt;p&gt;Name — Netaccess-subnet3&lt;br&gt;
Starting address — 10.0.2.0&lt;br&gt;
Size — /24&lt;/p&gt;

&lt;p&gt;After saving, your Subnets page will now show all three subnets neatly listed with their IPv4 ranges highlighted:&lt;br&gt;
NameIPv4 RangeAvailable IPsNetaccess-subnet110.0.0.0/24251Netaccess-subnet210.0.1.0/24251Netaccess-subnet310.0.2.0/24251&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%2F9mwalupb6xdkqo6o1ycc.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%2F9mwalupb6xdkqo6o1ycc.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each subnet has 251 usable IP addresses — When a resource is created, it uses one IP address from its subnet, and that IP is no longer available in the virtual network's address range. However, IP usage is managed at the subnet level, so a subnet can run out of addresses while the virtual network still has many unused IPs available in other subnets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cleanup — Deleting the Resources&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once I was done with the lab, I cleaned everything up to avoid unnecessary charges. Here's how:&lt;br&gt;
Delete the Netaccess Resource Group&lt;br&gt;
Go back to Resource groups, select Netaccess, click Delete resource group, type the name to confirm, and hit Delete.&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%2F0gozx30n2bfk9q1k86gz.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%2F0gozx30n2bfk9q1k86gz.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Delete the NetworkWatcherRG&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Azure automatically creates a NetworkWatcherRG resource group when you deploy networking resources. Delete this one too — same process: select it, click Delete resource group, type the name, confirm.&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%2Fy41turp9hcn1y9hst16q.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%2Fy41turp9hcn1y9hst16q.png" alt=" " width="800" height="362"&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%2Fgm67fswtldp47qtg5soo.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%2Fgm67fswtldp47qtg5soo.png" alt=" " width="799" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All Resource Groups Deleted&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once both are gone, your resource groups list is clean again — back to just the ones you had before (in my case, only Je-mega-soft remains).&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%2F5eu0jx82kaqiyup9390c.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%2F5eu0jx82kaqiyup9390c.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the full recap of what we did:&lt;/p&gt;

&lt;p&gt;✅ Searched for and opened Resource Groups&lt;br&gt;
✅ Created the Netaccess resource group in South Africa North&lt;br&gt;
✅ Searched for and navigated to Virtual Networks&lt;br&gt;
✅ Filled in VNet Basics — name Netaccess-VM, resource group Netaccess&lt;br&gt;
✅ On the Address space tab, deleted the auto-generated default subnet&lt;br&gt;
✅ Reviewed and created the Virtual Network (validation passed)&lt;br&gt;
✅ Deployment completed — clicked Go to resource&lt;br&gt;
✅ Navigated to the Subnets section inside the VNet&lt;br&gt;
✅ Clicked + Subnet to start adding custom subnets&lt;br&gt;
✅ Created Netaccess-subnet1 → 10.0.0.0/24&lt;br&gt;
✅ Created Netaccess-subnet2 → 10.0.1.0/24&lt;br&gt;
✅ Created Netaccess-subnet3 → 10.0.2.0/24&lt;br&gt;
✅ Cleaned up both Netaccess and NetworkWatcherRG resource groups&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Matters&lt;/strong&gt;&lt;br&gt;
Understanding how to manually structure your subnets gives you real control over how traffic flows in your cloud environment. Instead of accepting defaults, you're deliberately choosing IP ranges — which becomes crucial when you start doing things like:&lt;/p&gt;

&lt;p&gt;Separating frontend and backend application tiers&lt;br&gt;
Connecting on-premises networks via VPN Gateway or ExpressRoute&lt;br&gt;
Setting up Network Security Groups (NSGs) with rules per subnet&lt;br&gt;
Deploying services that require dedicated subnets (like Azure Bastion or App Gateway)&lt;/p&gt;

&lt;p&gt;This is foundational Azure networking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If this helped you, drop a ❤️ or leave a comment — especially if you ran into something different in your own setup. I'd love to hear about it.&lt;br&gt;
Until next time — keep building! 🚀&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>cloud</category>
      <category>devops</category>
      <category>networking</category>
    </item>
    <item>
      <title>How I Hosted a Free Website on Azure Blob Storage (No Server Needed!)</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Wed, 03 Jun 2026 07:33:07 +0000</pubDate>
      <link>https://dev.to/4thman/how-i-hosted-a-free-website-on-azure-blob-storage-no-server-needed-188</link>
      <guid>https://dev.to/4thman/how-i-hosted-a-free-website-on-azure-blob-storage-no-server-needed-188</guid>
      <description>&lt;p&gt;Introduction&lt;/p&gt;

&lt;p&gt;So I wanted to host a website without paying for a web server or dealing with complex configurations. Turns out, Azure Blob Storage has a brilliant feature called Static Website Hosting that lets you serve HTML, CSS, and JavaScript files directly from a storage container — kind of like having a mini web host built into your cloud storage.&lt;br&gt;
In this post, I'll walk you through exactly what I did, step by step, with screenshots. The website template I used is called Nexus Brew — a cyberpunk-themed cafe website from Tooplate.com, which offers free HTML templates.&lt;br&gt;
By the end of this guide, your site will be live at a URL like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jewebs.blob.core.windows.net/$web/index.html" rel="noopener noreferrer"&gt;https://jewebs.blob.core.windows.net/$web/index.html&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Step 1 — Download the Nexus Brew Template from Tooplate&lt;br&gt;
Before jumping into Azure, the first thing I did was grab the website template.&lt;br&gt;
I went to tooplate.com — a site that offers clean, modern, free HTML templates built with Bootstrap and CSS. They have over 80 templates for startups, small businesses, and portfolios.&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%2F9oaul8dvl3lpde1dbvy0.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%2F9oaul8dvl3lpde1dbvy0.png" alt=" " width="799" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I searched for and found the Nexus Brew template.&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%2Fwo95sy46nf0hytyl7slu.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%2Fwo95sy46nf0hytyl7slu.png" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's a dark, cyberpunk-style cafe website template with features like:&lt;/p&gt;

&lt;p&gt;A hero banner section&lt;br&gt;
A tabbed menu showcase&lt;br&gt;
An about section with content and images&lt;br&gt;
A contact form&lt;br&gt;
Social media links&lt;br&gt;
Smooth scroll navigation and hover effects&lt;/p&gt;

&lt;p&gt;It looks super cool — very futuristic and modern. Perfect for learning!&lt;br&gt;
I clicked the Download button on the template page to grab the zip file.&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%2Fbf2l6t3knaps0uii1c3g.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%2Fbf2l6t3knaps0uii1c3g.png" alt=" " width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The downloaded file was named 2146_nexus_brew.zip and it landed in my Downloads folder.&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%2F7iqfc0x73sypl5lw9a74.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%2F7iqfc0x73sypl5lw9a74.png" alt=" " width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2 — Extract the Zip File&lt;br&gt;
After downloading, &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%2Fhool87a0qgc4xtub3d19.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%2Fhool87a0qgc4xtub3d19.png" alt=" " width="799" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I extracted the zip file. Inside the 2146_nexus_brew folder, you'll find these files:&lt;/p&gt;

&lt;p&gt;index.html — the main HTML page&lt;br&gt;
tooplate-nexus-brew.css — the stylesheet&lt;br&gt;
tooplate-nexus-scripts.js — the JavaScript file&lt;br&gt;
ABOUT THIS TEMPLATE.txt — info about the template&lt;br&gt;
images/ — a folder containing all the images used on the site&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%2F63945xkf8uy1nhqbnpml.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%2F63945xkf8uy1nhqbnpml.png" alt=" " width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are all the files I'll need to upload to Azure later.&lt;/p&gt;

&lt;p&gt;Step 3 — Find Resource Groups in the Azure Portal&lt;br&gt;
Now it's time to head over to portal.azure.com.&lt;br&gt;
Once logged in, &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%2Flpdk03oaelvhox71cxx2.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%2Flpdk03oaelvhox71cxx2.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used the search bar at the top and typed "resource group". In the dropdown that appeared, I clicked on Resource groups (the one listed under Services).&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%2Fkhkhagnfmrmrxs8dd2tp.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%2Fkhkhagnfmrmrxs8dd2tp.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is a Resource Group?&lt;br&gt;
Think of a Resource Group as a folder in Azure where you keep related things together. It helps you manage, organize, and delete resources (like storage accounts) as a unit.&lt;/p&gt;

&lt;p&gt;Step 4 — View the Existing Resource Group Created in the Last Blog&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%2F3onm6haapiqbvy4rvedd.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%2F3onm6haapiqbvy4rvedd.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After clicking Resource groups, I could see my existing resource group named Je-mega-soft which was created in our last blog, located in South Africa North under Azure subscription 1.&lt;br&gt;
I clicked on it to open it.&lt;/p&gt;

&lt;p&gt;Step 5 — Search for Storage Accounts&lt;br&gt;
Inside the Resource Manager section, I used the search bar again and typed "storage account". From the suggestions, I clicked on Storage accounts (the one listed under Services — not the classic version).&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%2Fok5ejeq5n7vtmvs0g41s.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%2Fok5ejeq5n7vtmvs0g41s.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This took me to the Storage center where I could see existing storage accounts and create new ones.&lt;/p&gt;

&lt;p&gt;Step 6 — Create a New Storage Account&lt;br&gt;
On the Storage center page, I clicked the + Create button at the top to start creating a new storage account.&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%2Ff8x69t6kqeoeitvfkrut.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%2Ff8x69t6kqeoeitvfkrut.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the Basics tab of the creation form, I filled in these details:&lt;/p&gt;

&lt;p&gt;Resource group: Je-mega-soft (selected from the dropdown)&lt;br&gt;
Storage account name: jewebs (this has to be globally unique — all lowercase, no special characters)&lt;br&gt;
Region: (Africa) South Africa North (I chose the region closest to me)&lt;br&gt;
Preferred storage type: Azure Blob Storage or Azure Data Lake Storage&lt;br&gt;
Performance: Standard (recommended for most use cases)&lt;br&gt;
Redundancy: Locally Redundant Storage (LRS) (the most affordable option — stores 3 copies of your data within one data center)&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%2F0wdjw1nizuxnrcj8n12j.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%2F0wdjw1nizuxnrcj8n12j.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why LRS? For a static website that's just for learning or low-traffic use, LRS is more than enough and the cheapest option. &lt;/p&gt;

&lt;p&gt;Step 7 — Enable Anonymous Access in the Security Tab&lt;br&gt;
After filling in the Basics, I clicked Next to navigate through the tabs until I reached the Security tab.&lt;br&gt;
Here, the most important setting to enable is:&lt;br&gt;
✅ Allow enabling anonymous access on individual containers&lt;br&gt;
I made sure this checkbox was ticked.&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%2Fk266z1ivbyo2u6vqpk4y.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%2Fk266z1ivbyo2u6vqpk4y.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The red arrow in my screenshot points directly to this option — don't miss it!&lt;/p&gt;

&lt;p&gt;Why is this important? Without enabling anonymous access, your static website files would be private by default. No one on the internet would be able to open your website. Enabling this allows the $web container (which Azure creates for static websites) to serve files publicly.&lt;/p&gt;

&lt;p&gt;Once done, I clicked Review + create, reviewed the settings, and clicked 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%2Fo8vue51we30iws35yd34.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%2Fo8vue51we30iws35yd34.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 8 — Deployment Complete! Go to Resource&lt;br&gt;
After a few seconds, the deployment finished and I saw the "Your deployment is complete" message.&lt;br&gt;
The notification bell at the top right also showed a green checkmark saying "Deployment succeeded" — the deployment was to resource group Je-mega-soft.&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%2Fy2ok07guzoeccsw49jr6.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%2Fy2ok07guzoeccsw49jr6.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I clicked the Go to resource button (there's one in the main panel and one in the notification panel — either works) to open the newly created storage account.&lt;/p&gt;

&lt;p&gt;Step 9 — Navigate to Static Website Settings&lt;br&gt;
Now I was inside the jewebs storage account dashboard.&lt;br&gt;
On the left sidebar, I scrolled down and expanded the Data management section. Inside it, I clicked on Static website.&lt;/p&gt;

&lt;p&gt;If you don't see Data management, look for it in the left panel — it's usually collapsed. The red arrow in my screenshot points directly to it.&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%2Fvetja4ofyalnp2ef5sfh.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%2Fvetja4ofyalnp2ef5sfh.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 10 — Enable the Static Website Feature&lt;br&gt;
On the Static website page, by default the feature is set to Disabled.&lt;br&gt;
I clicked on Enabled to turn it on.&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%2Fatqkpysovwuekwubx8dm.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%2Fatqkpysovwuekwubx8dm.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As soon as I enabled it, two input fields appeared:&lt;/p&gt;

&lt;p&gt;Index document name: nexus_brew.html&lt;br&gt;
Error document path: ERROR-404.html&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%2Flgycikyhx0gbbjw9kjns.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%2Flgycikyhx0gbbjw9kjns.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the page that shows when someone visits a URL that doesn't exist. You can create a simple 404 page or just use the default.&lt;/p&gt;

&lt;p&gt;After filling these in, I clicked Save.&lt;/p&gt;

&lt;p&gt;Step 11 — Settings Saved Successfully&lt;br&gt;
A green notification appeared in the top right corner saying "Successfully updated static website settings".&lt;br&gt;
On the same page, I could now see:&lt;/p&gt;

&lt;p&gt;The static website is Enabled&lt;br&gt;
A message saying: "An Azure Storage container has been created to host your static website."&lt;br&gt;
A link to the container: $web&lt;br&gt;
The Primary endpoint URL: &lt;a href="https://jewebs.z1.web.core.windows.net/" rel="noopener noreferrer"&gt;https://jewebs.z1.web.core.windows.net/&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%2Fbgob76pa9qnuomvfa3al.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%2Fbgob76pa9qnuomvfa3al.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What is $web? This is a special Azure container that's automatically created when you enable static website hosting. It's where you'll upload all your website files.&lt;/p&gt;

&lt;p&gt;I clicked on $web to open that container.&lt;/p&gt;

&lt;p&gt;Step 12 — Upload Files to the $web Container&lt;br&gt;
Inside the $web container, it was empty — showing "No items found".&lt;br&gt;
I clicked the Upload button at the top to open the Upload blob panel on the right side.&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%2Fmd1zvn3pbhg336kd9xk6.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%2Fmd1zvn3pbhg336kd9xk6.png" alt=" " width="799" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I then selected all the files from the extracted Nexus Brew folder:&lt;/p&gt;

&lt;p&gt;index.html&lt;br&gt;
tooplate-nexus-brew.css&lt;br&gt;
tooplate-nexus-scripts.js&lt;br&gt;
ABOUT THIS TEMPLATE.txt&lt;br&gt;
The images folder&lt;/p&gt;

&lt;p&gt;The upload panel showed "7 file(s) selected" (the images folder contains multiple image files, so the total count goes up).&lt;/p&gt;

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

&lt;p&gt;I clicked the Upload button in the panel to start the upload.&lt;/p&gt;

&lt;p&gt;Important tip: Make sure to upload the contents of the folder, not the folder itself. Azure Blob Storage needs the files at the root level of $web for the static website to work correctly. If you upload a folder as a subfolder, your index page won't load properly.&lt;/p&gt;

&lt;p&gt;Step 13 — Files Successfully Uploaded&lt;br&gt;
After the upload completed, the $web container now showed all my files:&lt;br&gt;
NameTypeSizeimagesFolder—ABOUT THIS TEMPLATE.txtBlock blob557 Bindex.htmlBlock blob16.8 KiBtooplate-nexus-brew.cssBlock blob24.78 KiBtooplate-nexus-scripts.jsBlock blob3.52 KiB&lt;br&gt;
All 5 items were now visible and available in the container — uploaded on 6/2/2026 at 10:38 PM.&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%2Fx529ia06dk6tmty6mot5.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%2Fx529ia06dk6tmty6mot5.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 14 — Copy the URL of the Index File&lt;br&gt;
To get the direct URL of the uploaded index.html file, I:&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%2Ffrubzql0xzz50s5cesza.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%2Ffrubzql0xzz50s5cesza.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicked the three-dot menu (...) next to index.html&lt;br&gt;
Selected Copy URL from the context menu&lt;/p&gt;

&lt;p&gt;The URL I got was:&lt;br&gt;
&lt;a href="https://jewebs.blob.core.windows.net/$web/index.html" rel="noopener noreferrer"&gt;https://jewebs.blob.core.windows.net/$web/index.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: There are actually two URLs you can use:&lt;/p&gt;

&lt;p&gt;Blob URL (direct file): &lt;a href="https://jewebs.blob.core.windows.net/$web/index.html" rel="noopener noreferrer"&gt;https://jewebs.blob.core.windows.net/$web/index.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Static website URL (cleaner): &lt;a href="https://jewebs.z1.web.core.windows.net/" rel="noopener noreferrer"&gt;https://jewebs.z1.web.core.windows.net/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Website Is Live! 🎉&lt;/p&gt;

&lt;p&gt;That's it! The Nexus Brew template is now hosted live on Azure Blob Storage as a static website.&lt;br&gt;
You can visit it at:&lt;br&gt;
👉 &lt;a href="https://jewebs.blob.core.windows.net/$web/index.html" rel="noopener noreferrer"&gt;https://jewebs.blob.core.windows.net/$web/index.html&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%2F7f0kr6g51hytz5hgpya5.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%2F7f0kr6g51hytz5hgpya5.png" alt=" " width="800" height="407"&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%2Fsoqwtum6shyrtstktf8a.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%2Fsoqwtum6shyrtstktf8a.png" alt=" " width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;           Quick Summary of All Steps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here's a quick recap of everything we did:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Downloaded the Nexus Brew free HTML template from tooplate.com&lt;/li&gt;
&lt;li&gt;Extracted the zip file and noted the files inside&lt;/li&gt;
&lt;li&gt;Open the Azure Portal and search for Resource groups in the Azure portal&lt;/li&gt;
&lt;li&gt;Opened the existing Je-mega-soft resource group which was previously showed on our last blog how to create.&lt;/li&gt;
&lt;li&gt;Searched for Storage accounts and clicked Create&lt;/li&gt;
&lt;li&gt;Configured the storage account with name jewebs, region South Africa North, Standard performance, and LRS redundancy&lt;/li&gt;
&lt;li&gt;In the Security tab, enabled Allow anonymous access on containers&lt;/li&gt;
&lt;li&gt;Deployed and clicked Go to resource&lt;/li&gt;
&lt;li&gt;Navigated to Data management → Static website&lt;/li&gt;
&lt;li&gt;Enabled static website hosting and set the index document name&lt;/li&gt;
&lt;li&gt;Saved settings and noted the $web container and primary endpoint URL&lt;/li&gt;
&lt;li&gt;Opened the $web container and uploaded all website files&lt;/li&gt;
&lt;li&gt;Confirmed all files were uploaded successfully&lt;/li&gt;
&lt;li&gt;Copied the URL and opened the live websit
e&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why I Loved This Approach&lt;/p&gt;

&lt;p&gt;Honestly, this whole setup took less than 15 minutes and didn't require me to set up a web server, configure Nginx or Apache, deal with SSL certificates manually, or pay for a VM. Azure handles everything for you and even gives you HTTPS for free through the static website endpoint.&lt;br&gt;
For simple websites — portfolios, landing pages, documentation sites, or just practice — this is one of the easiest and cheapest ways to get something live on the internet.&lt;/p&gt;

&lt;p&gt;Resources Used:&lt;/p&gt;

&lt;p&gt;Tooplate.com — Free HTML templates&lt;br&gt;
Azure Static Website Hosting Docs&lt;br&gt;
Azure Account.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Step-by-Step Guide to Azure Blob Storage: Uploading Files, Enabling Public Access, and Secure SAS Link Generation</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Thu, 28 May 2026 22:09:47 +0000</pubDate>
      <link>https://dev.to/4thman/step-by-step-guide-to-azure-blob-storage-uploading-files-enabling-public-access-and-secure-sas-21lm</link>
      <guid>https://dev.to/4thman/step-by-step-guide-to-azure-blob-storage-uploading-files-enabling-public-access-and-secure-sas-21lm</guid>
      <description>&lt;p&gt;In this article, I will walk through how I created an Azure Storage account, configured Blob Storage, uploaded a file, made it publicly accessible, and finally generated a Shared Access Signature (SAS) link for secure access.&lt;/p&gt;

&lt;p&gt;This is part of my hands-on learning journey in Cloud Computing and DevOps using Microsoft Azure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating the Azure Storage Account&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%2F292c1x73zol1n02flu2f.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%2F292c1x73zol1n02flu2f.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first step was to create a Storage Account in Microsoft Azure. This is the core service that provides a scalable location for storing files such as images, videos, documents, and backups.&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%2Fv3rqaa8u9tfctb2tztmk.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%2Fv3rqaa8u9tfctb2tztmk.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configuration used:&lt;br&gt;
Resource Group: je-mega-soft&lt;br&gt;
Storage Account Name: prodjemegastorage1&lt;br&gt;
Region: Africa (South Africa North)&lt;br&gt;
Primary Service: Azure Blob Storage&lt;br&gt;
Performance: Standard&lt;br&gt;
Redundancy: Locally Redundant Storage (LRS)&lt;br&gt;
Access Tier: Hot&lt;br&gt;
Allow Blob Public Access: Enabled (on container level)&lt;br&gt;
Explanation of each choice:&lt;/p&gt;

&lt;p&gt;Resource Group (je-mega-soft):&lt;br&gt;
A resource group is simply a logical container in Azure that holds related resources. I used this to group everything related to my project for easy management and cleanup.&lt;/p&gt;

&lt;p&gt;Storage Account Name (prodjemegastorage1):&lt;br&gt;
This must be globally unique across Azure. I used a structured name to represent a production-style storage setup.&lt;/p&gt;

&lt;p&gt;Region (Africa – South Africa North):&lt;br&gt;
I selected this region because it is geographically closer to my location, which reduces latency and improves performance when accessing resources.&lt;/p&gt;

&lt;p&gt;Primary Service (Blob Storage):&lt;br&gt;
Blob Storage is designed for storing unstructured data like images, videos, logs, and backups. It is the best choice for this type of project.&lt;/p&gt;

&lt;p&gt;Performance (Standard):&lt;br&gt;
Standard performance was chosen because this is a learning/demo project. It is cost-effective and suitable for general-purpose storage. Premium would only be needed for high-speed enterprise workloads.&lt;/p&gt;

&lt;p&gt;Redundancy (Locally Redundant Storage - LRS):&lt;br&gt;
LRS stores data in a single data center but keeps multiple copies within that facility. I chose this because it is the cheapest option and perfect for testing and learning purposes.&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%2Finvo9pynj3bcm2fk23ah.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%2Finvo9pynj3bcm2fk23ah.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Access Tier (Hot):&lt;br&gt;
The Hot tier is used for data that is accessed frequently. Since I planned to upload and access the file during testing and sharing, this was the best fit.&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%2Fdxaampdlyienp7lritnw.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%2Fdxaampdlyienp7lritnw.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Public Access Enabled:&lt;br&gt;
I enabled public access at the container level because I wanted the uploaded file to be accessible via a URL without authentication.&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%2F09ooc2xjtlwa1bju3eu3.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%2F09ooc2xjtlwa1bju3eu3.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After reviewing all settings, I clicked Create and waited for deployment to complete.&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%2Fbzmnpeal1ahvqkoj6c8o.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%2Fbzmnpeal1ahvqkoj6c8o.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;After the storage account was successfully created, I moved to the next step which is creating a container.&lt;/p&gt;

&lt;p&gt;Path:&lt;br&gt;
Storage Account → Data Storage → Containers → + Add Container&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%2F4gzu7t4q2ee7027f4unl.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%2F4gzu7t4q2ee7027f4unl.png" alt=" " width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Container configuration:&lt;br&gt;
Name: jemega-img-vid&lt;br&gt;
Public Access Level: Container (anonymous read access for containers and blobs)&lt;br&gt;
Why I used this container name:&lt;/p&gt;

&lt;p&gt;I chose jemega-img-vid because:&lt;/p&gt;

&lt;p&gt;“jemega” represents my personal/project brand name&lt;br&gt;
“img-vid” clearly indicates the container is meant for images and videos&lt;br&gt;
It is short, readable, and meaningful for future scaling&lt;br&gt;
Public Access Level explanation:&lt;/p&gt;

&lt;p&gt;Setting the access level to Container means:&lt;/p&gt;

&lt;p&gt;Files inside the container can be publicly accessed&lt;br&gt;
Anyone with the link can view blobs without authentication&lt;br&gt;
This is useful for demos, static file hosting, and learning scenarios&lt;br&gt;
About the $logs container:&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%2Fqp4sw7xu37kylm5zfp0g.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%2Fqp4sw7xu37kylm5zfp0g.png" alt=" " width="799" height="358"&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%2Fqxhg1zuzdlltdjhmb3ii.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%2Fqxhg1zuzdlltdjhmb3ii.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;During setup, Azure automatically created a container called $logs.&lt;/p&gt;

&lt;p&gt;This container is used for system-generated logs such as:&lt;/p&gt;

&lt;p&gt;Storage analytics logs&lt;br&gt;
Request tracking logs&lt;br&gt;
Diagnostic monitoring data&lt;/p&gt;

&lt;p&gt;It is not meant for user files, so I did not modify or use it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uploading the File to Blob Storage&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%2F8o550f0qqwk6ho6bygma.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%2F8o550f0qqwk6ho6bygma.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, I uploaded a file into the container.&lt;/p&gt;

&lt;p&gt;File uploaded:&lt;br&gt;
just me.png&lt;/p&gt;

&lt;p&gt;Steps:&lt;/p&gt;

&lt;p&gt;Opened the container (jemega-img-vid)&lt;br&gt;
Clicked on Upload&lt;br&gt;
Selected the file from my local system&lt;br&gt;
Clicked Upload to store it in Azure Blob Storage&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%2F2mztiaqqh2zq3gujounx.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%2F2mztiaqqh2zq3gujounx.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once uploaded, Azure automatically generated a unique URL for the file.&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%2Fuy5qck2ho69oaqr3l9h2.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%2Fuy5qck2ho69oaqr3l9h2.png" alt=" " width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Blob URL generated:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://prodjemegastorage1.blob.core.windows.net/jemega-img-vid/just%20me.png" rel="noopener noreferrer"&gt;https://prodjemegastorage1.blob.core.windows.net/jemega-img-vid/just%20me.png&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Explanation:&lt;/p&gt;

&lt;p&gt;This URL directly points to the file inside the container. Because public access was enabled, the file can be accessed in a browser without authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating a Shared Access Signature (SAS)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To improve security while still allowing temporary access, I generated a Shared Access Signature (SAS).&lt;/p&gt;

&lt;p&gt;What is SAS?&lt;/p&gt;

&lt;p&gt;A SAS token is a secure way to grant limited access to storage resources without exposing your account keys. It allows you to define:&lt;/p&gt;

&lt;p&gt;What can be accessed&lt;br&gt;
How long access is valid&lt;br&gt;
What permissions are allowed&lt;br&gt;
Why I used Key 1:&lt;/p&gt;

&lt;p&gt;I used Key 1 from the storage account because it is one of the primary access keys used to generate SAS tokens. These keys should be protected because they grant full control over the storage account.&lt;/p&gt;

&lt;p&gt;SAS configuration used:&lt;br&gt;
Start Time: Today at 10:30 PM&lt;br&gt;
Expiry Time: 31st of this month at 12:03 PM&lt;br&gt;
Permissions: Read (sp=r)&lt;br&gt;
Protocol: HTTPS only&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%2Frn83pekint3jg5ejxhxj.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%2Frn83pekint3jg5ejxhxj.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why SAS is important:&lt;/p&gt;

&lt;p&gt;SAS provides:&lt;/p&gt;

&lt;p&gt;Temporary access instead of permanent public exposure&lt;br&gt;
Better security control over shared files&lt;br&gt;
Ability to revoke access by changing keys or expiry&lt;br&gt;
Fine-grained permissions (read, write, delete, etc.)&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Final SAS URL (Secure Access Link)&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Here is the final link generated using SAS:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://prodjemegastorage1.blob.core.windows.net/jemega-img-vid/just%20me.png?sp=r&amp;amp;st=2026-05-28T21:12:08Z&amp;amp;se=2026-05-31T11:03:08Z&amp;amp;spr=https&amp;amp;sv=2026-02-06&amp;amp;sr=b&amp;amp;sig=SseErml92AKi5t60kVuYCN8DNP%2BBkKm4CaXtHwhTqwo%3D" rel="noopener noreferrer"&gt;https://prodjemegastorage1.blob.core.windows.net/jemega-img-vid/just%20me.png?sp=r&amp;amp;st=2026-05-28T21:12:08Z&amp;amp;se=2026-05-31T11:03:08Z&amp;amp;spr=https&amp;amp;sv=2026-02-06&amp;amp;sr=b&amp;amp;sig=SseErml92AKi5t60kVuYCN8DNP%2BBkKm4CaXtHwhTqwo%3D&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This walkthrough helped me understand the basics of Azure Blob Storage, from creating a storage account and container to uploading a file and generating a SAS link for controlled access. I also learned how key settings like redundancy, access tier, and public access affect cost, performance, and security. Overall, it made cloud storage concepts clearer and gave me a solid foundation for working with Azure storage services going forward.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloud</category>
      <category>cloudcomputing</category>
      <category>devops</category>
    </item>
    <item>
      <title>I Created an Azure User, Gave It Global Admin Access, Revoked It, and Tested Azure IAM — Here’s What I Learned</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Fri, 22 May 2026 01:20:29 +0000</pubDate>
      <link>https://dev.to/4thman/i-created-an-azure-user-gave-it-global-admin-access-revoked-it-and-tested-azure-iam-heres-3p6p</link>
      <guid>https://dev.to/4thman/i-created-an-azure-user-gave-it-global-admin-access-revoked-it-and-tested-azure-iam-heres-3p6p</guid>
      <description>&lt;p&gt;As part of my Cloud Engineering and DevOps learning journey, I recently decided to go beyond theory and start practicing Identity and Access Management (IAM) directly inside Microsoft Azure.&lt;/p&gt;

&lt;p&gt;I wanted to understand how organizations manage users, permissions, administrative access, and security in real cloud environments. Instead of just reading about Microsoft Entra ID and RBAC, I performed a complete hands-on lab from start to finish.&lt;/p&gt;

&lt;p&gt;In this lab, I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;created a new Azure user&lt;/li&gt;
&lt;li&gt;configured Microsoft Authenticator&lt;/li&gt;
&lt;li&gt;assigned administrative roles&lt;/li&gt;
&lt;li&gt;logged into the new account using an incognito browser&lt;/li&gt;
&lt;li&gt;created another user using delegated access&lt;/li&gt;
&lt;li&gt;revoked the permissions&lt;/li&gt;
&lt;li&gt;tested whether the account could still perform administrative actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This exercise gave me a much deeper understanding of how cloud identity management works in enterprise environments.&lt;/p&gt;

&lt;p&gt;The screenshots attached throughout this post document the entire process step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting From the Default Directory
&lt;/h2&gt;

&lt;p&gt;I began by logging into the Azure Portal using my main administrator account inside the default directory.&lt;/p&gt;

&lt;p&gt;From there, I navigated to:&lt;/p&gt;

&lt;p&gt;Microsoft Entra ID → Users → All Users&lt;/p&gt;

&lt;p&gt;This section contains every identity managed inside the Azure environment, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;users&lt;/li&gt;
&lt;li&gt;groups&lt;/li&gt;
&lt;li&gt;applications&lt;/li&gt;
&lt;li&gt;permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, I already had administrative privileges, which allowed me to create and manage other users.&lt;/p&gt;

&lt;p&gt;One thing I’m beginning to understand about cloud platforms is that structure and organization are extremely important. Without proper identity management, large organizations would struggle to control access securely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the “Immortel” User
&lt;/h2&gt;

&lt;p&gt;The next step was creating a new user account named “Immortel.”&lt;/p&gt;

&lt;p&gt;During the setup process, I configured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;display name&lt;/li&gt;
&lt;li&gt;username&lt;/li&gt;
&lt;li&gt;password settings&lt;/li&gt;
&lt;li&gt;account information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After creating the account successfully, the new user appeared immediately inside the Azure users list.&lt;/p&gt;

&lt;p&gt;This helped me understand how organizations onboard employees into Microsoft Entra ID environments.&lt;/p&gt;

&lt;p&gt;At this stage, the account existed but had limited permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging Into the Immortel Account Using Incognito Mode
&lt;/h2&gt;

&lt;p&gt;Instead of signing out from my main administrator account, I opened a new incognito browser window and logged into the Immortel account separately.&lt;/p&gt;

&lt;p&gt;This allowed me to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remain logged into my administrator account&lt;/li&gt;
&lt;li&gt;test the Immortel account independently&lt;/li&gt;
&lt;li&gt;simulate how multiple administrators may work simultaneously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using incognito mode was actually very useful because it helped me manage both accounts side by side without interrupting my existing Azure session.&lt;/p&gt;

&lt;p&gt;At this stage, the Immortel account still had very limited access because no elevated administrative roles had been assigned yet.&lt;/p&gt;

&lt;p&gt;This demonstrated an important cloud security principle known as:&lt;/p&gt;

&lt;p&gt;Least Privilege Access&lt;/p&gt;

&lt;p&gt;This means users should only receive the minimum permissions necessary for their tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Microsoft Authenticator (MFA)
&lt;/h2&gt;

&lt;p&gt;While logging into the Immortel account, Azure required Multi-Factor Authentication (MFA) setup using Microsoft Authenticator.&lt;/p&gt;

&lt;p&gt;This process involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scanning a QR code&lt;/li&gt;
&lt;li&gt;linking the account to the Authenticator app&lt;/li&gt;
&lt;li&gt;approving sign-in requests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was one of the most practical parts of the exercise because it showed me how organizations secure user accounts beyond just passwords.&lt;/p&gt;

&lt;p&gt;I now understand why MFA is considered one of the most important security practices in cloud computing. Even if a password gets compromised, unauthorized users still cannot access the account without the additional verification step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assigning Global Administrator and Global Reader Roles
&lt;/h2&gt;

&lt;p&gt;After setting up the Immortel account successfully, I returned to my original administrator session inside the default directory and assigned two important roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global Administrator&lt;/li&gt;
&lt;li&gt;Global Reader&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This introduced me to Azure Role-Based Access Control (RBAC).&lt;/p&gt;

&lt;p&gt;Before this exercise, RBAC sounded theoretical while reading documentation. But seeing it work practically made everything much clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Global Administrator Role
&lt;/h2&gt;

&lt;p&gt;Global Administrator is one of the highest privileged roles inside Microsoft Entra ID.&lt;/p&gt;

&lt;p&gt;A user with this role can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create and manage users&lt;/li&gt;
&lt;li&gt;assign permissions&lt;/li&gt;
&lt;li&gt;manage identity settings&lt;/li&gt;
&lt;li&gt;configure security&lt;/li&gt;
&lt;li&gt;reset passwords&lt;/li&gt;
&lt;li&gt;control administrative settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In simple terms, this role has broad control over the Azure identity environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Global Reader Role
&lt;/h2&gt;

&lt;p&gt;Global Reader works differently.&lt;/p&gt;

&lt;p&gt;Users with this role can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;view configurations&lt;/li&gt;
&lt;li&gt;monitor environments&lt;/li&gt;
&lt;li&gt;review settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;but they cannot make administrative changes.&lt;/p&gt;

&lt;p&gt;This role is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;auditors&lt;/li&gt;
&lt;li&gt;compliance teams&lt;/li&gt;
&lt;li&gt;monitoring personnel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;who need visibility without modification access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logging Back Into the Immortel Session
&lt;/h2&gt;

&lt;p&gt;After assigning the roles, I refreshed and continued working inside the incognito browser session already logged into the Immortel account.&lt;/p&gt;

&lt;p&gt;The difference was immediately noticeable.&lt;/p&gt;

&lt;p&gt;The account now had elevated privileges and could access administrative capabilities that were unavailable earlier.&lt;/p&gt;

&lt;p&gt;This demonstrated how permissions in Azure are controlled dynamically through assigned roles.&lt;/p&gt;

&lt;p&gt;It also showed me how powerful RBAC is in enterprise cloud environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating Another User Named “Dare”
&lt;/h2&gt;

&lt;p&gt;To confirm the permissions were working correctly, I used the Immortel account to create another user named “Dare.”&lt;/p&gt;

&lt;p&gt;The creation process was successful.&lt;/p&gt;

&lt;p&gt;This confirmed that the Global Administrator privileges assigned earlier were active and functioning properly.&lt;/p&gt;

&lt;p&gt;At this point, I fully understood how delegated administration works in Azure environments.&lt;/p&gt;

&lt;p&gt;Organizations can temporarily assign elevated permissions to specific users without permanently granting full administrative control to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revoking the Administrative Access
&lt;/h2&gt;

&lt;p&gt;After completing the delegated administration test, I returned to my original administrator session inside the default directory.&lt;/p&gt;

&lt;p&gt;I then removed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global Administrator access&lt;/li&gt;
&lt;li&gt;Global Reader access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;from the Immortel account.&lt;/p&gt;

&lt;p&gt;This part of the exercise taught me another important IAM concept:&lt;/p&gt;

&lt;p&gt;Permissions can be granted and revoked at any time.&lt;/p&gt;

&lt;p&gt;This is critical in real-world environments for situations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;employee resignation&lt;/li&gt;
&lt;li&gt;temporary contractors&lt;/li&gt;
&lt;li&gt;security incidents&lt;/li&gt;
&lt;li&gt;role changes&lt;/li&gt;
&lt;li&gt;access reviews&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing the Revoked Account
&lt;/h2&gt;

&lt;p&gt;To confirm the permissions were truly removed, I returned to the incognito browser session using the Immortel account and attempted to create another user named “John.”&lt;/p&gt;

&lt;p&gt;This time, the operation failed.&lt;/p&gt;

&lt;p&gt;The account no longer had sufficient privileges to perform administrative tasks.&lt;/p&gt;

&lt;p&gt;This final test clearly demonstrated how Azure immediately enforces permission changes once access is revoked.&lt;/p&gt;

&lt;p&gt;It was satisfying seeing the entire IAM workflow function exactly as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Lab Taught Me
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;Microsoft Entra ID&lt;/li&gt;
&lt;li&gt;Identity and Access Management (IAM)&lt;/li&gt;
&lt;li&gt;RBAC&lt;/li&gt;
&lt;li&gt;Multi-Factor Authentication (MFA)&lt;/li&gt;
&lt;li&gt;delegated administration&lt;/li&gt;
&lt;li&gt;access control&lt;/li&gt;
&lt;li&gt;permission revocation&lt;/li&gt;
&lt;li&gt;cloud security principles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, it helped me realize that identity management is one of the most critical aspects of cloud security.&lt;/p&gt;

&lt;p&gt;Without proper IAM controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unauthorized access becomes easier&lt;/li&gt;
&lt;li&gt;security risks increase&lt;/li&gt;
&lt;li&gt;governance becomes difficult&lt;/li&gt;
&lt;li&gt;compliance becomes harder to maintain&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This was one of the most valuable Azure labs I’ve completed so far because it moved beyond theory into practical identity administration and security management.&lt;/p&gt;

&lt;p&gt;As I continue learning Cloud Engineering and DevOps, I’m beginning to appreciate how important hands-on practice is. Reading documentation is useful, but actually performing these tasks inside a real cloud environment creates a completely different level of understanding.&lt;/p&gt;

&lt;p&gt;I still have a long way to go, but each lab continues to build my confidence and understanding step by step.&lt;/p&gt;

&lt;p&gt;I’ll continue documenting my learning journey, projects, and hands-on experiences as I grow in cloud engineering and DevOps.&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%2F0354s6du370j94aoqq4s.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%2F0354s6du370j94aoqq4s.png" alt=" " width="799" height="351"&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%2Fogrogpzpm4vuwzixs3hz.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%2Fogrogpzpm4vuwzixs3hz.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flrfncpv26biyw0vp1few.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%2Flrfncpv26biyw0vp1few.png" alt=" " width="799" height="357"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ndaikyji8ld9q76wxft.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%2F9ndaikyji8ld9q76wxft.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4hnd362cy3vm6s2x7wr5.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%2F4hnd362cy3vm6s2x7wr5.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq9bqdtau01aj3jzmto0k.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%2Fq9bqdtau01aj3jzmto0k.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F11pkpka72ndmootlvwwr.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%2F11pkpka72ndmootlvwwr.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fel7d0f14cxfxlnwr3ltn.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%2Fel7d0f14cxfxlnwr3ltn.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fksf7g86ihc5kem0naf66.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%2Fksf7g86ihc5kem0naf66.png" alt=" " width="800" height="365"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb09cl7owdix7held8aiy.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%2Fb09cl7owdix7held8aiy.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdgib11lmqo3cz2hcpah8.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%2Fdgib11lmqo3cz2hcpah8.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hwhawzv5cwlncarztmm.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%2F0hwhawzv5cwlncarztmm.png" alt=" " width="800" height="362"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmueal9dsoammha3qa0xw.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%2Fmueal9dsoammha3qa0xw.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fau4711jka113w07wjqlq.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%2Fau4711jka113w07wjqlq.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvbn63efxx93vrq56tnlz.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%2Fvbn63efxx93vrq56tnlz.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz9h67wtwax86s3vo0c4g.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%2Fz9h67wtwax86s3vo0c4g.png" alt=" " width="800" height="360"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnklk2zh86z4040r4s1kt.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%2Fnklk2zh86z4040r4s1kt.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloud</category>
      <category>cloudcomputing</category>
      <category>webdev</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Thu, 14 May 2026 20:23:27 +0000</pubDate>
      <link>https://dev.to/4thman/-2cg4</link>
      <guid>https://dev.to/4thman/-2cg4</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj" class="crayons-story__hidden-navigation-link"&gt;Creating My First Azure Resource Group as I Begin My Journey Into Cloud Engineering and DevOps&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="/4thman" 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%2F3927892%2F43aac6d7-df74-4a90-bdbe-8ba55edf1a70.jpeg" alt="4thman profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/4thman" class="crayons-story__secondary fw-medium m:hidden"&gt;
              David Cletus
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                David Cletus
                
                
              
              &lt;div id="story-author-preview-content-3671755" 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="/4thman" 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%2F3927892%2F43aac6d7-df74-4a90-bdbe-8ba55edf1a70.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;David Cletus&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/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;May 14&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj" id="article-link-3671755"&gt;
          Creating My First Azure Resource Group as I Begin My Journey Into Cloud Engineering and DevOps
        &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/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&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/devops"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devops&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/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj" 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/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&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/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

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

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Creating My First Azure Resource Group as I Begin My Journey Into Cloud Engineering and DevOps</title>
      <dc:creator>David Cletus</dc:creator>
      <pubDate>Thu, 14 May 2026 20:18:29 +0000</pubDate>
      <link>https://dev.to/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj</link>
      <guid>https://dev.to/4thman/creating-my-first-azure-resource-group-as-i-begin-my-journey-into-cloud-engineering-and-devops-1cjj</guid>
      <description>&lt;p&gt;Today I took another practical step in my Cloud Engineering and DevOps learning journey by creating my first Resource Group on Microsoft Azure.&lt;/p&gt;

&lt;p&gt;As someone currently learning cloud computing and DevOps from scratch, this may seem like a small task, but it actually helped me understand how cloud infrastructure is organized in real-world environments.&lt;/p&gt;

&lt;p&gt;In the screenshot above, I was working inside the Azure Portal while creating a Resource Group called “Hagital-Resource-Group” and selecting the “South Africa North” region.&lt;/p&gt;

&lt;p&gt;This simple setup exposed me to important cloud concepts such as subscriptions, regions, resource organization, and infrastructure management.&lt;/p&gt;

&lt;p&gt;What is a Resource Group?&lt;/p&gt;

&lt;p&gt;A Resource Group in Azure acts like a container that holds related cloud resources together.&lt;/p&gt;

&lt;p&gt;For example, a company can keep:&lt;/p&gt;

&lt;p&gt;virtual machines&lt;br&gt;
databases&lt;br&gt;
storage accounts&lt;br&gt;
applications&lt;br&gt;
virtual networks&lt;/p&gt;

&lt;p&gt;inside one Resource Group instead of managing them separately.&lt;/p&gt;

&lt;p&gt;This makes resources easier to organize, monitor, secure, and manage.&lt;/p&gt;

&lt;p&gt;A simple way to think about it is like creating a folder on your computer for a particular project where all related files are stored together.&lt;/p&gt;

&lt;p&gt;What I Learned During This Setup&lt;/p&gt;

&lt;p&gt;While creating this Resource Group, I learned a few important things about how cloud platforms work.&lt;/p&gt;

&lt;p&gt;Subscriptions&lt;/p&gt;

&lt;p&gt;Azure uses subscriptions to manage billing and cloud services. Every resource created inside Azure belongs to a subscription.&lt;/p&gt;

&lt;p&gt;Regions&lt;/p&gt;

&lt;p&gt;Cloud providers do not keep all their infrastructure in one location. They build data centers across different countries and locations around the world called regions.&lt;/p&gt;

&lt;p&gt;I selected the “South Africa North” region because it is geographically closer to Africa and can help improve performance and reduce latency for users in this region.&lt;/p&gt;

&lt;p&gt;Resource Organization&lt;/p&gt;

&lt;p&gt;I also learned how organizations group cloud resources based on projects, departments, or applications. This becomes very important in large environments where thousands of resources may exist.&lt;/p&gt;

&lt;p&gt;Why I Started Learning Cloud Engineering and DevOps&lt;/p&gt;

&lt;p&gt;Technology is moving heavily toward cloud infrastructure and automation. Many organizations now rely on cloud providers like Microsoft Azure, AWS, and Google Cloud to run their systems and applications.&lt;/p&gt;

&lt;p&gt;I decided to start learning cloud engineering and DevOps because I want to understand modern infrastructure, automation, and how scalable systems are built and managed.&lt;/p&gt;

&lt;p&gt;Right now, I’m learning:&lt;/p&gt;

&lt;p&gt;cloud fundamentals&lt;br&gt;
Microsoft Azure&lt;br&gt;
Linux&lt;br&gt;
networking&lt;br&gt;
DevOps concepts&lt;br&gt;
virtual machines&lt;br&gt;
resource management&lt;/p&gt;

&lt;p&gt;I also plan to learn tools like Docker, Git, Kubernetes, Terraform, and CI/CD pipelines as I continue growing in this field.&lt;/p&gt;

&lt;p&gt;What I Learned Today&lt;/p&gt;

&lt;p&gt;Some of the key things I learned from this exercise include:&lt;/p&gt;

&lt;p&gt;how Azure Resource Groups work&lt;br&gt;
how subscriptions are used in Azure&lt;br&gt;
the importance of regions in cloud computing&lt;br&gt;
how cloud infrastructure is organized globally&lt;br&gt;
the value of hands-on practice while learning technology&lt;br&gt;
Final Thoughts&lt;/p&gt;

&lt;p&gt;This is still the beginning of my journey, but I’ve realized that small practical steps are important when learning cloud technologies.&lt;/p&gt;

&lt;p&gt;I plan to continue documenting my progress, sharing what I learn, and building projects along the way.&lt;/p&gt;

&lt;p&gt;There is still a lot to learn, but I’m excited about the process and looking forward to improving consistently over time.&lt;/p&gt;

&lt;h1&gt;
  
  
  cloudcomputing #azure #devops #cloudengineering #learninginpublic
&lt;/h1&gt;

</description>
      <category>azure</category>
      <category>beginners</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
